[jOOQ/jOOQ#15551] Add runtime UDT mapping

This commit is contained in:
Lukas Eder 2023-09-06 14:42:12 +02:00
parent 89cacbf10b
commit bce323ab32
11 changed files with 523 additions and 24 deletions

View File

@ -508,6 +508,17 @@ fun MutableList<MappedTable>.table(block: MappedTable.() -> Unit) {
add(e)
}
fun MappedSchema.udts(block: MutableList<MappedUDT>.() -> Unit) {
block(udts)
}
@JvmName("mutableListMappedUDT")
fun MutableList<MappedUDT>.udt(block: MappedUDT.() -> Unit) {
val e = MappedUDT()
block(e)
add(e)
}
fun RenderMapping.schemata(block: MutableList<MappedSchema>.() -> Unit) {
block(schemata)
}

View File

@ -46,7 +46,7 @@ import org.jooq.impl.TableImpl;
*
* @author Lukas Eder
*/
class RenamedTable<R extends Record> extends TableImpl<R> {
final class RenamedTable<R extends Record> extends TableImpl<R> {
RenamedTable(Schema schema, Table<R> delegate, String rename) {
super(name(rename), schema);

View File

@ -0,0 +1,55 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq;
import org.jooq.impl.UDTImpl;
/**
* A mapped table
*
* @author Lukas Eder
*/
final class RenamedUDT<R extends UDTRecord<R>> extends UDTImpl<R> {
RenamedUDT(Schema schema, UDT<R> delegate, String rename) {
super(rename, schema, delegate.getPackage(), delegate.isSynthetic());
for (Field<?> field : delegate.fields())
createField(field.getUnqualifiedName(), field.getDataType(), this);
}
}

View File

@ -44,11 +44,15 @@ import static org.jooq.tools.StringUtils.isEmpty;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Supplier;
import org.jooq.conf.MappedCatalog;
import org.jooq.conf.MappedSchema;
import org.jooq.conf.MappedSchemaObject;
import org.jooq.conf.MappedTable;
import org.jooq.conf.RenderMapping;
import org.jooq.conf.Settings;
@ -96,6 +100,7 @@ public class SchemaMapping implements Serializable {
private volatile transient Map<String, Catalog> catalogs;
private volatile transient Map<String, Schema> schemata;
private volatile transient Map<String, Table<?>> tables;
private volatile transient Map<String, UDT<?>> udts;
/**
* Construct a mapping from a {@link Configuration} object
@ -452,10 +457,31 @@ public class SchemaMapping implements Serializable {
* @param table The generated table to be mapped
* @return The configured table
*/
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "rawtypes" })
@Nullable
public <R extends Record> Table<R> map(Table<R> table) {
Table<R> result = table;
return map0(table, () -> (Map) getTables(), s -> s.getTables(), RenamedTable::new);
}
/**
* Apply mapping to a given UDT
*
* @param udt The generated udt to be mapped
* @return The configured udt
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
@Nullable
public <R extends UDTRecord<R>> UDT<R> map(UDT<R> udt) {
return map0(udt, () -> (Map) getUDTs(), s -> s.getUdts(), RenamedUDT::new);
}
private <Q extends Qualified> Q map0(
Q part,
Supplier<Map<String, Q>> map,
Function<MappedSchema, ? extends List<? extends MappedSchemaObject>> schemaObjects,
Function3<Schema, Q, String, Q> rename
) {
Q result = part;
// [#4652] Don't initialise table mapping if not necessary
if (result != null && (!mapping().getSchemata().isEmpty() || !mapping().getCatalogs().isEmpty())) {
@ -472,35 +498,35 @@ public class SchemaMapping implements Serializable {
String catalogName = catalog.getName();
String schemaName = schema.getName();
String tableName = result.getName();
String name = result.getName();
String key = StringUtils.isEmpty(catalogName) ?
(StringUtils.isEmpty(schemaName) ? tableName : (schemaName + "." + tableName))
: (catalogName + '.' + schemaName + '.' + tableName);
(StringUtils.isEmpty(schemaName) ? name : (schemaName + "." + name))
: (catalogName + '.' + schemaName + '.' + name);
// Lazy initialise table mapping
if (!getTables().containsKey(key)) {
if (!map.get().containsKey(key)) {
// [#1857] thread-safe lazy initialisation for those users who
// want to use Configuration and dependent objects in a "thread-safe" manner
synchronized (this) {
if (!getTables().containsKey(key)) {
if (!map.get().containsKey(key)) {
catalogLoop:
for (MappedCatalog c : mapping().getCatalogs()) {
if (matches(c, catalogName)) {
for (MappedSchema s : c.getSchemata()) {
if (matches(s, schemaName)) {
for (MappedTable t : s.getTables()) {
for (MappedSchemaObject t : schemaObjects.apply(s)) {
// A configured mapping was found, add a renamed table
if (matches(t, tableName)) {
if (matches(t, name)) {
// Ignore self-mappings and void-mappings
if (!isBlank(t.getOutput()))
if (t.getInput() != null && !t.getOutput().equals(tableName))
result = new RenamedTable<>(map(schema), result, t.getOutput());
if (t.getInput() != null && !t.getOutput().equals(name))
result = rename.apply(map(schema), result, t.getOutput());
else if (t.getInputExpression() != null)
result = new RenamedTable<>(map(schema), result, t.getInputExpression().matcher(tableName).replaceAll(t.getOutput()));
result = rename.apply(map(schema), result, t.getInputExpression().matcher(name).replaceAll(t.getOutput()));
break catalogLoop;
}
@ -509,7 +535,7 @@ public class SchemaMapping implements Serializable {
}
// [#7498] Even without table mapping configuration, we may still need to map the schema
result = new RenamedTable<>(map(schema), result, tableName);
result = rename.apply(map(schema), result, name);
break catalogLoop;
}
}
@ -518,35 +544,35 @@ public class SchemaMapping implements Serializable {
schemaLoop:
for (MappedSchema s : mapping().getSchemata()) {
if (matches(s, schemaName)) {
for (MappedTable t : s.getTables()) {
for (MappedSchemaObject t : schemaObjects.apply(s)) {
// A configured mapping was found, add a renamed table
if (matches(t, tableName)) {
if (matches(t, name)) {
// Ignore self-mappings and void-mappings
if (!isBlank(t.getOutput()))
if (t.getInput() != null && !t.getOutput().equals(tableName))
result = new RenamedTable<>(map(schema), result, t.getOutput());
if (t.getInput() != null && !t.getOutput().equals(name))
result = rename.apply(map(schema), result, t.getOutput());
else if (t.getInputExpression() != null)
result = new RenamedTable<>(map(schema), result, t.getInputExpression().matcher(tableName).replaceAll(t.getOutput()));
result = rename.apply(map(schema), result, t.getInputExpression().matcher(name).replaceAll(t.getOutput()));
break schemaLoop;
}
}
// [#7498] Even without table mapping configuration, we may still need to map the schema
result = new RenamedTable<>(map(schema), result, tableName);
result = rename.apply(map(schema), result, name);
break schemaLoop;
}
}
// Add mapped table or self if no mapping was found
getTables().put(key, result);
map.get().put(key, result);
}
}
}
result = (Table<R>) getTables().get(key);
result = map.get().get(key);
}
return result;
@ -562,7 +588,7 @@ public class SchemaMapping implements Serializable {
|| (s.getInputExpression() != null && s.getInputExpression().matcher(schemaName).matches());
}
private final boolean matches(MappedTable t, String tableName) {
private final boolean matches(MappedSchemaObject t, String tableName) {
return (t.getInput() != null && tableName.equals(t.getInput()))
|| (t.getInputExpression() != null && t.getInputExpression().matcher(tableName).matches());
}
@ -624,6 +650,20 @@ public class SchemaMapping implements Serializable {
return tables;
}
private final Map<String, UDT<?>> getUDTs() {
if (udts == null) {
// [#1857] thread-safe lazy initialisation for those users who
// want to use Configuration and dependent objects in a "thread-safe" manner
synchronized (this) {
if (udts == null) {
udts = new HashMap<>();
}
}
}
return udts;
}
// ------------------------------------------------------------------------
// Object API
// ------------------------------------------------------------------------

View File

@ -43,6 +43,9 @@ public class MappedSchema
@XmlElementWrapper(name = "tables")
@XmlElement(name = "table")
protected List<MappedTable> tables;
@XmlElementWrapper(name = "udts")
@XmlElement(name = "udt")
protected List<MappedUDT> udts;
/**
* The input schema name as defined in {@link org.jooq.Schema#getName()}
@ -121,6 +124,17 @@ public class MappedSchema
this.tables = tables;
}
public List<MappedUDT> getUdts() {
if (udts == null) {
udts = new ArrayList<MappedUDT>();
}
return udts;
}
public void setUdts(List<MappedUDT> udts) {
this.udts = udts;
}
/**
* The input schema name as defined in {@link org.jooq.Schema#getName()}
* <p>
@ -178,12 +192,34 @@ public class MappedSchema
return this;
}
public MappedSchema withUdts(MappedUDT... values) {
if (values!= null) {
for (MappedUDT value: values) {
getUdts().add(value);
}
}
return this;
}
public MappedSchema withUdts(Collection<MappedUDT> values) {
if (values!= null) {
getUdts().addAll(values);
}
return this;
}
public MappedSchema withUdts(List<MappedUDT> udts) {
setUdts(udts);
return this;
}
@Override
public final void appendTo(XMLBuilder builder) {
builder.append("input", input);
builder.append("inputExpression", inputExpression);
builder.append("output", output);
builder.append("tables", "table", tables);
builder.append("udts", "udt", udts);
}
@Override
@ -241,6 +277,15 @@ public class MappedSchema
return false;
}
}
if (udts == null) {
if (other.udts!= null) {
return false;
}
} else {
if (!udts.equals(other.udts)) {
return false;
}
}
return true;
}
@ -252,6 +297,7 @@ public class MappedSchema
result = ((prime*result)+((inputExpression == null)? 0 :inputExpression.pattern().hashCode()));
result = ((prime*result)+((output == null)? 0 :output.hashCode()));
result = ((prime*result)+((tables == null)? 0 :tables.hashCode()));
result = ((prime*result)+((udts == null)? 0 :udts.hashCode()));
return result;
}

View File

@ -0,0 +1,77 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.conf;
import java.util.regex.Pattern;
import org.jooq.Qualified;
/**
* A common base type for objects contained in {@link MappedSchema}, such as
* {@link MappedTable} or {@link MappedUDT}.
*
* @author Lukas Eder
*/
public interface MappedSchemaObject {
/**
* The input name as defined in {@link Qualified#getName()}
* <p>
* Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided.
*/
String getInput();
/**
* A regular expression matching the input name as defined in
* {@link Qualified#getName()}
* <p>
* Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided
*/
Pattern getInputExpression();
/**
* The output name as it will be rendered in SQL.
* <ul>
* <li>When &lt;input/&gt; is provided, &lt;output/&gt; is a constant
* value.</li>
* <li>When &lt;inputExpression/&gt; is provided, &lt;output/&gt; is a
* replacement expression.</li>
* </ul>
*/
String getOutput();
}

View File

@ -27,7 +27,7 @@ import org.jooq.util.jaxb.tools.XMLBuilder;
})
public class MappedTable
extends SettingsBase
implements Serializable, Cloneable, XMLAppendable
implements Serializable, Cloneable, MappedSchemaObject, XMLAppendable
{
private final static long serialVersionUID = 31900L;

View File

@ -0,0 +1,206 @@
package org.jooq.conf;
import java.io.Serializable;
import java.util.regex.Pattern;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlType;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.jooq.util.jaxb.tools.XMLAppendable;
import org.jooq.util.jaxb.tools.XMLBuilder;
/**
* A udt mapping configuration.
*
*
*
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "MappedUDT", propOrder = {
})
@SuppressWarnings({
"all"
})
public class MappedUDT
extends SettingsBase
implements Serializable, Cloneable, MappedSchemaObject, XMLAppendable
{
private final static long serialVersionUID = 31900L;
protected String input;
@XmlElement(type = String.class)
@XmlJavaTypeAdapter(RegexAdapter.class)
protected Pattern inputExpression;
@XmlElement(required = true)
protected String output;
/**
* The input UDT as defined in {@link org.jooq.UDT#getName()}
* <p>
* Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided.
*
*/
public String getInput() {
return input;
}
/**
* The input UDT as defined in {@link org.jooq.UDT#getName()}
* <p>
* Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided.
*
*/
public void setInput(String value) {
this.input = value;
}
/**
* A regular expression matching the input UDT name as defined in {@link org.jooq.UDT#getName()}
* <p>
* Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided
*
*/
public Pattern getInputExpression() {
return inputExpression;
}
/**
* A regular expression matching the input UDT name as defined in {@link org.jooq.UDT#getName()}
* <p>
* Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided
*
*/
public void setInputExpression(Pattern value) {
this.inputExpression = value;
}
/**
* The output UDT as it will be rendered in SQL.
* <ul>
* <li>When &lt;input/&gt; is provided, &lt;output/&gt; is a constant value.</li>
* <li>When &lt;inputExpression/&gt; is provided, &lt;output/&gt; is a replacement expression.</li>
* </ul>
*
*/
public String getOutput() {
return output;
}
/**
* The output UDT as it will be rendered in SQL.
* <ul>
* <li>When &lt;input/&gt; is provided, &lt;output/&gt; is a constant value.</li>
* <li>When &lt;inputExpression/&gt; is provided, &lt;output/&gt; is a replacement expression.</li>
* </ul>
*
*/
public void setOutput(String value) {
this.output = value;
}
/**
* The input UDT as defined in {@link org.jooq.UDT#getName()}
* <p>
* Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided.
*
*/
public MappedUDT withInput(String value) {
setInput(value);
return this;
}
/**
* A regular expression matching the input UDT name as defined in {@link org.jooq.UDT#getName()}
* <p>
* Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided
*
*/
public MappedUDT withInputExpression(Pattern value) {
setInputExpression(value);
return this;
}
/**
* The output UDT as it will be rendered in SQL.
* <ul>
* <li>When &lt;input/&gt; is provided, &lt;output/&gt; is a constant value.</li>
* <li>When &lt;inputExpression/&gt; is provided, &lt;output/&gt; is a replacement expression.</li>
* </ul>
*
*/
public MappedUDT withOutput(String value) {
setOutput(value);
return this;
}
@Override
public final void appendTo(XMLBuilder builder) {
builder.append("input", input);
builder.append("inputExpression", inputExpression);
builder.append("output", output);
}
@Override
public String toString() {
XMLBuilder builder = XMLBuilder.nonFormatting();
appendTo(builder);
return builder.toString();
}
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass()!= that.getClass()) {
return false;
}
MappedUDT other = ((MappedUDT) that);
if (input == null) {
if (other.input!= null) {
return false;
}
} else {
if (!input.equals(other.input)) {
return false;
}
}
if (inputExpression == null) {
if (other.inputExpression!= null) {
return false;
}
} else {
if (!inputExpression.pattern().equals(other.inputExpression.pattern())) {
return false;
}
}
if (output == null) {
if (other.output!= null) {
return false;
}
} else {
if (!output.equals(other.output)) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = ((prime*result)+((input == null)? 0 :input.hashCode()));
result = ((prime*result)+((inputExpression == null)? 0 :inputExpression.pattern().hashCode()));
result = ((prime*result)+((output == null)? 0 :output.hashCode()));
return result;
}
}

