From 66301d6574c587728f0044b32fc932dc243e9f4a Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Fri, 16 Aug 2013 14:00:23 +0200 Subject: [PATCH] [#2665] Added example for INSERT query transformation * Added example integration test for INSERT query transformation * Enhanced VisitContext to enable patching of QueryParts while traversing the tree --- .../test/_/testcases/VisitListenerTests.java | 111 +++++++++++++++++- jOOQ/src/main/java/org/jooq/Clause.java | 14 +++ jOOQ/src/main/java/org/jooq/VisitContext.java | 13 +- .../src/main/java/org/jooq/VisitListener.java | 20 ++++ .../java/org/jooq/impl/CustomCondition.java | 20 ++-- .../main/java/org/jooq/impl/CustomField.java | 20 ++-- .../java/org/jooq/impl/CustomQueryPart.java | 20 ++-- .../org/jooq/impl/DefaultRenderContext.java | 32 ++++- .../main/java/org/jooq/impl/SQLTemplate.java | 5 +- 9 files changed, 213 insertions(+), 42 deletions(-) diff --git a/jOOQ-test/src/org/jooq/test/_/testcases/VisitListenerTests.java b/jOOQ-test/src/org/jooq/test/_/testcases/VisitListenerTests.java index 0b586af8f3..ce219c2648 100644 --- a/jOOQ-test/src/org/jooq/test/_/testcases/VisitListenerTests.java +++ b/jOOQ-test/src/org/jooq/test/_/testcases/VisitListenerTests.java @@ -36,10 +36,12 @@ package org.jooq.test._.testcases; import static java.util.Arrays.asList; +import static org.jooq.Clause.CUSTOM; import static org.jooq.Clause.DELETE; import static org.jooq.Clause.DELETE_DELETE; import static org.jooq.Clause.DELETE_WHERE; import static org.jooq.Clause.INSERT; +import static org.jooq.Clause.INSERT_INSERT_INTO; import static org.jooq.Clause.SELECT; import static org.jooq.Clause.SELECT_FROM; import static org.jooq.Clause.SELECT_WHERE; @@ -47,12 +49,17 @@ import static org.jooq.Clause.TABLE_ALIAS; import static org.jooq.Clause.UPDATE; import static org.jooq.Clause.UPDATE_UPDATE; import static org.jooq.Clause.UPDATE_WHERE; +import static org.jooq.SQLDialect.ORACLE; +import static org.jooq.conf.ParamType.INLINED; import static org.jooq.impl.DSL.inline; +import static org.jooq.impl.DSL.queryPart; import static org.jooq.impl.DSL.select; import static org.jooq.impl.DSL.selectFrom; import static org.jooq.impl.DSL.selectOne; import static org.jooq.impl.DSL.trueCondition; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.sql.Date; import java.util.ArrayList; @@ -68,18 +75,22 @@ import org.jooq.Record1; import org.jooq.Record2; import org.jooq.Record3; import org.jooq.Record6; +import org.jooq.RenderContext; import org.jooq.Result; import org.jooq.Table; import org.jooq.TableRecord; import org.jooq.UpdatableRecord; import org.jooq.VisitContext; import org.jooq.conf.ParamType; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.CustomQueryPart; import org.jooq.impl.DefaultVisitListener; import org.jooq.test.BaseTest; import org.jooq.test.jOOQAbstractTest; import org.junit.Test; +@SuppressWarnings("serial") public class VisitListenerTests< A extends UpdatableRecord & Record6, AP, @@ -229,7 +240,57 @@ extends BaseTest void patchCheckOption( + final VisitContext context, + final Table table, + final Field field, + final E... values) + { + if (context.queryPart() == table) { + + // ... within a SQL INSERT INTO clause + List clauses = subselectClauses(context); + if (clauses.contains(INSERT_INSERT_INTO) + + // But avoid recursion! + && !clauses.contains(CUSTOM)) { + + // ... then, replace the table by an equivalent + // view with a CHECK OPTION clause + context.queryPart(new CustomQueryPart() { + @Override + public void toSQL(RenderContext ctx) { + ParamType previous = ctx.paramType(); + ctx.paramType(INLINED) + .visit(queryPart( + "(SELECT * FROM {0} WHERE {1} WITH CHECK OPTION)", + table, + field.in(values).or(field.isNull()) + )) + .paramType(previous); + } + }); + } + } + } + private void pushConditions(VisitContext context, Table table, Field field, E... values) { // Check if we're visiting the given table diff --git a/jOOQ/src/main/java/org/jooq/Clause.java b/jOOQ/src/main/java/org/jooq/Clause.java index 2fcc41a2ae..323e166f5c 100644 --- a/jOOQ/src/main/java/org/jooq/Clause.java +++ b/jOOQ/src/main/java/org/jooq/Clause.java @@ -727,4 +727,18 @@ public enum Clause { * */ TRUNCATE_TRUNCATE, + + // ------------------------------------------------------------------------- + // Other clauses + // ------------------------------------------------------------------------- + + /** + * A plain SQL template clause. + */ + TEMPLATE, + + /** + * A custom {@link QueryPart} clause. + */ + CUSTOM } diff --git a/jOOQ/src/main/java/org/jooq/VisitContext.java b/jOOQ/src/main/java/org/jooq/VisitContext.java index 9adcf1dc2b..7c9986860a 100644 --- a/jOOQ/src/main/java/org/jooq/VisitContext.java +++ b/jOOQ/src/main/java/org/jooq/VisitContext.java @@ -108,10 +108,21 @@ public interface VisitContext { /** * The most recent {@link QueryPart} that was encountered through - * {@link Context#visit(QueryPart)} + * {@link Context#visit(QueryPart)}. */ QueryPart queryPart(); + /** + * Replace the most recent {@link QueryPart} that was encountered through + * {@link Context#visit(QueryPart)}. + *

+ * This method can be called by {@link VisitListener} implementation + * methods, in particular by {@link VisitListener#visitStart(VisitContext)}. + * + * @param part The new QueryPart. + */ + void queryPart(QueryPart part); + /** * A path of {@link QueryPart}s going through the visiting tree. *

diff --git a/jOOQ/src/main/java/org/jooq/VisitListener.java b/jOOQ/src/main/java/org/jooq/VisitListener.java index 8215b34b6c..568176e236 100644 --- a/jOOQ/src/main/java/org/jooq/VisitListener.java +++ b/jOOQ/src/main/java/org/jooq/VisitListener.java @@ -100,6 +100,26 @@ public interface VisitListener extends EventListener { /** * Called before visiting a {@link QueryPart}. + *

+ * Certain VisitListener implementations may chose to replace + * the {@link QueryPart} contained in the argument {@link VisitContext} + * through {@link VisitContext#queryPart(QueryPart)}. This can be used for + * many use-cases, for example to add a CHECK OPTION to an + * Oracle INSERT statement:

+     * -- Original query
+     * INSERT INTO book (id, author_id, title)
+     * VALUES (10, 15, '1984')
+     *
+     * -- Transformed query
+     * INSERT INTO (
+     *   SELECT * FROM book
+     *   WHERE author_id IN (1, 2, 3)
+     *   WITH CHECK OPTION
+     * ) (id, author_id, title)
+     * VALUES (10, 15, '1984')
+     * 
The above SQL transformation allows to prevent inserting + * new books for authors other than those with + * author_id IN (1, 2, 3) * * @see Context#visit(QueryPart) */ diff --git a/jOOQ/src/main/java/org/jooq/impl/CustomCondition.java b/jOOQ/src/main/java/org/jooq/impl/CustomCondition.java index 1725909a3f..95d0ef88ec 100644 --- a/jOOQ/src/main/java/org/jooq/impl/CustomCondition.java +++ b/jOOQ/src/main/java/org/jooq/impl/CustomCondition.java @@ -35,6 +35,8 @@ */ package org.jooq.impl; +import static org.jooq.Clause.CUSTOM; + import org.jooq.BindContext; import org.jooq.Clause; import org.jooq.Condition; @@ -63,7 +65,8 @@ public abstract class CustomCondition extends AbstractCondition { /** * Generated UID */ - private static final long serialVersionUID = -3439681086987884991L; + private static final long serialVersionUID = -3439681086987884991L; + private static final Clause[] CLAUSES = { CUSTOM }; protected CustomCondition() { } @@ -93,20 +96,15 @@ public abstract class CustomCondition extends AbstractCondition { public void bind(BindContext context) throws DataAccessException { } - /** - * Subclasses may implement this method - *
- * {@inheritDoc} - */ - @Override - public Clause[] clauses(Context ctx) { - return null; - } - // ------------------------------------------------------------------------- // No further overrides allowed // ------------------------------------------------------------------------- + @Override + public final Clause[] clauses(Context ctx) { + return CLAUSES; + } + @Override public final boolean declaresFields() { return super.declaresFields(); diff --git a/jOOQ/src/main/java/org/jooq/impl/CustomField.java b/jOOQ/src/main/java/org/jooq/impl/CustomField.java index 88e3fbea42..83b34bb36e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/CustomField.java +++ b/jOOQ/src/main/java/org/jooq/impl/CustomField.java @@ -35,6 +35,8 @@ */ package org.jooq.impl; +import static org.jooq.Clause.CUSTOM; + import org.jooq.BindContext; import org.jooq.Clause; import org.jooq.Context; @@ -63,7 +65,8 @@ public abstract class CustomField extends AbstractField { /** * Generated UID */ - private static final long serialVersionUID = -1778024624798672262L; + private static final long serialVersionUID = -1778024624798672262L; + private static final Clause[] CLAUSES = { CUSTOM }; protected CustomField(String name, DataType type) { super(name, type); @@ -94,20 +97,15 @@ public abstract class CustomField extends AbstractField { public void bind(BindContext context) throws DataAccessException { } - /** - * Subclasses may implement this method - *
- * {@inheritDoc} - */ - @Override - public Clause[] clauses(Context ctx) { - return super.clauses(ctx); - } - // ------------------------------------------------------------------------- // No further overrides allowed // ------------------------------------------------------------------------- + @Override + public final Clause[] clauses(Context ctx) { + return CLAUSES; + } + @Override public final Field as(String alias) { return super.as(alias); diff --git a/jOOQ/src/main/java/org/jooq/impl/CustomQueryPart.java b/jOOQ/src/main/java/org/jooq/impl/CustomQueryPart.java index 94e9c414a6..609ed4cce3 100644 --- a/jOOQ/src/main/java/org/jooq/impl/CustomQueryPart.java +++ b/jOOQ/src/main/java/org/jooq/impl/CustomQueryPart.java @@ -35,6 +35,8 @@ */ package org.jooq.impl; +import static org.jooq.Clause.CUSTOM; + import org.jooq.BindContext; import org.jooq.Clause; import org.jooq.Context; @@ -74,7 +76,8 @@ public abstract class CustomQueryPart extends AbstractQueryPart { /** * Generated UID */ - private static final long serialVersionUID = -3439681086987884991L; + private static final long serialVersionUID = -3439681086987884991L; + private static final Clause[] CLAUSES = { CUSTOM }; protected CustomQueryPart() { } @@ -104,20 +107,15 @@ public abstract class CustomQueryPart extends AbstractQueryPart { public void bind(BindContext context) throws DataAccessException { } - /** - * Subclasses may implement this method - *
- * {@inheritDoc} - */ - @Override - public Clause[] clauses(Context ctx) { - return null; - } - // ------------------------------------------------------------------------- // No further overrides allowed // ------------------------------------------------------------------------- + @Override + public final Clause[] clauses(Context ctx) { + return CLAUSES; + } + @Override public final boolean declaresFields() { return super.declaresFields(); diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java b/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java index 77be298c4c..6485648127 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java @@ -172,16 +172,28 @@ class DefaultRenderContext extends AbstractContext implements Ren @Override public final RenderContext visit(QueryPart part) { if (part != null) { + + // Issue start clause events + // ----------------------------------------------------------------- Clause[] clauses = visitListeners.length > 0 ? clause(part) : null; if (clauses != null) for (int i = 0; i < clauses.length; i++) start(clauses[i]); - - start(part); - super.visit(part); - end(part); - + // Perform the actual visiting, or recurse into the replacement + // ----------------------------------------------------------------- + QueryPart original = part; + QueryPart replacement = start(part); + + if (original == replacement) + super.visit(original); + else + visit(replacement); + + end(replacement); + + // Issue end clause events + // ----------------------------------------------------------------- if (clauses != null) for (int i = clauses.length - 1; i >= 0; i--) end(clauses[i]); @@ -190,12 +202,14 @@ class DefaultRenderContext extends AbstractContext implements Ren return this; } - private final void start(QueryPart part) { + private final QueryPart start(QueryPart part) { visitParts.addLast(part); for (VisitListener listener : visitListeners) { listener.visitStart(visitContext); } + + return visitParts.peekLast(); } private final void end(QueryPart part) { @@ -272,6 +286,12 @@ class DefaultRenderContext extends AbstractContext implements Ren return visitParts.peekLast(); } + @Override + public final void queryPart(QueryPart part) { + visitParts.pollLast(); + visitParts.addLast(part); + } + @Override public final QueryPart[] queryParts() { return visitParts.toArray(new QueryPart[visitParts.size()]); diff --git a/jOOQ/src/main/java/org/jooq/impl/SQLTemplate.java b/jOOQ/src/main/java/org/jooq/impl/SQLTemplate.java index d685b48d12..4bbdf9e0d3 100644 --- a/jOOQ/src/main/java/org/jooq/impl/SQLTemplate.java +++ b/jOOQ/src/main/java/org/jooq/impl/SQLTemplate.java @@ -35,6 +35,8 @@ */ package org.jooq.impl; +import static org.jooq.Clause.TEMPLATE; + import java.util.List; import org.jooq.BindContext; @@ -64,6 +66,7 @@ class SQLTemplate implements Template { * Generated UID */ private static final long serialVersionUID = -7514156096865122018L; + private static final Clause[] CLAUSES = { TEMPLATE }; private final String sql; private final List substitutes; @@ -84,7 +87,7 @@ class SQLTemplate implements Template { @Override public final Clause[] clauses(Context ctx) { - return null; + return CLAUSES; } } }