[#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:
parent
7e49dc6c26
commit
66301d6574
@ -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
|
||||
|
||||
@ -727,4 +727,18 @@ public enum Clause {
|
||||
* </ul>
|
||||
*/
|
||||
TRUNCATE_TRUNCATE,
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Other clauses
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A plain SQL template clause.
|
||||
*/
|
||||
TEMPLATE,
|
||||
|
||||
/**
|
||||
* A custom {@link QueryPart} clause.
|
||||
*/
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
*/
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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()]);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user