diff --git a/jOOQ-codegen/src/main/java/org/jooq/codegen/GenerationUtil.java b/jOOQ-codegen/src/main/java/org/jooq/codegen/GenerationUtil.java index 7785ce9b09..e5be3ca51d 100644 --- a/jOOQ-codegen/src/main/java/org/jooq/codegen/GenerationUtil.java +++ b/jOOQ-codegen/src/main/java/org/jooq/codegen/GenerationUtil.java @@ -548,7 +548,7 @@ class GenerationUtil { } static ExpressionType expressionType(String expression) { - if (TYPE_REFERENCE_PATTERN.matcher(expression).matches()) + if (!"null".equals(expression) && TYPE_REFERENCE_PATTERN.matcher(expression).matches()) return CONSTRUCTOR_REFERENCE; else return EXPRESSION; diff --git a/jOOQ-codegen/src/main/java/org/jooq/codegen/JavaGenerator.java b/jOOQ-codegen/src/main/java/org/jooq/codegen/JavaGenerator.java index 56ba9f9f9e..5923ead9af 100644 --- a/jOOQ-codegen/src/main/java/org/jooq/codegen/JavaGenerator.java +++ b/jOOQ-codegen/src/main/java/org/jooq/codegen/JavaGenerator.java @@ -5796,6 +5796,20 @@ public class JavaGenerator extends AbstractGenerator { final String columnName = column.getName(); final List converter = out.ref(list(column.getType(resolver(out)).getConverter())); final List binding = out.ref(list(column.getType(resolver(out)).getBinding())); + final List generator = new ArrayList<>(); + + + + + + + + + + + + + final String columnVisibility = @@ -5807,19 +5821,19 @@ public class JavaGenerator extends AbstractGenerator { out.javadoc("The column %s.[[before= ][%s]]", column.getQualifiedOutputName(), list(escapeEntities(comment(column)))); if (scala) { - out.println("%sval %s: %s[%s, %s] = createField(%s.name(\"%s\"), %s, \"%s\"" + converterTemplate(converter) + converterTemplate(binding) + ")", - columnVisibility, scalaWhitespaceSuffix(columnId), TableField.class, recordType, columnType, DSL.class, columnName, columnTypeRef, escapeString(comment(column)), converter, binding); + out.println("%sval %s: %s[%s, %s] = createField(%s.name(\"%s\"), %s, \"%s\"" + converterTemplate(converter) + converterTemplate(binding) + converterTemplate(generator) + ")", + columnVisibility, scalaWhitespaceSuffix(columnId), TableField.class, recordType, columnType, DSL.class, columnName, columnTypeRef, escapeString(comment(column)), converter, binding, generator); } else if (kotlin) { - out.println("%sval %s: %s<%s, %s?> = createField(%s.name(\"%s\"), %s, this, \"%s\"" + converterTemplate(converter) + converterTemplate(binding) + ")", - columnVisibility, columnId, TableField.class, recordType, columnType, DSL.class, columnName, columnTypeRef, escapeString(comment(column)), converter, binding); + out.println("%sval %s: %s<%s, %s?> = createField(%s.name(\"%s\"), %s, this, \"%s\"" + converterTemplate(converter) + converterTemplate(binding) + converterTemplate(generator) + ")", + columnVisibility, columnId, TableField.class, recordType, columnType, DSL.class, columnName, columnTypeRef, escapeString(comment(column)), converter, binding, generator); } else { String isStatic = generateInstanceFields() ? "" : "static "; String tableRef = generateInstanceFields() ? "this" : out.ref(getStrategy().getJavaIdentifier(table), 2); - out.println("%s%sfinal %s<%s, %s> %s = createField(%s.name(\"%s\"), %s, %s, \"%s\"" + converterTemplate(converter) + converterTemplate(binding) + ");", - columnVisibility, isStatic, TableField.class, recordType, columnType, columnId, DSL.class, columnName, columnTypeRef, tableRef, escapeString(comment(column)), converter, binding); + out.println("%s%sfinal %s<%s, %s> %s = createField(%s.name(\"%s\"), %s, %s, \"%s\"" + converterTemplate(converter) + converterTemplate(binding) + converterTemplate(generator) + ");", + columnVisibility, isStatic, TableField.class, recordType, columnType, columnId, DSL.class, columnName, columnTypeRef, tableRef, escapeString(comment(column)), converter, binding, generator); } } diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/AbstractDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/AbstractDatabase.java index 21c4f468b7..3250dab59b 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/AbstractDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/AbstractDatabase.java @@ -1403,8 +1403,8 @@ public abstract class AbstractDatabase implements Database { } if (StringUtils.isBlank(type.getName())) { - if (StringUtils.isBlank(type.getUserType())) { - log.warn("Bad configuration for . Either or is required: " + type); + if (StringUtils.isBlank(type.getUserType()) && StringUtils.isBlank(type.getGenerator())) { + log.warn("Bad configuration for . Either , , or is required: " + type); it2.remove(); continue; @@ -1412,9 +1412,10 @@ public abstract class AbstractDatabase implements Database { if (StringUtils.isBlank(type.getBinding()) && StringUtils.isBlank(type.getConverter()) && + StringUtils.isBlank(type.getGenerator()) && !Boolean.TRUE.equals(type.isEnumConverter()) && type.getLambdaConverter() == null) { - log.warn("Bad configuration for . Either or or or is required: " + type); + log.warn("Bad configuration for . Either , , , , or is required: " + type); it2.remove(); continue; diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/AbstractTypedElementDefinition.java b/jOOQ-meta/src/main/java/org/jooq/meta/AbstractTypedElementDefinition.java index 4fd718ddf7..3dbfd806da 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/AbstractTypedElementDefinition.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/AbstractTypedElementDefinition.java @@ -210,6 +210,7 @@ public abstract class AbstractTypedElementDefinition ForcedType forcedType = db.getConfiguredForcedType(child, definedType); if (forcedType != null) { String uType = forcedType.getName(); + String generator = forcedType.getGenerator(); String converter = null; String binding = result.getBinding(); @@ -219,6 +220,9 @@ public abstract class AbstractTypedElementDefinition ? customType.getType() : customType.getName(); + if (generator == null) + generator = customType.getGenerator(); + // [#5877] [#6567] EnumConverters profit from simplified configuration if (Boolean.TRUE.equals(customType.isEnumConverter()) || EnumConverter.class.getName().equals(customType.getConverter())) { @@ -277,7 +281,7 @@ public abstract class AbstractTypedElementDefinition if (customType != null) log.warn("Custom type conflict", child + " has custom type " + customType + " forced by " + forcedType + " but a data type rewrite applies"); - result = new DefaultDataTypeDefinition(db, child.getSchema(), uType, l, p, s, n, r, g, d, i, (Name) null, converter, binding, null); + result = new DefaultDataTypeDefinition(db, child.getSchema(), uType, l, p, s, n, r, g, d, i, (Name) null, generator, converter, binding, null); } // Other forced types are UDT's, enums, etc. @@ -287,7 +291,7 @@ public abstract class AbstractTypedElementDefinition s = result.getScale(); String t = result.getType(); Name u = result.getQualifiedUserType(); - result = new DefaultDataTypeDefinition(db, definedType.getSchema(), t, l, p, s, n, r, g, d, i, u, converter, binding, uType); + result = new DefaultDataTypeDefinition(db, definedType.getSchema(), t, l, p, s, n, r, g, d, i, u, generator, converter, binding, uType); } // [#4597] If we don't have a type-rewrite (forcedDataType) or a @@ -302,6 +306,9 @@ public abstract class AbstractTypedElementDefinition log.warn("Bad configuration for " + forcedType.getName() + ". No matching found, and no matching SQLDataType found: " + forcedType); } } + + if (generator != null) + ((DefaultDataTypeDefinition) result).generator(generator); } return result; @@ -341,6 +348,7 @@ public abstract class AbstractTypedElementDefinition .withBinding(forcedType.getBinding()) .withEnumConverter(forcedType.isEnumConverter()) .withLambdaConverter(forcedType.getLambdaConverter()) + .withGenerator(forcedType.getGenerator()) .withConverter(forcedType.getConverter()) .withName(name) .withType(forcedType.getUserType()); diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/DataTypeDefinition.java b/jOOQ-meta/src/main/java/org/jooq/meta/DataTypeDefinition.java index 8559c61d20..af60ba89a1 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/DataTypeDefinition.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/DataTypeDefinition.java @@ -41,9 +41,12 @@ import java.util.List; // ... import org.jooq.Name; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; import org.jooq.meta.jaxb.ForcedType; +import org.jetbrains.annotations.Nullable; + /** * A definition for a data type object. * @@ -56,16 +59,25 @@ public interface DataTypeDefinition { */ String getType(); + /** + * The generator type that is applied to this data type, or + * null, if no such generator type is configured. + */ + @Nullable + String getGenerator(); + /** * The converter type that is applied to this data type, or * null, if no such converter type is configured. */ + @Nullable String getConverter(); /** * The binding type that is applied to this data type, or * null, if no such binding type is configured. */ + @Nullable String getBinding(); /** @@ -131,6 +143,11 @@ public interface DataTypeDefinition { */ GenerationOption getGenerationOption(); + /** + * The computed column generation location. + */ + GenerationLocation getGenerationLocation(); + /** * Whether this data type is an identity. */ diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/DefaultDataTypeDefinition.java b/jOOQ-meta/src/main/java/org/jooq/meta/DefaultDataTypeDefinition.java index 7335704521..781c410bf1 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/DefaultDataTypeDefinition.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/DefaultDataTypeDefinition.java @@ -51,6 +51,7 @@ import java.util.Set; import org.jooq.Name; import org.jooq.SQLDialect; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; import org.jooq.tools.JooqLogger; import org.jooq.tools.StringUtils; @@ -71,6 +72,7 @@ public class DefaultDataTypeDefinition implements DataTypeDefinition { private final String type; private final Name userType; private final String javaType; + private String generator; private final String converter; private final String binding; private final boolean nullable; @@ -196,6 +198,10 @@ public class DefaultDataTypeDefinition implements DataTypeDefinition { } public DefaultDataTypeDefinition(Database database, SchemaDefinition schema, String typeName, Number length, Number precision, Number scale, Boolean nullable, boolean readonly, String generatedAlwaysAs, String defaultValue, boolean identity, Name userType, String converter, String binding, String javaType) { + this(database, schema, typeName, length, precision, scale, nullable, readonly, generatedAlwaysAs, defaultValue, identity, userType, null, converter, binding, javaType); + } + + public DefaultDataTypeDefinition(Database database, SchemaDefinition schema, String typeName, Number length, Number precision, Number scale, Boolean nullable, boolean readonly, String generatedAlwaysAs, String defaultValue, boolean identity, Name userType, String generator, String converter, String binding, String javaType) { this.database = database; this.schema = schema; @@ -203,6 +209,7 @@ public class DefaultDataTypeDefinition implements DataTypeDefinition { this.type = typeName == null ? "OTHER" : typeName; this.userType = userType; this.javaType = javaType; + this.generator = generator; this.converter = converter; this.binding = binding; @@ -228,6 +235,9 @@ public class DefaultDataTypeDefinition implements DataTypeDefinition { this.generatedAlwaysAs = generatedAlwaysAs; this.defaultValue = defaultValue; this.identity = identity; + + if (generator != null) + database.create().configuration().commercial(() -> "Client side computed columns are a commercial only feature. Please consider upgrading to the jOOQ Professional Edition or jOOQ Enterprise Edition."); } @Override @@ -284,6 +294,21 @@ public class DefaultDataTypeDefinition implements DataTypeDefinition { return generationOption; } + @Override + public GenerationLocation getGenerationLocation() { + return generator == null ? GenerationLocation.SERVER : GenerationLocation.CLIENT; + } + + private static final JooqLogger logGenerator = JooqLogger.getLogger(DefaultDataTypeDefinition.class, "logGenerator", 1); + + public final DefaultDataTypeDefinition generator(String g) { + if (g != null && !database.create().configuration().commercial()) + logGenerator.info("Computed columns", "Client side computed columns are a commercial only jOOQ feature. If you wish to profit from this feature, please upgrade to the jOOQ Professional Edition"); + + this.generator = g; + return this; + } + public final DefaultDataTypeDefinition generationOption(GenerationOption go) { this.generationOption = go; return this; @@ -335,6 +360,11 @@ public class DefaultDataTypeDefinition implements DataTypeDefinition { return type; } + @Override + public final String getGenerator() { + return generator; + } + @Override public final String getConverter() { return converter; diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/jaxb/CustomType.java b/jOOQ-meta/src/main/java/org/jooq/meta/jaxb/CustomType.java index a77ba044fa..1ee884684e 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/jaxb/CustomType.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/jaxb/CustomType.java @@ -36,6 +36,8 @@ public class CustomType implements Serializable, XMLAppendable @XmlJavaTypeAdapter(StringAdapter.class) protected String type; @XmlJavaTypeAdapter(StringAdapter.class) + protected String generator; + @XmlJavaTypeAdapter(StringAdapter.class) protected String converter; protected Boolean enumConverter; protected LambdaConverter lambdaConverter; @@ -78,6 +80,24 @@ public class CustomType implements Serializable, XMLAppendable this.type = value; } + /** + * @deprecated Use ForcedType only + * + */ + @Deprecated + public String getGenerator() { + return generator; + } + + /** + * @deprecated Use ForcedType only + * + */ + @Deprecated + public void setGenerator(String value) { + this.generator = value; + } + /** * @deprecated Use ForcedType only * @@ -178,6 +198,16 @@ public class CustomType implements Serializable, XMLAppendable return this; } + /** + * @deprecated Use ForcedType only + * + */ + @Deprecated + public CustomType withGenerator(String value) { + setGenerator(value); + return this; + } + /** * @deprecated Use ForcedType only * @@ -217,6 +247,7 @@ public class CustomType implements Serializable, XMLAppendable public final void appendTo(XMLBuilder builder) { builder.append("name", name); builder.append("type", type); + builder.append("generator", generator); builder.append("converter", converter); builder.append("enumConverter", enumConverter); builder.append("lambdaConverter", lambdaConverter); @@ -260,6 +291,15 @@ public class CustomType implements Serializable, XMLAppendable return false; } } + if (generator == null) { + if (other.generator!= null) { + return false; + } + } else { + if (!generator.equals(other.generator)) { + return false; + } + } if (converter == null) { if (other.converter!= null) { return false; @@ -305,6 +345,7 @@ public class CustomType implements Serializable, XMLAppendable int result = 1; result = ((prime*result)+((name == null)? 0 :name.hashCode())); result = ((prime*result)+((type == null)? 0 :type.hashCode())); + result = ((prime*result)+((generator == null)? 0 :generator.hashCode())); result = ((prime*result)+((converter == null)? 0 :converter.hashCode())); result = ((prime*result)+((enumConverter == null)? 0 :enumConverter.hashCode())); result = ((prime*result)+((lambdaConverter == null)? 0 :lambdaConverter.hashCode())); diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/jaxb/ForcedType.java b/jOOQ-meta/src/main/java/org/jooq/meta/jaxb/ForcedType.java index ce5a84f5f2..9594bc8667 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/jaxb/ForcedType.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/jaxb/ForcedType.java @@ -37,6 +37,8 @@ public class ForcedType implements Serializable, XMLAppendable @XmlJavaTypeAdapter(StringAdapter.class) protected String userType; @XmlJavaTypeAdapter(StringAdapter.class) + protected String generator; + @XmlJavaTypeAdapter(StringAdapter.class) protected String converter; protected Boolean enumConverter; protected LambdaConverter lambdaConverter; @@ -119,6 +121,26 @@ public class ForcedType implements Serializable, XMLAppendable this.userType = value; } + /** + * A {@link org.jooq.Generator} implementation used for client-side computed columns. + *

+ * This feature is available in the commercial distribution only. + * + */ + public String getGenerator() { + return generator; + } + + /** + * A {@link org.jooq.Generator} implementation used for client-side computed columns. + *

+ * This feature is available in the commercial distribution only. + * + */ + public void setGenerator(String value) { + this.generator = value; + } + /** * A converter implementation for the {@link #getUserType()}. * @@ -395,6 +417,17 @@ public class ForcedType implements Serializable, XMLAppendable return this; } + /** + * A {@link org.jooq.Generator} implementation used for client-side computed columns. + *

+ * This feature is available in the commercial distribution only. + * + */ + public ForcedType withGenerator(String value) { + setGenerator(value); + return this; + } + /** * A converter implementation for the {@link #getUserType()}. * @@ -529,6 +562,7 @@ public class ForcedType implements Serializable, XMLAppendable builder.append("priority", priority); builder.append("name", name); builder.append("userType", userType); + builder.append("generator", generator); builder.append("converter", converter); builder.append("enumConverter", enumConverter); builder.append("lambdaConverter", lambdaConverter); @@ -591,6 +625,15 @@ public class ForcedType implements Serializable, XMLAppendable return false; } } + if (generator == null) { + if (other.generator!= null) { + return false; + } + } else { + if (!generator.equals(other.generator)) { + return false; + } + } if (converter == null) { if (other.converter!= null) { return false; @@ -727,6 +770,7 @@ public class ForcedType implements Serializable, XMLAppendable result = ((prime*result)+((priority == null)? 0 :priority.hashCode())); result = ((prime*result)+((name == null)? 0 :name.hashCode())); result = ((prime*result)+((userType == null)? 0 :userType.hashCode())); + result = ((prime*result)+((generator == null)? 0 :generator.hashCode())); result = ((prime*result)+((converter == null)? 0 :converter.hashCode())); result = ((prime*result)+((enumConverter == null)? 0 :enumConverter.hashCode())); result = ((prime*result)+((lambdaConverter == null)? 0 :lambdaConverter.hashCode())); diff --git a/jOOQ-meta/src/main/resources/org/jooq/meta/xsd/jooq-codegen-3.17.0.xsd b/jOOQ-meta/src/main/resources/org/jooq/meta/xsd/jooq-codegen-3.17.0.xsd index 62f981161f..78ee4f6951 100644 --- a/jOOQ-meta/src/main/resources/org/jooq/meta/xsd/jooq-codegen-3.17.0.xsd +++ b/jOOQ-meta/src/main/resources/org/jooq/meta/xsd/jooq-codegen-3.17.0.xsd @@ -1339,6 +1339,18 @@ This feature is available in the commercial distribution only.]]>< + + + + + + + @java.lang.Deprecated + @java.lang.Deprecated + + + + @@ -1473,6 +1485,12 @@ This feature is available in the commercial distribution only.]]>< If provided, {@link #getName()} will be ignored, and either {@link #getConverter()} or {@link #getBinding()} is required]]> + + + +This feature is available in the commercial distribution only.]]> + diff --git a/jOOQ/src/main/java/org/jooq/DataType.java b/jOOQ/src/main/java/org/jooq/DataType.java index 3ffcc4b113..dcb6f0a55b 100644 --- a/jOOQ/src/main/java/org/jooq/DataType.java +++ b/jOOQ/src/main/java/org/jooq/DataType.java @@ -77,9 +77,9 @@ import java.util.List; import java.util.function.Function; import org.jooq.Converters.UnknownType; -import org.jooq.exception.DataAccessException; import org.jooq.exception.DataTypeException; import org.jooq.impl.DSL; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; import org.jooq.impl.SQLDataType; import org.jooq.types.DayToSecond; @@ -467,6 +467,32 @@ public interface DataType extends Named { */ boolean readonly(); + /** + * Get the readonly attribute of this data type, combined with other flags + * that influence readonly behaviour. + *

+ * A column may be marked as {@link #readonly()} for various reasons, + * including: + *

    + *
  • When it is marked as readonly explicitly by the code generator.
  • + *
  • When it is marked as readonly implicitly because it's a computed + * column with {@link #generationLocation()} being + * {@link GenerationLocation#SERVER}.
  • + *
+ *

+ * Some columns are readonly for users, meaning users of the jOOQ API cannot + * write to them, but jOOQ, internally, may still write to those columns. + * Such columns may include: + *

    + *
  • Columns that are computed with {@link #generationLocation()} being + * {@link GenerationLocation#CLIENT}
  • + *
  • Columns used for optimistic locking
  • + *
+ *

+ * This feature is implemented in commercial distributions only. + */ + boolean readonlyInternal(); + /** * Whether this column is computed. *

@@ -475,7 +501,17 @@ public interface DataType extends Named { boolean computed(); /** - * Set the computed column expression of this data type. + * Whether this column is computed on the client. + *

+ * This is true only if {@link #computed()} and + * {@link #generationLocation()} == {@link GenerationLocation#CLIENT}. + *

+ * This feature is implemented in commercial distributions only. + */ + boolean computedOnClient(); + + /** + * Set the computed column expression of this data type to a constant value. *

* This implicitly sets {@link #readonly()} to true. *

@@ -486,7 +522,8 @@ public interface DataType extends Named { DataType generatedAlwaysAs(T generatedAlwaysAsValue); /** - * Set the computed column expression of this data type. + * Set the computed column expression of this data type to a constant + * expression. *

* This implicitly sets {@link #readonly()} to true. *

@@ -496,14 +533,50 @@ public interface DataType extends Named { @Support({ DERBY, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) DataType generatedAlwaysAs(Field generatedAlwaysAsValue); + /** + * Set the computed column expression of this data type to a dynamic + * expression. + *

+ * Unlike {@link #generatedAlwaysAs(Object)} and + * {@link #generatedAlwaysAs(Field)}, which produce a constant value or + * expression, this allows for generating a dynamic expression if used along + * with {@link #generationLocation()} and {@link GenerationLocation#CLIENT}, + * in order to implement client side computed columns. + *

+ * If {@link #generationLocation()} is {@link GenerationLocation#SERVER}, + * then this does not affect generated DML statements, and will be evaluated + * only in DDL statements, when creating the table. + *

+ * This implicitly sets {@link #readonly()} to true. + *

+ * This feature is implemented in commercial distributions only. + */ + @NotNull + DataType generatedAlwaysAs(Generator generatedAlwaysAsValue); + /** * Get the computed column expression of this data type, if any. *

+ * This eagerly evaluates the {@link #generatedAlwaysAsGenerator()} + * generator, which may not produce the same expression upon execution of a + * query, in case {@link #generationLocation()} is + * {@link GenerationLocation#CLIENT}. The behaviour of + * {@link GenerationLocation#SERVER} is not affected. The method has been + * left unmodified for backwards compatibility with jOOQ 3.16. + *

* This feature is implemented in commercial distributions only. */ @Nullable Field generatedAlwaysAs(); + /** + * Get the computed column expression of this data type, if any. + *

+ * This feature is implemented in commercial distributions only. + */ + @Nullable + Generator generatedAlwaysAsGenerator(); + /** * Set the {@link #generationOption()} of the computed column expression to * {@link GenerationOption#STORED}. @@ -549,6 +622,46 @@ public interface DataType extends Named { @Support GenerationOption generationOption(); + /** + * Set the {@link #generationLocation()} of the computed column expression. + *

+ * Specifies whether the {@link #generatedAlwaysAs()} expression is computed + * on the {@link GenerationLocation#SERVER} (by default) or in the + * {@link GenerationLocation#CLIENT}. The latter is supported in all + * dialects, the former only in relevant dialects. + *

+ * The computation happens in {@link Insert}, {@link Update}, or + * {@link Merge} statements in case {@link #generationOption()} is + * {@link GenerationOption#STORED}, or in {@link Select} in case the + * {@link #generationOption()} is {@link GenerationOption#VIRTUAL}. + *

+ * This feature is implemented in commercial distributions only. + */ + @NotNull + @Support + DataType generationLocation(GenerationLocation generationOption); + + /** + * Get the {@link GenerationLocation} of the computed column expression of + * this data type, if any. + *

+ * Specifies whether the {@link #generatedAlwaysAs()} expression is computed + * on the {@link GenerationLocation#SERVER} (by default) or in the + * {@link GenerationLocation#CLIENT}. The latter is supported in all + * dialects, the former only in relevant dialects. + *

+ * The computation happens in {@link Insert}, {@link Update}, or + * {@link Merge} statements in case {@link #generationOption()} is + * {@link GenerationOption#STORED}, or in {@link Select} in case the + * {@link #generationOption()} is {@link GenerationOption#VIRTUAL}. + *

+ *

+ * This feature is implemented in commercial distributions only. + */ + @NotNull + @Support + GenerationLocation generationLocation(); + /** * Synonym for {@link #nullable(boolean)}, passing true as an * argument. diff --git a/jOOQ/src/main/java/org/jooq/Generator.java b/jOOQ/src/main/java/org/jooq/Generator.java new file mode 100644 index 0000000000..8dd083cd7e --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/Generator.java @@ -0,0 +1,48 @@ +/* + * 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 + * + * http://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: http://www.jooq.org/licenses + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq; + +import java.io.Serializable; +import java.util.function.Supplier; + +/** + * A generator can be used with {@link DataType#generatedAlwaysAs(Generator)} to + * implement dynamic, client side computed columns. + */ +@FunctionalInterface +public interface Generator extends Supplier>, Serializable {} diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractDMLQuery.java b/jOOQ/src/main/java/org/jooq/impl/AbstractDMLQuery.java index 533a43ddf9..1ac8cc3cc4 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractDMLQuery.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractDMLQuery.java @@ -195,8 +195,8 @@ abstract class AbstractDMLQuery extends AbstractRowCountQuery - private final WithImpl with; - private final Table table; + final WithImpl with; + final Table table; final SelectFieldList returning; final List> returningResolvedAsterisks; Result returnedResult; diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractDataType.java b/jOOQ/src/main/java/org/jooq/impl/AbstractDataType.java index 70a971adbb..96c4284419 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractDataType.java @@ -70,6 +70,7 @@ import java.time.OffsetTime; import java.util.Collection; import java.util.List; import java.util.Set; +import java.util.function.Supplier; // ... // ... @@ -85,6 +86,7 @@ import org.jooq.Domain; import org.jooq.EmbeddableRecord; import org.jooq.EnumType; import org.jooq.Field; +import org.jooq.Generator; import org.jooq.Geography; import org.jooq.Geometry; import org.jooq.JSON; @@ -98,11 +100,13 @@ import org.jooq.Result; import org.jooq.Row; import org.jooq.SQLDialect; import org.jooq.XML; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; import org.jooq.impl.QOM.UEmpty; import org.jooq.types.Interval; import org.jooq.types.UNumber; +import jakarta.persistence.GenerationType; // ... /** @@ -149,12 +153,22 @@ implements @Override public abstract boolean readonly(); + @Override + public final boolean readonlyInternal() { + return readonly() && !computedOnClient(); + } + @Override public abstract DataType readonly(boolean r); @Override public final boolean computed() { - return generatedAlwaysAs() != null; + return generatedAlwaysAsGenerator() != null; + } + + @Override + public final boolean computedOnClient() { + return computed() && generationLocation() == GenerationLocation.CLIENT; } @Override @@ -163,10 +177,21 @@ implements } @Override - public abstract DataType generatedAlwaysAs(Field generatedAlwaysAsValue); + public final DataType generatedAlwaysAs(Field generatedAlwaysAsValue) { + return generatedAlwaysAs(() -> generatedAlwaysAsValue); + } @Override - public abstract Field generatedAlwaysAs(); + public abstract DataType generatedAlwaysAs(Generator generatedAlwaysAsValue); + + @Override + public final Field generatedAlwaysAs() { + Generator s = generatedAlwaysAsGenerator(); + return s == null ? null : s.get(); + } + + @Override + public abstract Generator generatedAlwaysAsGenerator(); @Override public final DataType stored() { @@ -184,6 +209,12 @@ implements @Override public abstract GenerationOption generationOption(); + @Override + public abstract DataType generationLocation(GenerationLocation generationLocation); + + @Override + public abstract GenerationLocation generationLocation(); + @Override public abstract DataType collation(Collation c); diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractDataTypeX.java b/jOOQ/src/main/java/org/jooq/impl/AbstractDataTypeX.java index 420d541137..6c3890b578 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractDataTypeX.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractDataTypeX.java @@ -40,13 +40,17 @@ package org.jooq.impl; import static org.jooq.Nullability.NOT_NULL; import static org.jooq.impl.Tools.CONFIG; +import java.util.function.Supplier; + import org.jooq.CharacterSet; import org.jooq.Collation; import org.jooq.Comment; import org.jooq.DataType; import org.jooq.Field; +import org.jooq.Generator; import org.jooq.Name; import org.jooq.Nullability; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; import org.jooq.tools.JooqLogger; @@ -69,8 +73,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { Integer newLength, Nullability newNullability, boolean newReadonly, - Field newGeneratedAlwaysAs, + Generator newGeneratedAlwaysAs, GenerationOption newGenerationOption, + GenerationLocation newGenerationLocation, Collation newCollation, CharacterSet newCharacterSet, boolean newIdentity, @@ -85,8 +90,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { length0(), n, readonly(), - generatedAlwaysAs(), + generatedAlwaysAsGenerator(), generationOption(), + generationLocation(), collation(), characterSet(), !n.nullable() && identity(), @@ -105,8 +111,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { length0(), nullability(), r, - generatedAlwaysAs(), + generatedAlwaysAsGenerator(), generationOption(), + generationLocation(), collation(), characterSet(), identity(), @@ -117,7 +124,7 @@ abstract class AbstractDataTypeX extends AbstractDataType { private static final JooqLogger logGeneratedAlwaysAs = JooqLogger.getLogger(AbstractDataTypeX.class, "generateAlwaysAs", 1); @Override - public final DataType generatedAlwaysAs(Field g) { + public final DataType generatedAlwaysAs(Generator g) { if (g != null && !CONFIG.commercial()) logGeneratedAlwaysAs.info("Computed columns", "Computed columns are a commercial only jOOQ feature. If you wish to profit from this feature, please upgrade to the jOOQ Professional Edition"); @@ -129,6 +136,7 @@ abstract class AbstractDataTypeX extends AbstractDataType { g != null ? true : readonly(), g, generationOption(), + generationLocation(), collation(), characterSet(), identity(), @@ -147,7 +155,29 @@ abstract class AbstractDataTypeX extends AbstractDataType { length0(), nullability(), readonly(), - generatedAlwaysAs(), + generatedAlwaysAsGenerator(), + g, + generationLocation(), + collation(), + characterSet(), + identity(), + defaultValue() + ); + } + + @Override + public final DataType generationLocation(GenerationLocation g) { + if (g != null && !CONFIG.commercial()) + logGeneratedAlwaysAs.info("Computed columns", "Computed columns are a commercial only jOOQ feature. If you wish to profit from this feature, please upgrade to the jOOQ Professional Edition"); + + return construct( + precision0(), + scale0(), + length0(), + nullability(), + readonly(), + generatedAlwaysAsGenerator(), + generationOption(), g, collation(), characterSet(), @@ -164,8 +194,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { length0(), nullability(), readonly(), - generatedAlwaysAs(), + generatedAlwaysAsGenerator(), generationOption(), + generationLocation(), c, characterSet(), identity(), @@ -181,8 +212,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { length0(), nullability(), readonly(), - generatedAlwaysAs(), + generatedAlwaysAsGenerator(), generationOption(), + generationLocation(), collation(), c, identity(), @@ -198,8 +230,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { length0(), i ? NOT_NULL : nullability(), readonly(), - generatedAlwaysAs(), + generatedAlwaysAsGenerator(), generationOption(), + generationLocation(), collation(), characterSet(), i, @@ -215,8 +248,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { length0(), nullability(), readonly(), - d != null ? null : generatedAlwaysAs(), + d != null ? null : generatedAlwaysAsGenerator(), generationOption(), + generationLocation(), collation(), characterSet(), identity(), @@ -232,8 +266,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { length0(), nullability(), readonly(), - generatedAlwaysAs(), + generatedAlwaysAsGenerator(), generationOption(), + generationLocation(), collation(), characterSet(), identity(), @@ -249,8 +284,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { length0(), nullability(), readonly(), - generatedAlwaysAs(), + generatedAlwaysAsGenerator(), generationOption(), + generationLocation(), collation(), characterSet(), identity(), @@ -266,8 +302,9 @@ abstract class AbstractDataTypeX extends AbstractDataType { l, nullability(), readonly(), - generatedAlwaysAs(), + generatedAlwaysAsGenerator(), generationOption(), + generationLocation(), collation(), characterSet(), identity(), diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractTable.java b/jOOQ/src/main/java/org/jooq/impl/AbstractTable.java index 5398b1f9c0..f57d85c5b4 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractTable.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractTable.java @@ -87,6 +87,7 @@ import org.jooq.DataType; import org.jooq.DivideByOnStep; import org.jooq.Field; import org.jooq.ForeignKey; +import org.jooq.Generator; import org.jooq.Identity; import org.jooq.Index; import org.jooq.JoinType; @@ -118,6 +119,7 @@ import org.jooq.TablePartitionByStep; import org.jooq.UniqueKey; // ... // ... +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.tools.JooqLogger; import org.jetbrains.annotations.NotNull; @@ -756,16 +758,30 @@ abstract class AbstractTable extends AbstractNamed implements * @param name The name of the field (case-sensitive!) * @param type The data type of the field */ - @SuppressWarnings("unchecked") protected static final TableField createField(Name name, DataType type, Table table, String comment, Converter converter, Binding binding) { - final Binding actualBinding = DefaultBinding.newBinding(converter, type, binding); - final DataType actualType = + return createField(name, type, table, comment, converter, binding, null); + } + + /** + * Subclasses may call this method to create {@link TableField} objects that + * are linked to this table. + * + * @param name The name of the field (case-sensitive!) + * @param type The data type of the field + */ + @SuppressWarnings("unchecked") + protected static final TableField createField(Name name, DataType type, Table table, String comment, Converter converter, Binding binding, Generator generator) { + Binding actualBinding = DefaultBinding.newBinding(converter, type, binding); + DataType actualType = converter == null && binding == null ? (DataType) type : type.asConvertedDataType(actualBinding); + if (generator != null) + actualType = actualType.generatedAlwaysAs(generator).generationLocation(GenerationLocation.CLIENT); + // [#5999] TODO: Allow for user-defined Names - final TableFieldImpl tableField = new TableFieldImpl<>(name, actualType, table, DSL.comment(comment), actualBinding); + TableFieldImpl tableField = new TableFieldImpl<>(name, actualType, table, DSL.comment(comment), actualBinding); // [#1199] The public API of Table returns immutable field lists if (table instanceof TableImpl) diff --git a/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java b/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java index 395826421a..569b21ffeb 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java @@ -38,15 +38,16 @@ package org.jooq.impl; import static org.jooq.impl.Tools.CONFIG; -import static org.jooq.impl.Tools.CTX; import org.jooq.CharacterSet; import org.jooq.Collation; import org.jooq.Configuration; import org.jooq.DataType; import org.jooq.Field; +import org.jooq.Generator; import org.jooq.Nullability; import org.jooq.SQLDialect; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; /** @@ -75,14 +76,15 @@ final class ArrayDataType extends DefaultDataType { Integer length, Nullability nullability, boolean readonly, - Field generatedAlwaysAs, + Generator generatedAlwaysAs, GenerationOption generationOption, + GenerationLocation generationLocation, Collation collation, CharacterSet characterSet, boolean identity, Field defaultValue ) { - super(t, precision, scale, length, nullability, readonly, generatedAlwaysAs, generationOption, collation, characterSet, identity, defaultValue); + super(t, precision, scale, length, nullability, readonly, generatedAlwaysAs, generationOption, generationLocation, collation, characterSet, identity, defaultValue); this.elementType = elementType; } @@ -95,8 +97,9 @@ final class ArrayDataType extends DefaultDataType { Integer newLength, Nullability newNullability, boolean newReadonly, - Field newGeneratedAlwaysAs, + Generator newGeneratedAlwaysAs, GenerationOption newGenerationOption, + GenerationLocation newGenerationLocation, Collation newCollation, CharacterSet newCharacterSet, boolean newIdentity, @@ -112,6 +115,7 @@ final class ArrayDataType extends DefaultDataType { newReadonly, newGeneratedAlwaysAs, newGenerationOption, + newGenerationLocation, newCollation, newCharacterSet, newIdentity, diff --git a/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java b/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java index dd50cca622..86275082d0 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java @@ -48,6 +48,7 @@ import org.jooq.Converter; import org.jooq.Converters; import org.jooq.DataType; import org.jooq.Field; +import org.jooq.Generator; import org.jooq.Nullability; import org.jooq.Record; import org.jooq.Result; @@ -55,11 +56,9 @@ import org.jooq.Row; import org.jooq.SQLDialect; import org.jooq.exception.DataTypeException; import org.jooq.impl.DefaultBinding.InternalBinding; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - /** * A DataType used for converted types using {@link Converter} * @@ -89,8 +88,9 @@ final class ConvertedDataType extends AbstractDataTypeX { Integer newLength, Nullability newNullability, boolean newReadonly, - Field newGeneratedAlwaysAs, + Generator newGeneratedAlwaysAs, GenerationOption newGenerationOption, + GenerationLocation newGenerationLocation, Collation newCollation, CharacterSet newCharacterSet, boolean newIdentity, @@ -102,8 +102,9 @@ final class ConvertedDataType extends AbstractDataTypeX { newLength, newNullability, newReadonly, - (Field) newGeneratedAlwaysAs, + (Generator) newGeneratedAlwaysAs, newGenerationOption, + newGenerationLocation, newCollation, newCharacterSet, newIdentity, @@ -209,8 +210,8 @@ final class ConvertedDataType extends AbstractDataTypeX { } @Override - public final Field generatedAlwaysAs() { - return (Field) delegate.generatedAlwaysAs(); + public final Generator generatedAlwaysAsGenerator() { + return (Generator) delegate.generatedAlwaysAsGenerator(); } @Override @@ -218,6 +219,11 @@ final class ConvertedDataType extends AbstractDataTypeX { return delegate.generationOption(); } + @Override + public final GenerationLocation generationLocation() { + return delegate.generationLocation(); + } + @Override public final Collation collation() { return delegate.collation(); diff --git a/jOOQ/src/main/java/org/jooq/impl/DataTypeProxy.java b/jOOQ/src/main/java/org/jooq/impl/DataTypeProxy.java index b830a0f526..a3ee804dc0 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DataTypeProxy.java +++ b/jOOQ/src/main/java/org/jooq/impl/DataTypeProxy.java @@ -45,9 +45,11 @@ import org.jooq.Collation; import org.jooq.Configuration; import org.jooq.DataType; import org.jooq.Field; +import org.jooq.Generator; import org.jooq.Name; import org.jooq.Nullability; import org.jooq.SQLDialect; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; /** @@ -58,23 +60,25 @@ import org.jooq.impl.QOM.GenerationOption; */ final class DataTypeProxy extends AbstractDataType { - private AbstractDataType type; - private final Integer overridePrecision; - private final Integer overrideScale; - private final Integer overrideLength; - private final Nullability overrideNullability; - private final Boolean overrideReadonly; - private final Field overrideGeneratedAlwaysAs; - private final GenerationOption overrideGenerationOption; - private final Collation overrideCollation; - private final CharacterSet overrideCharacterSet; - private final Boolean overrideIdentity; - private final Field overrideDefaultValue; + private AbstractDataType type; + private final Integer overridePrecision; + private final Integer overrideScale; + private final Integer overrideLength; + private final Nullability overrideNullability; + private final Boolean overrideReadonly; + private final Generator overrideGeneratedAlwaysAs; + private final GenerationOption overrideGenerationOption; + private final GenerationLocation overrideGenerationLocation; + private final Collation overrideCollation; + private final CharacterSet overrideCharacterSet; + private final Boolean overrideIdentity; + private final Field overrideDefaultValue; DataTypeProxy(AbstractDataType type) { - this(type, null, null, null, null, null, null, null, null, null, null, null); + this(type, null, null, null, null, null, null, null, null, null, null, null, null); } + @SuppressWarnings("unchecked") private DataTypeProxy( AbstractDataType type, Integer overridePrecision, @@ -82,8 +86,9 @@ final class DataTypeProxy extends AbstractDataType { Integer overrideLength, Nullability overrideNullability, Boolean overrideReadonly, - Field overrideGeneratedAlwaysAs, + Generator overrideGeneratedAlwaysAs, GenerationOption overrideGenerationOption, + GenerationLocation overrideGenerationLocation, Collation overrideCollation, CharacterSet overrideCharacterSet, Boolean overrideIdentity, @@ -99,6 +104,7 @@ final class DataTypeProxy extends AbstractDataType { this.overrideReadonly = overrideReadonly; this.overrideGeneratedAlwaysAs = overrideGeneratedAlwaysAs; this.overrideGenerationOption = overrideGenerationOption; + this.overrideGenerationLocation = overrideGenerationLocation; this.overrideCollation = overrideCollation; this.overrideCharacterSet = overrideCharacterSet; this.overrideIdentity = overrideIdentity; @@ -159,6 +165,7 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, overrideGeneratedAlwaysAs, overrideGenerationOption, + overrideGenerationLocation, overrideCollation, overrideCharacterSet, overrideIdentity, @@ -182,6 +189,7 @@ final class DataTypeProxy extends AbstractDataType { r, overrideGeneratedAlwaysAs, overrideGenerationOption, + overrideGenerationLocation, overrideCollation, overrideCharacterSet, overrideIdentity, @@ -190,12 +198,12 @@ final class DataTypeProxy extends AbstractDataType { } @Override - public final Field generatedAlwaysAs() { - return defaultIfNull(overrideGeneratedAlwaysAs, type.generatedAlwaysAs()); + public final Generator generatedAlwaysAsGenerator() { + return defaultIfNull(overrideGeneratedAlwaysAs, type.generatedAlwaysAsGenerator()); } @Override - public final DataType generatedAlwaysAs(Field g) { + public final DataType generatedAlwaysAs(Generator g) { return new DataTypeProxy<>( this, overridePrecision, @@ -205,6 +213,7 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, g, overrideGenerationOption, + overrideGenerationLocation, overrideCollation, overrideCharacterSet, overrideIdentity, @@ -228,6 +237,31 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, overrideGeneratedAlwaysAs, g, + overrideGenerationLocation, + overrideCollation, + overrideCharacterSet, + overrideIdentity, + overrideDefaultValue + ); + } + + @Override + public final GenerationLocation generationLocation() { + return defaultIfNull(overrideGenerationLocation, type.generationLocation()); + } + + @Override + public final DataType generationLocation(GenerationLocation g) { + return new DataTypeProxy<>( + this, + overridePrecision, + overrideScale, + overrideLength, + overrideNullability, + overrideReadonly, + overrideGeneratedAlwaysAs, + overrideGenerationOption, + g, overrideCollation, overrideCharacterSet, overrideIdentity, @@ -251,6 +285,7 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, overrideGeneratedAlwaysAs, overrideGenerationOption, + overrideGenerationLocation, c, overrideCharacterSet, overrideIdentity, @@ -274,6 +309,7 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, overrideGeneratedAlwaysAs, overrideGenerationOption, + overrideGenerationLocation, overrideCollation, c, overrideIdentity, @@ -297,6 +333,7 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, overrideGeneratedAlwaysAs, overrideGenerationOption, + overrideGenerationLocation, overrideCollation, overrideCharacterSet, i, @@ -320,6 +357,7 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, overrideGeneratedAlwaysAs, overrideGenerationOption, + overrideGenerationLocation, overrideCollation, overrideCharacterSet, overrideIdentity, @@ -373,6 +411,7 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, overrideGeneratedAlwaysAs, overrideGenerationOption, + overrideGenerationLocation, overrideCollation, overrideCharacterSet, overrideIdentity, @@ -396,6 +435,7 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, overrideGeneratedAlwaysAs, overrideGenerationOption, + overrideGenerationLocation, overrideCollation, overrideCharacterSet, overrideIdentity, @@ -419,6 +459,7 @@ final class DataTypeProxy extends AbstractDataType { overrideReadonly, overrideGeneratedAlwaysAs, overrideGenerationOption, + overrideGenerationLocation, overrideCollation, overrideCharacterSet, overrideIdentity, diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java b/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java index e410495dee..8f8a3facba 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java @@ -94,6 +94,7 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.regex.Pattern; // ... @@ -105,6 +106,7 @@ import org.jooq.Converter; import org.jooq.DataType; import org.jooq.EnumType; import org.jooq.Field; +import org.jooq.Generator; import org.jooq.Name; import org.jooq.Nullability; import org.jooq.QualifiedRecord; @@ -112,6 +114,7 @@ import org.jooq.SQLDialect; import org.jooq.exception.MappingException; import org.jooq.exception.SQLDialectNotSupportedException; import org.jooq.impl.DefaultBinding.InternalBinding; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; import org.jooq.types.UByte; import org.jooq.types.UInteger; @@ -256,8 +259,9 @@ public class DefaultDataType extends AbstractDataTypeX { private final Nullability nullability; private final boolean readonly; - private final Field generatedAlwaysAs; + private final Generator generatedAlwaysAs; private final GenerationOption generationOption; + private final GenerationLocation generationLocation; private final Collation collation; private final CharacterSet characterSet; private final boolean identity; @@ -327,10 +331,10 @@ public class DefaultDataType extends AbstractDataTypeX { } DefaultDataType(SQLDialect dialect, DataType sqlDataType, Class type, Binding binding, Name qualifiedTypeName, String typeName, String castTypeName, Integer precision, Integer scale, Integer length, Nullability nullability, Field defaultValue) { - this(dialect, sqlDataType, type, binding, qualifiedTypeName, typeName, castTypeName, precision, scale, length, nullability, false, null, GenerationOption.DEFAULT, null, null, false, defaultValue); + this(dialect, sqlDataType, type, binding, qualifiedTypeName, typeName, castTypeName, precision, scale, length, nullability, false, null, GenerationOption.DEFAULT, GenerationLocation.SERVER, null, null, false, defaultValue); } - DefaultDataType(SQLDialect dialect, DataType sqlDataType, Class type, Binding binding, Name qualifiedTypeName, String typeName, String castTypeName, Integer precision, Integer scale, Integer length, Nullability nullability, boolean readonly, Field generatedAlwaysAs, GenerationOption generationOption, Collation collation, CharacterSet characterSet, boolean identity, Field defaultValue) { + DefaultDataType(SQLDialect dialect, DataType sqlDataType, Class type, Binding binding, Name qualifiedTypeName, String typeName, String castTypeName, Integer precision, Integer scale, Integer length, Nullability nullability, boolean readonly, Generator generatedAlwaysAs, GenerationOption generationOption, GenerationLocation generationLocation, Collation collation, CharacterSet characterSet, boolean identity, Field defaultValue) { super(qualifiedTypeName, NO_COMMENT); // Initialise final instance members @@ -352,6 +356,7 @@ public class DefaultDataType extends AbstractDataTypeX { this.readonly = readonly; this.generatedAlwaysAs = generatedAlwaysAs; this.generationOption = generationOption == null ? GenerationOption.DEFAULT : generationOption; + this.generationLocation = generationLocation == null ? GenerationLocation.SERVER : generationLocation; this.collation = collation; this.characterSet = characterSet; this.identity = identity; @@ -396,8 +401,9 @@ public class DefaultDataType extends AbstractDataTypeX { Integer newLength, Nullability newNullability, boolean newReadonly, - Field newGeneratedAlwaysAs, + Generator newGeneratedAlwaysAs, GenerationOption newGenerationOption, + GenerationLocation newGenerationLocation, Collation newCollation, CharacterSet newCharacterSet, boolean newIdentity, @@ -412,6 +418,7 @@ public class DefaultDataType extends AbstractDataTypeX { newReadonly, newGeneratedAlwaysAs, newGenerationOption, + newGenerationLocation, newCollation, newCharacterSet, newIdentity, @@ -429,8 +436,9 @@ public class DefaultDataType extends AbstractDataTypeX { Integer length, Nullability nullability, boolean readonly, - Field generatedAlwaysAs, + Generator generatedAlwaysAs, GenerationOption generationOption, + GenerationLocation generationLocation, Collation collation, CharacterSet characterSet, boolean identity, @@ -451,6 +459,7 @@ public class DefaultDataType extends AbstractDataTypeX { this.readonly = readonly; this.generatedAlwaysAs = generatedAlwaysAs; this.generationOption = generationOption; + this.generationLocation = generationLocation; this.collation = collation; this.characterSet = characterSet; this.identity = identity; @@ -491,7 +500,7 @@ public class DefaultDataType extends AbstractDataTypeX { } @Override - public final Field generatedAlwaysAs() { + public final Generator generatedAlwaysAsGenerator() { return generatedAlwaysAs; } @@ -500,6 +509,11 @@ public class DefaultDataType extends AbstractDataTypeX { return generationOption; } + @Override + public final GenerationLocation generationLocation() { + return generationLocation; + } + @Override public final Collation collation() { return collation; @@ -556,7 +570,7 @@ public class DefaultDataType extends AbstractDataTypeX { // ... and then, set them back to the original value // [#2710] TODO: Remove this logic along with cached data types - return dataType.construct(precision, scale, length, nullability, readonly, generatedAlwaysAs, generationOption, collation, characterSet, identity, defaultValue); + return dataType.construct(precision, scale, length, nullability, readonly, generatedAlwaysAs, generationOption, generationLocation, collation, characterSet, identity, defaultValue); } // If this is already the dialect's specific data type, return this diff --git a/jOOQ/src/main/java/org/jooq/impl/DomainDataType.java b/jOOQ/src/main/java/org/jooq/impl/DomainDataType.java index 2e83ed1231..f594ca4f13 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DomainDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/DomainDataType.java @@ -65,8 +65,9 @@ final class DomainDataType extends DefaultDataType { baseType.lengthDefined() ? baseType.length() : null, baseType.nullability(), baseType.readonly(), - baseType.generatedAlwaysAs(), + baseType.generatedAlwaysAsGenerator(), baseType.generationOption(), + baseType.generationLocation(), null, // TODO: Collation null, // TODO: CharacterSet (?) false, diff --git a/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java b/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java index 1c7a376616..8823d6089d 100644 --- a/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java +++ b/jOOQ/src/main/java/org/jooq/impl/FieldMapsForInsert.java @@ -39,6 +39,7 @@ package org.jooq.impl; import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static org.jooq.Clause.FIELD_ROW; import static org.jooq.Clause.INSERT_SELECT; import static org.jooq.Clause.INSERT_VALUES; @@ -49,9 +50,11 @@ import static org.jooq.SQLDialect.YUGABYTEDB; import static org.jooq.conf.WriteIfReadonly.IGNORE; import static org.jooq.conf.WriteIfReadonly.THROW; import static org.jooq.impl.DSL.name; +import static org.jooq.impl.DSL.select; import static org.jooq.impl.Keywords.K_DEFAULT_VALUES; import static org.jooq.impl.Keywords.K_VALUES; import static org.jooq.impl.QueryPartCollectionView.wrap; +import static org.jooq.impl.Tools.EMPTY_FIELD; import static org.jooq.impl.Tools.anyMatch; import static org.jooq.impl.Tools.collect; import static org.jooq.impl.Tools.filter; @@ -64,10 +67,12 @@ import java.util.AbstractList; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -84,12 +89,15 @@ import org.jooq.Record; import org.jooq.RenderContext.CastMode; import org.jooq.SQLDialect; import org.jooq.Select; +import org.jooq.SelectJoinStep; import org.jooq.Table; import org.jooq.conf.WriteIfReadonly; import org.jooq.exception.DataTypeException; import org.jooq.impl.AbstractStoreQuery.UnknownField; import org.jooq.impl.QOM.UNotYetImplemented; +import org.jetbrains.annotations.NotNull; + /** * @author Lukas Eder */ @@ -124,6 +132,12 @@ final class FieldMapsForInsert extends AbstractQueryPart implements UNotYetImple .end(INSERT_VALUES); } + + + + + + // Single record inserts can use the standard syntax in any dialect @@ -171,13 +185,6 @@ final class FieldMapsForInsert extends AbstractQueryPart implements UNotYetImple - - - - - - - @@ -187,11 +194,7 @@ final class FieldMapsForInsert extends AbstractQueryPart implements UNotYetImple case FIREBIRD: { - ctx.formatSeparator() - .start(INSERT_SELECT) - .visit(insertSelect(ctx)) - .end(INSERT_SELECT); - + toSQLInsertSelect(ctx, insertSelect(ctx)); break; } @@ -209,6 +212,38 @@ final class FieldMapsForInsert extends AbstractQueryPart implements UNotYetImple } } + static final void toSQLInsertSelect(Context ctx, Select select) { + ctx.formatSeparator() + .start(INSERT_SELECT) + .visit(select) + .end(INSERT_SELECT); + } + + static final Set> keysAndComputedOnClient(Set> keys, Table table) { + + + + + + + + + return keys; + } + + + + + + + + + + + + + + @@ -521,15 +556,15 @@ final class FieldMapsForInsert extends AbstractQueryPart implements UNotYetImple return rows > 0; } - final List> toSQLReferenceKeys(Context ctx) { + final Set> toSQLReferenceKeys(Context ctx) { // [#1506] with DEFAULT VALUES, we might not have any columns to render if (!isExecutable()) - return emptyList(); + return emptySet(); // [#2995] Do not generate empty column lists. if (values.isEmpty()) - return emptyList(); + return emptySet(); // [#4629] Do not generate column lists for unknown columns unknownFields: { @@ -537,11 +572,11 @@ final class FieldMapsForInsert extends AbstractQueryPart implements UNotYetImple if (!(field instanceof UnknownField)) break unknownFields; - return emptyList(); + return emptySet(); } // [#989] Avoid qualifying fields in INSERT field declaration - List> fields = collect(removeReadonly(ctx, flattenCollection(values.keySet(), true, true), e -> e)); + Set> fields = keysAndComputedOnClient(keysFlattened(ctx), table); if (!fields.isEmpty()) ctx.sql(" (").visit(wrap(fields).qualify(false)).sql(')'); diff --git a/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java index a7de0c3be6..349ed7d119 100644 --- a/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java @@ -67,6 +67,7 @@ import static org.jooq.impl.DSL.row; 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.FieldMapsForInsert.toSQLInsertSelect; import static org.jooq.impl.Keywords.K_DEFAULT; import static org.jooq.impl.Keywords.K_DEFAULT_VALUES; import static org.jooq.impl.Keywords.K_DO_NOTHING; @@ -654,7 +655,7 @@ final class InsertQueryImpl extends AbstractStoreQuery impl .sql(' ') .declareTables(true, c -> c.visit(table(c))); - List> fields = insertMaps.toSQLReferenceKeys(ctx); + Set> fields = insertMaps.toSQLReferenceKeys(ctx); ctx.end(INSERT_INSERT_INTO); @@ -691,11 +692,7 @@ final class InsertQueryImpl extends AbstractStoreQuery impl // [#8353] TODO: Support overlapping embeddables - ctx.formatSeparator() - .start(INSERT_SELECT) - .visit(s) - .end(INSERT_SELECT); - + toSQLInsertSelect(ctx, s); ctx.data().remove(DATA_INSERT_SELECT_WITHOUT_INSERT_COLUMN_LIST); ctx.data().remove(DATA_INSERT_SELECT); } diff --git a/jOOQ/src/main/java/org/jooq/impl/MultisetDataType.java b/jOOQ/src/main/java/org/jooq/impl/MultisetDataType.java index c6e60e13a1..3016177b55 100644 --- a/jOOQ/src/main/java/org/jooq/impl/MultisetDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/MultisetDataType.java @@ -49,10 +49,12 @@ import java.util.Map.Entry; import org.jooq.CharacterSet; import org.jooq.Collation; import org.jooq.Field; +import org.jooq.Generator; import org.jooq.Nullability; import org.jooq.Record; import org.jooq.Result; import org.jooq.Row; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; /** @@ -86,14 +88,15 @@ final class MultisetDataType extends DefaultDataType Integer length, Nullability nullability, boolean readonly, - Field> generatedAlwaysAs, + Generator> generatedAlwaysAs, GenerationOption generationOption, + GenerationLocation generationLocation, Collation collation, CharacterSet characterSet, boolean identity, Field> defaultValue ) { - super(t, precision, scale, length, nullability, readonly, generatedAlwaysAs, generationOption, collation, characterSet, identity, defaultValue); + super(t, precision, scale, length, nullability, readonly, generatedAlwaysAs, generationOption, generationLocation, collation, characterSet, identity, defaultValue); this.row = row; this.recordType = recordType; @@ -107,8 +110,9 @@ final class MultisetDataType extends DefaultDataType Integer newLength, Nullability newNullability, boolean newReadonly, - Field> newGeneratedAlwaysAs, + Generator> newGeneratedAlwaysAs, GenerationOption newGenerationOption, + GenerationLocation newGenerationLocation, Collation newCollation, CharacterSet newCharacterSet, boolean newIdentity, @@ -125,6 +129,7 @@ final class MultisetDataType extends DefaultDataType newReadonly, newGeneratedAlwaysAs, newGenerationOption, + newGenerationLocation, newCollation, newCharacterSet, newIdentity, diff --git a/jOOQ/src/main/java/org/jooq/impl/QOM.java b/jOOQ/src/main/java/org/jooq/impl/QOM.java index d6e94182d3..33f739c3c9 100644 --- a/jOOQ/src/main/java/org/jooq/impl/QOM.java +++ b/jOOQ/src/main/java/org/jooq/impl/QOM.java @@ -5815,6 +5815,24 @@ public final class QOM { } } + /** + * The GenerationLocation type. + *

+ * Specify where a computed column should be computed, i.e. in the client or on the + * server. + */ + public enum GenerationLocation { + CLIENT(keyword("client")), + SERVER(keyword("server")), + ; + + final Keyword keyword; + + private GenerationLocation(Keyword keyword) { + this.keyword = keyword; + } + } + /** * The GenerationOption type. *

diff --git a/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java b/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java index bde2d37235..0328d74a0b 100644 --- a/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java @@ -40,7 +40,6 @@ package org.jooq.impl; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; import static org.jooq.impl.Tools.CONFIG; -import static org.jooq.impl.Tools.CTX; import static org.jooq.impl.Tools.newRecord; import static org.jooq.impl.Tools.recordType; @@ -51,9 +50,11 @@ import java.util.Map.Entry; import org.jooq.CharacterSet; import org.jooq.Collation; import org.jooq.Field; +import org.jooq.Generator; import org.jooq.Nullability; import org.jooq.Record; import org.jooq.Row; +import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; /** @@ -88,14 +89,15 @@ final class RecordDataType extends DefaultDataType { Integer length, Nullability nullability, boolean readonly, - Field generatedAlwaysAs, + Generator generatedAlwaysAs, GenerationOption generationOption, + GenerationLocation generationLocation, Collation collation, CharacterSet characterSet, boolean identity, Field defaultValue ) { - super(t, precision, scale, length, nullability, readonly, generatedAlwaysAs, generationOption, collation, characterSet, identity, defaultValue); + super(t, precision, scale, length, nullability, readonly, generatedAlwaysAs, generationOption, generationLocation, collation, characterSet, identity, defaultValue); this.row = row; } @@ -108,8 +110,9 @@ final class RecordDataType extends DefaultDataType { Integer newLength, Nullability newNullability, boolean newReadonly, - Field newGeneratedAlwaysAs, + Generator newGeneratedAlwaysAs, GenerationOption newGenerationOption, + GenerationLocation newGenerationLocation, Collation newCollation, CharacterSet newCharacterSet, boolean newIdentity, @@ -125,6 +128,7 @@ final class RecordDataType extends DefaultDataType { newReadonly, newGeneratedAlwaysAs, newGenerationOption, + newGenerationLocation, newCollation, newCharacterSet, newIdentity, diff --git a/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java index 42ddb91d00..dcc5c39b31 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java @@ -83,6 +83,7 @@ import static org.jooq.impl.DSL.row; import static org.jooq.impl.DSL.select; import static org.jooq.impl.DSL.trueCondition; import static org.jooq.impl.FieldMapForUpdate.removeReadonly; +import static org.jooq.impl.FieldMapsForInsert.keysAndComputedOnClient; import static org.jooq.impl.Keywords.K_FROM; import static org.jooq.impl.Keywords.K_LIMIT; import static org.jooq.impl.Keywords.K_ORDER_BY; @@ -90,6 +91,7 @@ import static org.jooq.impl.Keywords.K_ROW; import static org.jooq.impl.Keywords.K_SET; import static org.jooq.impl.Keywords.K_UPDATE; import static org.jooq.impl.Keywords.K_WHERE; +import static org.jooq.impl.Tools.EMPTY_FIELD; import static org.jooq.impl.Tools.fieldName; import static org.jooq.impl.Tools.map; import static org.jooq.impl.Tools.unqualified; @@ -97,9 +99,11 @@ import static org.jooq.impl.Tools.visitSubquery; import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import org.jooq.Clause; import org.jooq.Condition; @@ -180,6 +184,8 @@ final class UpdateQueryImpl extends AbstractStoreQuery impl + + private static final Set SUPPORT_RVE_SET = SQLDialect.supportedBy(H2, HSQLDB, POSTGRES, YUGABYTEDB); private static final Set REQUIRE_RVE_ROW_CLAUSE = SQLDialect.supportedBy(POSTGRES, YUGABYTEDB); @@ -539,13 +545,54 @@ final class UpdateQueryImpl extends AbstractStoreQuery impl return from; } - @SuppressWarnings({ "unchecked", "rawtypes" }) @Override final void accept0(Context ctx) { + + + + + + + + + + + + + + + + + + + + accept1(ctx); + } + + private final UpdateQueryImpl copy(Consumer> consumer) { + UpdateQueryImpl u = new UpdateQueryImpl<>(configuration(), with, table); + + if (!returning.isEmpty()) + u.setReturning(returning); + + u.updateMap.putAll(updateMap); + u.multiRow = multiRow; + u.multiSelect = multiSelect; + u.multiValue = multiValue; + u.from.addAll(from); + u.condition.setWhere(condition.getWhere()); + u.orderBy.addAll(orderBy); + u.limit = limit; + consumer.accept(u); + return u; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + final void accept1(Context ctx) { boolean declareTables = ctx.declareTables(); ctx.start(UPDATE_UPDATE) .visit(K_UPDATE) @@ -589,10 +636,7 @@ final class UpdateQueryImpl extends AbstractStoreQuery impl map.put(k, Tools.field(v, k)); } - ctx.formatIndentStart() - .formatSeparator() - .visit(map) - .formatIndentEnd(); + toSQLUpdateMap(ctx, map); } @@ -609,9 +653,6 @@ final class UpdateQueryImpl extends AbstractStoreQuery impl - - - else { Row row = removeReadonly(ctx, multiRow); @@ -658,12 +699,8 @@ final class UpdateQueryImpl extends AbstractStoreQuery impl } // A regular (non-multi-row) update was specified - else { - ctx.formatIndentStart() - .formatSeparator() - .visit(updateMap) - .formatIndentEnd(); - } + else + toSQLUpdateMap(ctx, updateMap); ctx.end(UPDATE_SET); @@ -671,19 +708,9 @@ final class UpdateQueryImpl extends AbstractStoreQuery impl - switch (ctx.family()) { - - - - - - - default: - acceptFrom(ctx); - break; - } + acceptFrom(ctx); if (limit != null && NO_SUPPORT_LIMIT.contains(ctx.dialect()) || !orderBy.isEmpty() && NO_SUPPORT_ORDER_BY_LIMIT.contains(ctx.dialect())) { Field[] keyFields = @@ -736,6 +763,13 @@ final class UpdateQueryImpl extends AbstractStoreQuery impl ctx.end(UPDATE_RETURNING); } + private static final void toSQLUpdateMap(Context ctx, FieldMapForUpdate updateMap) { + ctx.formatIndentStart() + .formatSeparator() + .visit(updateMap) + .formatIndentEnd(); + } +