Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/main/java/net/sf/jsqlparser/statement/truncate/Truncate.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<TableReference> targets = new ArrayList<>();
private IdentityOption identityOption;
private CascadeBehavior dropBehavior;
private StorageOption storageOption;
private MaterializedViewLogOption materializedViewLogOption;

@Override
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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");
}
Expand Down
94 changes: 71 additions & 23 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1868,17 +1868,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
* 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);
}

/**
Expand Down Expand Up @@ -3313,6 +3303,48 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
}
}

/** 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");
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
{
<K_TRUNCATE>
[ LOOKAHEAD(2) <K_TABLE> { truncate.setTableToken(true); } ]
target=RelationTarget() { truncate.getTargets().add(target); }
( LOOKAHEAD(2) "," target=RelationTarget() { truncate.getTargets().add(target); } )*
[ ( <K_RESTART> { truncate.setIdentityOption(Truncate.IdentityOption.RESTART); }
| ContextualKeyword("CONTINUE") { truncate.setIdentityOption(Truncate.IdentityOption.CONTINUE); } )
[
LOOKAHEAD({ isKeywordAhead("PRESERVE") || getToken(1).kind == K_PURGE })
( ContextualKeyword("PRESERVE") { log = "PRESERVE"; }
| <K_PURGE> { log = "PURGE"; } )
<K_MATERIALIZED> <K_VIEW> <K_LOG>
]
[
LOOKAHEAD({ getToken(1).kind == K_DROP || isKeywordAhead("REUSE") })
( <K_DROP> { storage = "DROP"; }
[ <K_ALL> { storage = "DROP_ALL"; } ]
| ContextualKeyword("REUSE") { storage = "REUSE"; } )
<K_STORAGE>
]
[ ( <K_RESTART> { identity = "RESTART"; }
| ContextualKeyword("CONTINUE") { identity = "CONTINUE"; } )
<K_IDENTITY> ]
[ <K_CASCADE> { truncate.setDropBehavior(CascadeBehavior.CASCADE); }
| <K_RESTRICT> { truncate.setDropBehavior(CascadeBehavior.RESTRICT); } ]
{ return truncate; }
[ <K_CASCADE> { behavior = "CASCADE"; }
| <K_RESTRICT> { behavior = "RESTRICT"; } ]
{
RelationOptions.apply(truncate, log, storage, identity, behavior);
return truncate;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading