Skip to content

Commit dedc3a8

Browse files
authored
feat: support Oracle TRUNCATE storage and log options (#2856)
1 parent ce830b5 commit dedc3a8

3 files changed

Lines changed: 222 additions & 23 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/truncate/Truncate.java‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,21 @@ public enum IdentityOption {
2323
RESTART, CONTINUE
2424
}
2525

26+
public enum StorageOption {
27+
DROP, DROP_ALL, REUSE
28+
}
29+
30+
public enum MaterializedViewLogOption {
31+
PRESERVE, PURGE
32+
}
33+
2634
private boolean tableToken;
2735
private boolean only;
2836
private final List<TableReference> targets = new ArrayList<>();
2937
private IdentityOption identityOption;
3038
private CascadeBehavior dropBehavior;
39+
private StorageOption storageOption;
40+
private MaterializedViewLogOption materializedViewLogOption;
3141

3242
@Override
3343
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
@@ -107,6 +117,32 @@ public void setIdentityOption(IdentityOption option) {
107117
identityOption = option;
108118
}
109119

120+
public StorageOption getStorageOption() {
121+
return storageOption;
122+
}
123+
124+
public void setStorageOption(StorageOption storageOption) {
125+
this.storageOption = storageOption;
126+
}
127+
128+
public Truncate withStorageOption(StorageOption storageOption) {
129+
setStorageOption(storageOption);
130+
return this;
131+
}
132+
133+
public MaterializedViewLogOption getMaterializedViewLogOption() {
134+
return materializedViewLogOption;
135+
}
136+
137+
public void setMaterializedViewLogOption(MaterializedViewLogOption option) {
138+
materializedViewLogOption = option;
139+
}
140+
141+
public Truncate withMaterializedViewLogOption(MaterializedViewLogOption option) {
142+
setMaterializedViewLogOption(option);
143+
return this;
144+
}
145+
110146
public CascadeBehavior getDropBehavior() {
111147
return dropBehavior;
112148
}
@@ -151,6 +187,13 @@ public StringBuilder appendTo(StringBuilder builder) {
151187
}
152188
targets.get(i).appendTo(builder);
153189
}
190+
if (materializedViewLogOption != null) {
191+
builder.append(' ').append(materializedViewLogOption).append(" MATERIALIZED VIEW LOG");
192+
}
193+
if (storageOption != null) {
194+
builder.append(' ').append(storageOption == StorageOption.DROP_ALL ? "DROP ALL"
195+
: storageOption.name()).append(" STORAGE");
196+
}
154197
if (identityOption != null) {
155198
builder.append(' ').append(identityOption).append(" IDENTITY");
156199
}

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1868,17 +1868,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
18681868
* JOIN ONLY t or FROM a, ONLY b, rather than a table or column named only.
18691869
*/
18701870
private boolean isRelationOnlyAhead(Token prev) {
1871-
int next = getToken(2).kind;
1872-
boolean name = next == S_IDENTIFIER || next == S_QUOTED_IDENTIFIER || next == OPENING_BRACKET;
1873-
switch (prev.kind) {
1874-
case K_FROM: case K_JOIN: case K_UPDATE: case K_INTO: case K_USING:
1875-
return name || next >= MIN_NON_RESERVED_WORD && next <= MAX_NON_RESERVED_WORD;
1876-
case K_COMMA:
1877-
// a column definition continues with its type instead, as in (a INT, only INT)
1878-
return name;
1879-
default:
1880-
return false;
1881-
}
1871+
return RelationOptions.isOnlyAhead(prev, getToken(2).kind);
18821872
}
18831873

