Skip to content

Commit

Permalink
Feature/50 support merge statement (#53)
Browse files Browse the repository at this point in the history
* #52: `MERGE` now supports `WHERE`
  • Loading branch information
redcatbear authored Nov 26, 2019
1 parent e8a5413 commit e78d2bc
Show file tree
Hide file tree
Showing 17 changed files with 372 additions and 79 deletions.
14 changes: 14 additions & 0 deletions doc/user_guide/statements/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ merge.whenNotMatched()

Check the JavaDoc of class `Merge` for a more information.

### Using a Filter on a `MERGE` Strategy

All three merge strategies can be narrowed down with a `WHERE` clause, to limit the dataset they work on.

Here is an example for a `MERGE` statement with a `DELETE` strategy.

```java
merge.using("src") //
.on(eq(column("src", "c1"), column("dst", "c1"))) //
.whenMatched() //
.thenDelete() //
.where(gt(column("src", "c5"), integerLiteral(1000)));
```

### Rendering `MERGE` Statements

Use the `MergeRenderer` to render `Merge` objects into SQL strings.
Expand Down
18 changes: 15 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.exasol</groupId>
<artifactId>sql-statement-builder</artifactId>
<version>2.0.0</version>
<version>3.0.0</version>
<name>Exasol SQL Statement Builder</name>
<description>This module provides a Builder for SQL statements that helps creating the correct structure and validates variable parts of the statements.</description>
<url>https://github.com/exasol/sql-statement-builder</url>
Expand Down Expand Up @@ -100,6 +101,17 @@
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.24.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
Expand Down Expand Up @@ -169,7 +181,7 @@
</executions>
<configuration>
<charset>UTF-8</charset>
<doclint/>
<doclint />
<serialwarn>true</serialwarn>
<failOnError>true</failOnError>
<failOnWarnings>true</failOnWarnings>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public abstract class AbstractInsertValueTable<T extends AbstractInsertValueTabl

/**
* Create the abstract base for a fragment containing a value table.
*
*
* @param root root fragment
*/
public AbstractInsertValueTable(final Fragment root) {
Expand All @@ -23,7 +23,7 @@ public AbstractInsertValueTable(final Fragment root) {
protected abstract T self();

protected synchronized void createInsertValueInstanceIfItDoesNotExist() {
if (this.insertValueTable == null) {
if (!hasValues()) {
this.insertValueTable = new ValueTable(this);
}
}
Expand All @@ -35,7 +35,7 @@ protected synchronized void createInsertValueInstanceIfItDoesNotExist() {
* @return <code>this</code> for fluent programming
*/
public synchronized T field(final String... names) {
if (this.insertFields == null) {
if (!hasFields()) {
this.insertFields = new InsertFields(this.getRoot());
}
this.insertFields.add(names);
Expand All @@ -49,7 +49,7 @@ public synchronized T field(final String... names) {
* @return <code>this</code> for fluent programming
*/
public synchronized T valueTable(final ValueTable table) {
if (this.insertValueTable != null) {
if (hasValues()) {
throw new IllegalStateException("Cannot add a value table to an INSERT command that already has one.");
}
this.insertValueTable = table;
Expand Down Expand Up @@ -108,4 +108,22 @@ public synchronized T valuePlaceholders(final int amount) {
}
return self();
}
}

/**
* Check if a value table is present.
*
* @return {@code true} if a value table exists
*/
public boolean hasValues() {
return this.insertValueTable != null;
}

/**
* Check if a insert fields are defined.
*
* @return {@code true} if insert fields are defined
*/
public boolean hasFields() {
return this.insertFields != null;
}
}
73 changes: 54 additions & 19 deletions src/main/java/com/exasol/sql/dml/merge/Merge.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public class Merge extends AbstractFragment implements SqlStatement, MergeFragme
private final Table destinationTable;
private UsingClause using;
private OnClause on;
private BooleanExpression condition;
private MatchedClause matched;
private NotMatchedClause notMatched;

Expand Down Expand Up @@ -60,6 +59,24 @@ public Merge using(final String sourceTable, final String as) {
return this;
}

/**
* Get the {@code USING} clause of the {@code MERGE} statement.
*
* @return destination table
*/
public UsingClause getUsing() {
return this.using;
}

/**
* Check if the {@code USING} clause exists.
*
* @return {@code true} if the {@code USING} clause exists.
*/
protected boolean hasUsing() {
return this.using != null;
}

/**
* Define the merge criteria.
*
Expand All @@ -71,42 +88,60 @@ public Merge on(final BooleanExpression condition) {
return this;
}

/**
* Get the merge criteria (i.e. the `ON` clause).
*
* @return criteria that must be met for the rows in source and destination to be considered a match.
*/
public OnClause getOn() {
return this.on;
}

/**
* Check if the {@code ON} clause exists.
*
* @return {@code true} if the {@code ON} clause exists.
*/
protected boolean hasOn() {
return this.on != null;
}

/**
* Define the merge strategy if the match criteria is met.
*
* @return match strategy
*/
public MatchedClause whenMatched() {
this.matched = new MatchedClause(this.root);
this.matched = new MatchedClause(this);
return this.matched;
}

/**
* Define the merge strategy if the match criteria is not met.
* Check if the {@code WHEN MATCHED} clause exists.
*
* @return not matched strategy
* @return {@code true} if the {@code WHEN MATCHED} clause exists
*/
public NotMatchedClause whenNotMatched() {
this.notMatched = new NotMatchedClause(this.root);
return this.notMatched;
protected boolean hasMatched() {
return this.matched != null;
}

/**
* Get the {@code USING} clause of the {@code MERGE} statement.
* Define the merge strategy if the match criteria is not met.
*
* @return destination table
* @return not matched strategy
*/
public UsingClause getUsing() {
return this.using;
public NotMatchedClause whenNotMatched() {
this.notMatched = new NotMatchedClause(this);
return this.notMatched;
}

/**
* Get the merge condition.
* Check if the {@code WHEN NOT MATCHED} clause exists.
*
* @return criteria that must be met for the rows in source and destination to be considered a match.
* @return true if the {@code WHEN NOT MATCHED} clause exists
*/
public BooleanExpression getCondition() {
return this.condition;
protected boolean hasNotMatched() {
return this.notMatched != null;
}

@Override
Expand All @@ -115,16 +150,16 @@ public void accept(final MergeVisitor visitor) {
if (this.destinationTable != null) {
this.destinationTable.accept(visitor);
}
if (this.using != null) {
if (hasUsing()) {
this.using.accept(visitor);
}
if (this.on != null) {
if (hasOn()) {
this.on.accept(visitor);
}
if (this.matched != null) {
if (hasMatched()) {
this.matched.accept(visitor);
}
if (this.notMatched != null) {
if (hasNotMatched()) {
this.notMatched.accept(visitor);
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/com/exasol/sql/dml/merge/MergeDeleteClause.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.exasol.sql.dml.merge;

import com.exasol.sql.AbstractFragment;
import com.exasol.sql.Fragment;

/**
* This class represents the {@code MERGE} strategy of deleting matched rows.
*/
public class MergeDeleteClause extends AbstractFragment implements MergeFragment {
public class MergeDeleteClause extends MergeMethodDefinition implements MergeFragment {
/**
* Create a new instance of a {@link MergeDeleteClause}.
*
Expand All @@ -19,5 +18,8 @@ public MergeDeleteClause(final Fragment root) {
@Override
public void accept(final MergeVisitor visitor) {
visitor.visit(this);
if (hasWhere()) {
this.where.accept(visitor);
}
}
}
41 changes: 39 additions & 2 deletions src/main/java/com/exasol/sql/dml/merge/MergeInsertClause.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import com.exasol.sql.Fragment;
import com.exasol.sql.dml.insert.AbstractInsertValueTable;
import com.exasol.sql.dql.select.WhereClause;
import com.exasol.sql.expression.BooleanExpression;

/**
* Represents the {@code MERGE} strategy of inserting rows from the source that do not match any row in the destination.
*/
public class MergeInsertClause extends AbstractInsertValueTable<MergeInsertClause> implements MergeFragment {
protected WhereClause where = null;

/**
* Create a new instance of a {@link MergeInsertClause}.
*
Expand All @@ -21,14 +25,47 @@ protected MergeInsertClause self() {
return this;
}

/**
* Add a {@code WHERE} clause insertion definition.
*
* @param expression filter expression
* @return parent {@code MERGE} statement
*/
public Merge where(final BooleanExpression expression) {
final Merge merge = (Merge) this.getRoot();
this.where = new WhereClause(merge, expression);
return merge;
}

/**
* Get the {@code WHERE} clause of the insert definition.
*
* @return {@code WHERE} clause
*/
public WhereClause getWhere() {
return this.where;
}

/**
* Check if the {@code WHERE} clause exists.
*
* @return {@code true} if the {@code WHERE} clause exists
*/
public boolean hasWhere() {
return this.where != null;
}

@Override
public void accept(final MergeVisitor visitor) {
visitor.visit(this);
if (this.insertFields != null) {
if (hasFields()) {
this.insertFields.accept(visitor);
}
if (this.insertValueTable != null) {
if (hasValues()) {
this.insertValueTable.accept(visitor);
}
if (hasWhere()) {
this.where.accept(visitor);
}
}
}
52 changes: 52 additions & 0 deletions src/main/java/com/exasol/sql/dml/merge/MergeMethodDefinition.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.exasol.sql.dml.merge;

import com.exasol.sql.AbstractFragment;
import com.exasol.sql.Fragment;
import com.exasol.sql.dql.select.WhereClause;
import com.exasol.sql.expression.BooleanExpression;

/**
* Abstract base class for merge method definitions like {@code WHEN MATCHED THEN UPDATE}.
*/
public abstract class MergeMethodDefinition extends AbstractFragment {
protected WhereClause where = null;

/**
* Create the abstract base for a merge method definition.
*
* @param root root {@code MERGE} statement
*/
public MergeMethodDefinition(final Fragment root) {
super(root);
}

/**
* Add a {@code WHERE} clause {@code MERGE} definition.
*
* @param expression filter expression
* @return parent {@code MERGE} statement
*/
public Merge where(final BooleanExpression expression) {
final Merge merge = (Merge) this.getRoot();
this.where = new WhereClause(merge, expression);
return merge;
}

/**
* Get the {@code WHERE} clause of the merge method definition.
*
* @return {@code WHERE} clause
*/
public WhereClause getWhere() {
return this.where;
}

/**
* Check if the {@code WHERE} clause exists.
*
* @return {@code true} if the {@code WHERE} clause exists
*/
public boolean hasWhere() {
return this.where != null;
}
}
Loading

0 comments on commit e78d2bc

Please sign in to comment.