View File

@ -97,6 +97,14 @@ public class ObjectFactory {
return new MappedTable();
}
/**
* Create an instance of {@link MappedUDT }
*
*/
public MappedUDT createMappedUDT() {
return new MappedUDT();
}
/**
* Create an instance of {@link RenderFormatting }
*

View File

@ -329,6 +329,7 @@ import org.jooq.TableElement;
import org.jooq.TableField;
import org.jooq.TableRecord;
import org.jooq.UDT;
import org.jooq.UDTRecord;
import org.jooq.UpdatableRecord;
import org.jooq.WindowSpecification;
import org.jooq.XML;
@ -3665,6 +3666,16 @@ final class Tools {
return table;
}
/**
* Map a {@link UDT} according to the configured {@link org.jooq.SchemaMapping}
*/
static final <R extends UDTRecord<R>> UDT<R> getMappedUDT(Scope scope, UDT<R> udt) {
if (scope != null)
return scope.configuration().schemaMapping().map(udt);
return udt;
}
/**
* Map an {@link QualifiedRecord} according to the configured
* {@link org.jooq.SchemaMapping}
@ -3680,6 +3691,14 @@ final class Tools {
*/
static final String getMappedUDTName(Scope scope, QualifiedRecord<?> record) {
RecordQualifier<?> udt = record.getQualifier();
if (udt instanceof UDT<?> u) {
UDT<?> u2 = getMappedUDT(scope, u);
if (u2 != null && u2 != u)
return u2.getQualifiedName().unquotedName().toString();
}
Schema mapped = getMappedSchema(scope, udt.getSchema());
StringBuilder sb = new StringBuilder();

View File

@ -1778,6 +1778,10 @@ Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided]]></jxb:javad
<element name="tables" type="jooq-runtime:MappedTables" minOccurs="0" maxOccurs="1">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Configure table mapping for runtime table rewriting in generated SQL.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="udts" type="jooq-runtime:MappedUDTs" minOccurs="0" maxOccurs="1">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Configure table mapping for runtime UDT rewriting in generated SQL.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
</all>
</complexType>
@ -1814,6 +1818,39 @@ Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided]]></jxb:javad
</all>
</complexType>
<complexType name="MappedUDTs">
<sequence>
<element name="udt" type="jooq-runtime:MappedUDT" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
</complexType>
<complexType name="MappedUDT">
<annotation><appinfo><jxb:class><jxb:javadoc><![CDATA[A udt mapping configuration.]]></jxb:javadoc></jxb:class></appinfo></annotation>
<all>
<element name="input" type="string" minOccurs="0" maxOccurs="1">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[The input UDT as defined in {@link org.jooq.UDT#getName()}
<p>
Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="inputExpression" type="string" minOccurs="0" maxOccurs="1">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[A regular expression matching the input UDT name as defined in {@link org.jooq.UDT#getName()}
<p>
Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="output" type="string" minOccurs="1" maxOccurs="1">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[The output UDT as it will be rendered in SQL.
<ul>
<li>When &lt;input/&gt; is provided, &lt;output/&gt; is a constant value.</li>
<li>When &lt;inputExpression/&gt; is provided, &lt;output/&gt; is a replacement expression.</li>
</ul>]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
</all>
</complexType>
<simpleType name="ParamType">
<restriction base="string">