[#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
This commit is contained in:
Lukas Eder 2013-08-16 14:00:23 +02:00
parent 7e49dc6c26
commit 66301d6574
9 changed files with 213 additions and 42 deletions

View File

@ -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<A> & Record6<Integer, String, String, Date, Integer, ?>,
AP,
@ -229,7 +240,57 @@ extends BaseTest<A, AP, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, UU, I, IPK, T7
selectOne()
.from(TBook())
.where(TBook_TITLE().eq("changed"))
));
));
if (dialect().family() == ORACLE) {
// Cannot insert books for author_id = 2
try {
create(new OnlyAuthorIDEqual1())
.insertInto(TBook())
.set(TBook_ID(), 5)
.set(TBook_AUTHOR_ID(), 2)
.set(TBook_TITLE(), "1234")
.set(TBook_PUBLISHED_IN(), 2000)
.set(TBook_LANGUAGE_ID(), 1)
.execute();
fail();
}
catch (DataAccessException expected) {
assertTrue(
expected.getMessage(),
expected.getMessage().toUpperCase().contains("ORA-01402"));
}
// Can insert books for author_id = 1
assertEquals(1,
create(new OnlyAuthorIDEqual1())
.insertInto(TBook())
.set(TBook_ID(), 5)
.set(TBook_AUTHOR_ID(), 1)
.set(TBook_TITLE(), "1234")
.set(TBook_PUBLISHED_IN(), 2000)
.set(TBook_LANGUAGE_ID(), 1)
.execute());
assertEquals(1, create().selectFrom(TBook()).where(TBook_ID().eq(5)).fetch().size());
// Cannot insert any new authors
try {
create(new OnlyAuthorIDEqual1())
.insertInto(TAuthor())
.set(TAuthor_ID(), 3)
.set(TAuthor_FIRST_NAME(), "Jon")
.set(TAuthor_LAST_NAME(), "Doe")
.execute();
fail();
}
catch (DataAccessException expected) {
assertTrue(
expected.getMessage(),
expected.getMessage().toUpperCase().contains("ORA-01402"));
}
}
}
// -------------------------------------------------------------------------
@ -444,6 +505,20 @@ extends BaseTest<A, AP, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, UU, I, IPK, T7
}
}
@Override
public void visitStart(VisitContext context) {
// Operating on RenderContext only, as we're using inline values
if (context.renderContext() == null)
return;
// Add Oracle CHECK OPTIONs to INSERT statements, if applicable
if (context.configuration().dialect().family() == ORACLE) {
patchCheckOption(context, TBook(), TBook_AUTHOR_ID(), 1);
patchCheckOption(context, TAuthor(), TAuthor_ID(), 1);
}
}
@Override
public void visitEnd(VisitContext context) {
@ -468,6 +543,40 @@ extends BaseTest<A, AP, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, UU, I, IPK, T7
}
}
private <E> void patchCheckOption(
final VisitContext context,
final Table<?> table,
final Field<E> field,
final E... values)
{
if (context.queryPart() == table) {
// ... within a SQL INSERT INTO clause
List<Clause> 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 <E> void pushConditions(VisitContext context, Table<?> table, Field<E> field, E... values) {
// Check if we're visiting the given table

View File

@ -727,4 +727,18 @@ public enum Clause {
* </ul>
*/
TRUNCATE_TRUNCATE,
// -------------------------------------------------------------------------
// Other clauses
// -------------------------------------------------------------------------
/**
* A plain SQL template clause.
*/
TEMPLATE,
/**
* A custom {@link QueryPart} clause.
*/
CUSTOM
}

View File

@ -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)}.
* <p>
* This method can be called by {@link VisitListener} implementation
* methods, in particular by {@link VisitListener#visitStart(VisitContext)}.
*
* @param part The new <code>QueryPart</code>.
*/
void queryPart(QueryPart part);
/**
* A path of {@link QueryPart}s going through the visiting tree.
* <p>

View File

@ -100,6 +100,26 @@ public interface VisitListener extends EventListener {
/**
* Called before visiting a {@link QueryPart}.
* <p>
* Certain <code>VisitListener</code> 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 <code>CHECK OPTION</code> to an
* Oracle <code>INSERT</code> statement: <code><pre>
* -- 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')
* </pre></code> The above SQL transformation allows to prevent inserting
* new books for authors other than those with
* <code>author_id IN (1, 2, 3)</code>
*
* @see Context#visit(QueryPart)
*/

View File

@ -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
* <hr/>
* {@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();

View File

@ -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<T> extends AbstractField<T> {
/**
* 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<T> type) {
super(name, type);
@ -94,20 +97,15 @@ public abstract class CustomField<T> extends AbstractField<T> {
public void bind(BindContext context) throws DataAccessException {
}
/**
* Subclasses may implement this method
* <hr/>
* {@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<T> as(String alias) {
return super.as(alias);

View File

@ -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
* <hr/>
* {@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();

View File

@ -172,16 +172,28 @@ class DefaultRenderContext extends AbstractContext<RenderContext> 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<RenderContext> 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<RenderContext> 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()]);

View File

@ -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<QueryPart> substitutes;
@ -84,7 +87,7 @@ class SQLTemplate implements Template {
@Override
public final Clause[] clauses(Context<?> ctx) {
return null;
return CLAUSES;
}
}
}