Skip to content

Commit 86eaea7

Browse files
committed
Support Redshift materialized view BACKUP clauses
1 parent 6c726d8 commit 86eaea7

4 files changed

Lines changed: 115 additions & 8 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/create/view/CreateView.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class CreateView implements Statement {
3232
private boolean secure = false;
3333
private TemporaryOption temp = TemporaryOption.NONE;
3434
private AutoRefreshOption autoRefresh = AutoRefreshOption.NONE;
35+
private Boolean backup;
3536
private boolean withReadOnly = false;
3637
private boolean ifNotExists = false;
3738
private List<String> viewCommentOptions = null;
@@ -122,8 +123,11 @@ public void setWithData(Boolean withData) {
122123
this.withData = withData;
123124
}
124125

125-
/** Checks combinations introduced by the PostgreSQL view clauses. */
126+
/** Checks combinations requiring ordinary, recursive or materialized views. */
126127
public void validateOptions() {
128+
if (backup != null && !materialized) {
129+
throw new IllegalArgumentException("BACKUP requires a materialized view");
130+
}
127131
if (recursive && (materialized || columnNames == null || columnNames.isEmpty()
128132
|| checkOption != null)) {
129133
throw new IllegalArgumentException(
@@ -220,6 +224,30 @@ public void setAutoRefresh(AutoRefreshOption autoRefresh) {
220224
this.autoRefresh = autoRefresh;
221225
}
222226

227+
/** Null means omitted; true and false preserve Redshift's BACKUP YES and BACKUP NO. */
228+
public Boolean getBackup() {
229+
return backup;
230+
}
231+
232+
public void setBackup(Boolean backup) {
233+
this.backup = backup;
234+
}
235+
236+
public CreateView withBackup(Boolean backup) {
237+
setBackup(backup);
238+
return this;
239+
}
240+
241+
/** Shared SQL rendering for options immediately following the materialized view name. */
242+
public void appendMaterializationOptionsTo(StringBuilder sql) {
243+
if (backup != null) {
244+
sql.append(backup ? " BACKUP YES" : " BACKUP NO");
245+
}
246+
if (autoRefresh != AutoRefreshOption.NONE) {
247+
sql.append(" AUTO REFRESH ").append(autoRefresh.name());
248+
}
249+
}
250+
223251
public boolean isWithReadOnly() {
224252
return withReadOnly;
225253
}
@@ -266,9 +294,7 @@ public String toString() {
266294
if (ifNotExists && ifNotExistsAfterViewName) {
267295
sql.append(" IF NOT EXISTS");
268296
}
269-
if (autoRefresh != AutoRefreshOption.NONE) {
270-
sql.append(" AUTO REFRESH ").append(autoRefresh.name());
271-
}
297+
appendMaterializationOptionsTo(sql);
272298
if (columnNames != null) {
273299
sql.append("(");
274300
sql.append(columnNames);

src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
*/
1010
package net.sf.jsqlparser.util.deparser;
1111

12-
import net.sf.jsqlparser.statement.create.view.AutoRefreshOption;
1312
import net.sf.jsqlparser.statement.create.view.CreateView;
1413
import net.sf.jsqlparser.statement.create.view.TemporaryOption;
1514
import net.sf.jsqlparser.statement.select.PlainSelect;
@@ -74,9 +73,7 @@ public void deParse(CreateView createView) {
7473
if (createView.isIfNotExists() && createView.isIfNotExistsAfterViewName()) {
7574
builder.append(" IF NOT EXISTS");
7675
}
77-
if (createView.getAutoRefresh() != AutoRefreshOption.NONE) {
78-
builder.append(" AUTO REFRESH ").append(createView.getAutoRefresh().name());
79-
}
76+
createView.appendMaterializationOptionsTo(builder);
8077
if (createView.getColumnNames() != null) {
8178
builder.append("(");
8279
builder.append(createView.getColumnNames());

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12907,6 +12907,8 @@ CreateView CreateView(boolean isUsingOrReplace):
1290712907
<K_VIEW>
1290812908
[ LOOKAHEAD(3) <K_IF> <K_NOT> <K_EXISTS> { createView.setIfNotExists(true); } ]
1290912909
view=Table() { createView.setView(view); }
12910+
[ LOOKAHEAD({ isKeywordAhead("BACKUP") }) tk=<S_IDENTIFIER>
12911+
(tk=<K_YES> | tk=<K_NO>) { createView.setBackup(tk.kind == K_YES); } ]
1291012912
[LOOKAHEAD(3) <K_AUTO> <K_REFRESH> (tk=<K_YES> | tk=<K_NO>) { createView.setAutoRefresh(AutoRefreshOption.from(tk.image)); } ]
1291112913
[LOOKAHEAD(3) <K_IF> <K_NOT> <K_EXISTS> {
1291212914
createView.setIfNotExists(true);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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.create;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
14+
import net.sf.jsqlparser.JSQLParserException;
15+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
16+
import net.sf.jsqlparser.schema.Table;
17+
import net.sf.jsqlparser.statement.create.view.AutoRefreshOption;
18+
import net.sf.jsqlparser.statement.create.view.CreateView;
19+
import net.sf.jsqlparser.statement.select.Select;
20+
import net.sf.jsqlparser.test.TestUtils;
21+
import net.sf.jsqlparser.util.TablesNamesFinder;
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 RedshiftCreateViewTest {
27+
@ParameterizedTest
28+
@ValueSource(strings = {"", " BACKUP YES", " BACKUP NO"})
29+
void preserveBackupAndOmission(String clause) throws JSQLParserException {
30+
String sql = "CREATE MATERIALIZED VIEW myview" + clause + " AS SELECT * FROM mytab";
31+
CreateView view = (CreateView) TestUtils.assertSqlCanBeParsedAndDeparsed(sql);
32+
assertEquals(clause.isEmpty() ? null : clause.endsWith("YES"), view.getBackup());
33+
CreateView reparsed = (CreateView) CCJSqlParserUtil.parse(view.toString());
34+
assertEquals(view.getBackup(), reparsed.getBackup());
35+
assertTrue(TablesNamesFinder.findTables(sql).contains("mytab"));
36+
}
37+
38+
@ParameterizedTest
39+
@ValueSource(strings = {"YES", "NO"})
40+
void combineWithAutoRefresh(String value) throws JSQLParserException {
41+
CreateView view = (CreateView) TestUtils.assertSqlCanBeParsedAndDeparsed(
42+
"CREATE MATERIALIZED VIEW report.mv BACKUP NO AUTO REFRESH " + value
43+
+ " AS SELECT * FROM source");
44+
assertEquals(Boolean.FALSE, view.getBackup());
45+
assertEquals(AutoRefreshOption.from(value), view.getAutoRefresh());
46+
}
47+
48+
@ParameterizedTest
49+
@ValueSource(strings = {
50+
"CREATE MATERIALIZED VIEW mv BACKUP AS SELECT 1",
51+
"CREATE MATERIALIZED VIEW mv BACKUP TRUE AS SELECT 1",
52+
"CREATE MATERIALIZED VIEW mv BACKUP YES BACKUP NO AS SELECT 1",
53+
"CREATE MATERIALIZED VIEW mv AUTO REFRESH YES BACKUP NO AS SELECT 1",
54+
"CREATE VIEW mv BACKUP NO AS SELECT 1"
55+
})
56+
void rejectMalformedOptions(String sql) {
57+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql,
58+
parser -> parser.withUnsupportedStatements(false)));
59+
}
60+
61+
@Test
62+
void constructAndClearBackup() throws JSQLParserException {
63+
CreateView view = new CreateView().withMaterialized(true).withBackup(false)
64+
.withView(new Table("mv"))
65+
.withSelect((Select) CCJSqlParserUtil.parse("SELECT * FROM source"));
66+
TestUtils.assertStatementCanBeDeparsedAs(view,
67+
"CREATE MATERIALIZED VIEW mv BACKUP NO AS SELECT * FROM source");
68+
view.setBackup(null);
69+
TestUtils.assertStatementCanBeDeparsedAs(view,
70+
"CREATE MATERIALIZED VIEW mv AS SELECT * FROM source");
71+
view.setMaterialized(false);
72+
view.setBackup(true);
73+
assertThrows(IllegalArgumentException.class, view::validateOptions);
74+
}
75+
76+
@Test
77+
void backupRemainsAnIdentifier() throws JSQLParserException {
78+
TestUtils.assertSqlCanBeParsedAndDeparsed(
79+
"CREATE MATERIALIZED VIEW backup BACKUP YES AS SELECT backup FROM backup");
80+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT backup FROM backup");
81+
}
82+
}

0 commit comments

Comments
 (0)