From 26905f350f35438a5a01baea096611e58250f165 Mon Sep 17 00:00:00 2001 From: minleejae Date: Sat, 24 Oct 2026 16:26:41 +0900 Subject: [PATCH] feat: support Oracle TRUNCATE storage and log options --- .../statement/truncate/Truncate.java | 43 +++++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 94 +++++++++++---- .../truncate/OracleTruncateTest.java | 108 ++++++++++++++++++ 3 files changed, 222 insertions(+), 23 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/truncate/OracleTruncateTest.java 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 34e03d17f6..81fc5cdddd 100644 --- a/src/main/java/net/sf/jsqlparser/statement/truncate/Truncate.java +++ b/src/main/java/net/sf/jsqlparser/statement/truncate/Truncate.java @@ -23,11 +23,21 @@ public enum IdentityOption { RESTART, CONTINUE } + public enum StorageOption { + DROP, DROP_ALL, REUSE + } + + public enum MaterializedViewLogOption { + PRESERVE, PURGE + } + 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) { @@ -107,6 +117,32 @@ public void setIdentityOption(IdentityOption option) { identityOption = option; } + 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; } @@ -151,6 +187,13 @@ public StringBuilder appendTo(StringBuilder builder) { } 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"); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2138c7eb68..ca8289ec01 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1868,17 +1868,7 @@ public class CCJSqlParser extends AbstractJSqlParser { * JOIN ONLY t or FROM a, ONLY b, rather than a table or column named only. */ private boolean isRelationOnlyAhead(Token prev) { - int next = getToken(2).kind; - 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 instead, as in (a INT, only INT) - return name; - default: - return false; - } + return RelationOptions.isOnlyAhead(prev, getToken(2).kind); } /** @@ -3313,6 +3303,48 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** 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"); @@ -5236,15 +5268,11 @@ LockStatement LockStatement(): { } LockStatement.Target LockTarget(): -{ TableReference reference; LockStatement.Target target; } +{ TableReference reference; } { reference=RelationTarget() { - target = new LockStatement.Target(reference.getTable()); - target.setOnly(reference.isOnly()); - target.setIncludeDescendants(reference.isIncludeDescendants()); - target.setParenthesized(reference.isParenthesized()); - return target; + return RelationOptions.lockTarget(reference); } } @@ -20023,18 +20051,38 @@ ReassignOwned ReassignOwned(): } Truncate Truncate(): -{ Truncate truncate = new Truncate(); TableReference target; } +{ + Truncate truncate = new Truncate(); + TableReference target; + String log = null, storage = null, identity = null, behavior = null; +} { [ LOOKAHEAD(2) { truncate.setTableToken(true); } ] target=RelationTarget() { truncate.getTargets().add(target); } ( LOOKAHEAD(2) "," target=RelationTarget() { truncate.getTargets().add(target); } )* - [ ( { truncate.setIdentityOption(Truncate.IdentityOption.RESTART); } - | ContextualKeyword("CONTINUE") { truncate.setIdentityOption(Truncate.IdentityOption.CONTINUE); } ) + [ + 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"; } ) ] - [ { truncate.setDropBehavior(CascadeBehavior.CASCADE); } - | { truncate.setDropBehavior(CascadeBehavior.RESTRICT); } ] - { return truncate; } + [ { behavior = "CASCADE"; } + | { behavior = "RESTRICT"; } ] + { + RelationOptions.apply(truncate, log, storage, identity, behavior); + return truncate; + } } /** 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()); + } +}