18841874
/**
@@ -3313,6 +3303,48 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
33133303
}
33143304
}
33153305

3306+
/** Keeps relation-option branches out of the generated parser's nearly full constant pool. */
3307+
private static final class RelationOptions {
3308+
private RelationOptions() {}
3309+
3310+
static LockStatement.Target lockTarget(TableReference reference) {
3311+
LockStatement.Target target = new LockStatement.Target(reference.getTable());
3312+
target.setOnly(reference.isOnly());
3313+
target.setIncludeDescendants(reference.isIncludeDescendants());
3314+
target.setParenthesized(reference.isParenthesized());
3315+
return target;
3316+
}
3317+
3318+
static boolean isOnlyAhead(Token prev, int next) {
3319+
boolean name = next == S_IDENTIFIER || next == S_QUOTED_IDENTIFIER || next == OPENING_BRACKET;
3320+
switch (prev.kind) {
3321+
case K_FROM: case K_JOIN: case K_UPDATE: case K_INTO: case K_USING:
3322+
return name || next >= MIN_NON_RESERVED_WORD && next <= MAX_NON_RESERVED_WORD;
3323+
case K_COMMA:
3324+
// a column definition continues with its type, as in (a INT, only INT)
3325+
return name;
3326+
default:
3327+
return false;
3328+
}
3329+
}
3330+
3331+
static void apply(Truncate truncate, String log, String storage,
3332+
String identity, String behavior) {
3333+
if (log != null) {
3334+
truncate.setMaterializedViewLogOption(Truncate.MaterializedViewLogOption.valueOf(log));
3335+
}
3336+
if (storage != null) {
3337+
truncate.setStorageOption(Truncate.StorageOption.valueOf(storage));
3338+
}
3339+
if (identity != null) {
3340+
truncate.setIdentityOption(Truncate.IdentityOption.valueOf(identity));
3341+
}
3342+
if (behavior != null) {
3343+
truncate.setDropBehavior(CascadeBehavior.valueOf(behavior));
3344+
}
3345+
}
3346+
}
3347+
33163348
private void requireNoDropOwnerTable(Table owner) throws ParseException {
33173349
if (owner != null) {
33183350
throw new ParseException("Duplicate ON clause in DROP");
@@ -5236,15 +5268,11 @@ LockStatement LockStatement(): {
52365268
}
52375269

52385270
LockStatement.Target LockTarget():
5239-
{ TableReference reference; LockStatement.Target target; }
5271+
{ TableReference reference; }
52405272
{
52415273
reference=RelationTarget()
52425274
{
5243-
target = new LockStatement.Target(reference.getTable());
5244-
target.setOnly(reference.isOnly());
5245-
target.setIncludeDescendants(reference.isIncludeDescendants());
5246-
target.setParenthesized(reference.isParenthesized());
5247-
return target;
5275+
return RelationOptions.lockTarget(reference);
52485276
}
52495277
}
52505278

@@ -20023,18 +20051,38 @@ ReassignOwned ReassignOwned():
2002320051
}
2002420052

2002520053
Truncate Truncate():
20026-
{ Truncate truncate = new Truncate(); TableReference target; }
20054+
{
20055+
Truncate truncate = new Truncate();
20056+
TableReference target;
20057+
String log = null, storage = null, identity = null, behavior = null;
20058+
}
2002720059
{
2002820060
<K_TRUNCATE>
2002920061
[ LOOKAHEAD(2) <K_TABLE> { truncate.setTableToken(true); } ]
2003020062
target=RelationTarget() { truncate.getTargets().add(target); }
2003120063
( LOOKAHEAD(2) "," target=RelationTarget() { truncate.getTargets().add(target); } )*
20032-
[ ( <K_RESTART> { truncate.setIdentityOption(Truncate.IdentityOption.RESTART); }
20033-
| ContextualKeyword("CONTINUE") { truncate.setIdentityOption(Truncate.IdentityOption.CONTINUE); } )
20064+
[
20065+
LOOKAHEAD({ isKeywordAhead("PRESERVE") || getToken(1).kind == K_PURGE })
20066+
( ContextualKeyword("PRESERVE") { log = "PRESERVE"; }
20067+
| <K_PURGE> { log = "PURGE"; } )
20068+
<K_MATERIALIZED> <K_VIEW> <K_LOG>
20069+
]
20070+
[
20071+
LOOKAHEAD({ getToken(1).kind == K_DROP || isKeywordAhead("REUSE") })
20072+
( <K_DROP> { storage = "DROP"; }
20073+
[ <K_ALL> { storage = "DROP_ALL"; } ]
20074+
| ContextualKeyword("REUSE") { storage = "REUSE"; } )
20075+
<K_STORAGE>
20076+
]
20077+
[ ( <K_RESTART> { identity = "RESTART"; }
20078+
| ContextualKeyword("CONTINUE") { identity = "CONTINUE"; } )
2003420079
<K_IDENTITY> ]
20035-
[ <K_CASCADE> { truncate.setDropBehavior(CascadeBehavior.CASCADE); }
20036-
| <K_RESTRICT> { truncate.setDropBehavior(CascadeBehavior.RESTRICT); } ]
20037-
{ return truncate; }
20080+
[ <K_CASCADE> { behavior = "CASCADE"; }
20081+
| <K_RESTRICT> { behavior = "RESTRICT"; } ]
20082+
{
20083+
RelationOptions.apply(truncate, log, storage, identity, behavior);
20084+
return truncate;
20085+
}
2003820086
}
2003920087

2004020088
/**
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.truncate;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertNull;
14+
import static org.junit.jupiter.api.Assertions.assertThrows;
15+
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
18+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
19+
import net.sf.jsqlparser.schema.Table;
20+
import net.sf.jsqlparser.statement.CascadeBehavior;
21+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.ValueSource;
25+
26+
class OracleTruncateTest {
27+
@ParameterizedTest
28+
@ValueSource(strings = {"samples", "hr.\"Sample.Table\""})
29+
void preservesLogStorageAndCascadeOptions(String table) throws JSQLParserException {
30+
for (Truncate.MaterializedViewLogOption log : new Truncate.MaterializedViewLogOption[] {
31+
null, Truncate.MaterializedViewLogOption.PRESERVE,
32+
Truncate.MaterializedViewLogOption.PURGE}) {
33+
for (Truncate.StorageOption storage : new Truncate.StorageOption[] {null,
34+
Truncate.StorageOption.DROP, Truncate.StorageOption.DROP_ALL,
35+
Truncate.StorageOption.REUSE}) {
36+
for (String cascade : new String[] {"", " CASCADE"}) {
37+
String sql = "TRUNCATE TABLE " + table
38+
+ (log == null ? "" : " " + log + " MATERIALIZED VIEW LOG")
39+
+ (storage == null ? ""
40+
: storage == Truncate.StorageOption.DROP_ALL
41+
? " DROP ALL STORAGE"
42+
: " " + storage + " STORAGE")
43+
+ cascade;
44+
for (Dialect dialect : new Dialect[] {null, Dialect.ORACLE}) {
45+
Truncate truncate = parse(sql, dialect);
46+
assertEquals(table, truncate.getTable().getFullyQualifiedName());
47+
assertEquals(log, truncate.getMaterializedViewLogOption());
48+
assertEquals(storage, truncate.getStorageOption());
49+
assertEquals(cascade.isEmpty() ? null : CascadeBehavior.CASCADE,
50+
truncate.getDropBehavior());
51+
assertNull(truncate.getIdentityOption());
52+
assertEquals(sql, truncate.toString());
53+
roundTrip(truncate, dialect);
54+
assertEquals(2,
55+
CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size());
56+
}
57+
}
58+
}
59+
}
60+
}
61+
62+
@Test
63+
void supportsModelConstructionAndClearingOptions() throws JSQLParserException {
64+
Truncate truncate = new Truncate().withTableToken(true).withTable(new Table("samples"))
65+
.withMaterializedViewLogOption(Truncate.MaterializedViewLogOption.PURGE)
66+
.withStorageOption(Truncate.StorageOption.DROP_ALL).withCascade(true);
67+
assertEquals("TRUNCATE TABLE samples PURGE MATERIALIZED VIEW LOG DROP ALL STORAGE CASCADE",
68+
truncate.toString());
69+
roundTrip(truncate, Dialect.ORACLE);
70+
truncate.setMaterializedViewLogOption(Truncate.MaterializedViewLogOption.PRESERVE);
71+
truncate.setStorageOption(Truncate.StorageOption.REUSE);
72+
assertEquals("TRUNCATE TABLE samples PRESERVE MATERIALIZED VIEW LOG REUSE STORAGE CASCADE",
73+
truncate.toString());
74+
roundTrip(truncate, null);
75+
truncate.setMaterializedViewLogOption(null);
76+
truncate.setStorageOption(null);
77+
assertEquals("TRUNCATE TABLE samples CASCADE", truncate.toString());
78+
roundTrip(truncate, null);
79+
}
80+
81+
@ParameterizedTest
82+
@ValueSource(strings = {"DROP", "DROP ALL", "REUSE", "PRESERVE MATERIALIZED VIEW",
83+
"PURGE LOG", "DROP STORAGE REUSE STORAGE",
84+
"REUSE STORAGE PURGE MATERIALIZED VIEW LOG", "CASCADE REUSE STORAGE"})
85+
void rejectsIncompleteRepeatedAndOutOfOrderOptions(String options) {
86+
assertThrows(JSQLParserException.class,
87+
() -> parse("TRUNCATE TABLE samples " + options, Dialect.ORACLE));
88+
}
89+
90+
private static Truncate parse(String sql, Dialect dialect) throws JSQLParserException {
91+
return (Truncate) CCJSqlParserUtil.parse(sql, parser -> {
92+
if (dialect != null) {
93+
parser.withDialect(dialect);
94+
}
95+
});
96+
}
97+
98+
private static void roundTrip(Truncate truncate, Dialect dialect) throws JSQLParserException {
99+
StringBuilder output = new StringBuilder();
100+
truncate.accept(new StatementDeParser(output), null);
101+
assertEquals(truncate.toString(), output.toString());
102+
Truncate reparsed = parse(output.toString(), dialect);
103+
assertEquals(truncate.getMaterializedViewLogOption(),
104+
reparsed.getMaterializedViewLogOption());
105+
assertEquals(truncate.getStorageOption(), reparsed.getStorageOption());
106+
assertEquals(output.toString(), reparsed.toString());
107+
}
108+
}

0 commit comments

Comments
 (0)