diff --git a/jOOQ-codegen-gradle/src/main/java/org/jooq/codegen/gradle/MetaExtensions.java b/jOOQ-codegen-gradle/src/main/java/org/jooq/codegen/gradle/MetaExtensions.java index 8768b719e3..1998ef3ce5 100644 --- a/jOOQ-codegen-gradle/src/main/java/org/jooq/codegen/gradle/MetaExtensions.java +++ b/jOOQ-codegen-gradle/src/main/java/org/jooq/codegen/gradle/MetaExtensions.java @@ -621,6 +621,12 @@ public class MetaExtensions { setRecordClass(o); } + public void recordTypeClass(Action action) { + MatcherRuleExtension o = objects.newInstance(MatcherRuleExtension.class, objects); + action.execute(o); + setRecordTypeClass(o); + } + public void interfaceClass(Action action) { MatcherRuleExtension o = objects.newInstance(MatcherRuleExtension.class, objects); action.execute(o); diff --git a/jOOQ-meta-kotlin/src/main/kotlin/org/jooq/meta/kotlin/Extensions.kt b/jOOQ-meta-kotlin/src/main/kotlin/org/jooq/meta/kotlin/Extensions.kt index d53ed9db3a..03488cd2b6 100644 --- a/jOOQ-meta-kotlin/src/main/kotlin/org/jooq/meta/kotlin/Extensions.kt +++ b/jOOQ-meta-kotlin/src/main/kotlin/org/jooq/meta/kotlin/Extensions.kt @@ -456,6 +456,13 @@ fun MatchersUDTType.recordClass(block: MatcherRule.() -> Unit) { block(recordClass) } +fun MatchersUDTType.recordTypeClass(block: MatcherRule.() -> Unit) { + if (recordTypeClass == null) + recordTypeClass = MatcherRule() + + block(recordTypeClass) +} + fun MatchersUDTType.interfaceClass(block: MatcherRule.() -> Unit) { if (interfaceClass == null) interfaceClass = MatcherRule() diff --git a/jOOQ/src/main/java/org/jooq/InsertSetStep.java b/jOOQ/src/main/java/org/jooq/InsertSetStep.java index 4aa9a0589a..19c1ce41f4 100644 --- a/jOOQ/src/main/java/org/jooq/InsertSetStep.java +++ b/jOOQ/src/main/java/org/jooq/InsertSetStep.java @@ -37,6 +37,8 @@ */ package org.jooq; +import org.jooq.conf.Settings; + import org.jetbrains.annotations.*; // ... @@ -339,9 +341,11 @@ public interface InsertSetStep { * Set values in the INSERT statement. *

* This is the same as calling {@link #set(Map)} with the argument record - * treated as a Map<Field<?>, Object>, except that the - * {@link Record#touched()} flags are taken into consideration in order to - * update only touched values. + * treated as a Map<Field<?>, Object>, except that + * the {@link Record#touched()} flags (or {@link Record#modified()} flags, + * depending on the query's {@link Settings#getRecordDirtyTracking()} + * configuration) are taken into consideration in order to update only + * touched (or modified) values. * * @see #set(Map) */ diff --git a/jOOQ/src/main/java/org/jooq/MergeMatchedSetStep.java b/jOOQ/src/main/java/org/jooq/MergeMatchedSetStep.java index 3a9eb9c1a5..a95d5c0409 100644 --- a/jOOQ/src/main/java/org/jooq/MergeMatchedSetStep.java +++ b/jOOQ/src/main/java/org/jooq/MergeMatchedSetStep.java @@ -56,6 +56,8 @@ import static org.jooq.SQLDialect.POSTGRES; import java.util.Map; +import org.jooq.conf.Settings; + import org.jetbrains.annotations.NotNull; /** @@ -153,9 +155,11 @@ public interface MergeMatchedSetStep { * statement's WHEN MATCHED clause. *

* This is the same as calling {@link #set(Map)} with the argument record - * treated as a Map<Field<?>, Object>, except that the - * {@link Record#touched()} flags are taken into consideration in order to - * update only touched values. + * treated as a Map<Field<?>, Object>, except that + * the {@link Record#touched()} flags (or {@link Record#modified()} flags, + * depending on the query's {@link Settings#getRecordDirtyTracking()} + * configuration) are taken into consideration in order to update only + * touched (or modified) values. * * @see #set(Map) */ diff --git a/jOOQ/src/main/java/org/jooq/Record.java b/jOOQ/src/main/java/org/jooq/Record.java index 47af0159d6..9ba7a8f573 100644 --- a/jOOQ/src/main/java/org/jooq/Record.java +++ b/jOOQ/src/main/java/org/jooq/Record.java @@ -739,7 +739,9 @@ public interface Record extends Fields, Attachable, Comparable, Formatta * from the database. *

* When a record is {@link #modified()}, then it has always been - * {@link #touched()} as well. + * {@link #touched()} as well. Unlike the {@link #touched()} property, this + * property cannot be set and is derived only from the comparison between + * this record and the {@link #original()} record. * * @see #original() * @see #modified(Field) @@ -749,11 +751,13 @@ public interface Record extends Fields, Attachable, Comparable, Formatta boolean modified(); /** - * Check if a field's value has been modified since the record was created or - * fetched from the database, using {@link #field(Field)} for lookup. + * Check if a field's value has been modified since the record was created + * or fetched from the database, using {@link #field(Field)} for lookup. *

* When a record is {@link #modified()}, then it has always been - * {@link #touched()} as well. + * {@link #touched()} as well. Unlike the {@link #touched(Field)} property, + * this property cannot be set and is derived only from the comparison + * between #get(Field) and {@link #original(Field)} values. * * @see #modified() * @see #original(Field) @@ -761,11 +765,13 @@ public interface Record extends Fields, Attachable, Comparable, Formatta boolean modified(Field field); /** - * Check if a field's value has been modified since the record was created or - * fetched from the database, using {@link #field(int)} for lookup. + * Check if a field's value has been modified since the record was created + * or fetched from the database, using {@link #field(int)} for lookup. *

* When a record is {@link #modified()}, then it has always been - * {@link #touched()} as well. + * {@link #touched()} as well. Unlike the {@link #touched(int)} property, + * this property cannot be set and is derived only from the comparison + * between #get(int) and {@link #original(int)} values. * * @param fieldIndex The 0-based field index in this record. * @see #modified() @@ -774,11 +780,13 @@ public interface Record extends Fields, Attachable, Comparable, Formatta boolean modified(int fieldIndex); /** - * Check if a field's value has been modified since the record was created or - * fetched from the database, using {@link #field(String)} for lookup. + * Check if a field's value has been modified since the record was created + * or fetched from the database, using {@link #field(String)} for lookup. *

* When a record is {@link #modified()}, then it has always been - * {@link #touched()} as well. + * {@link #touched()} as well. Unlike the {@link #touched(String)} property, + * this property cannot be set and is derived only from the comparison + * between #get(String) and {@link #original(String)} values. * * @see #modified() * @see #original(String) @@ -790,7 +798,9 @@ public interface Record extends Fields, Attachable, Comparable, Formatta * fetched from the database, using {@link #field(Name)} for lookup. *

* When a record is {@link #modified()}, then it has always been - * {@link #touched()} as well. + * {@link #touched()} as well. Unlike the {@link #touched(Name)} property, + * this property cannot be set and is derived only from the comparison + * between #get(Name) and {@link #original(Name)} values. * * @see #modified() * @see #original(Name) diff --git a/jOOQ/src/main/java/org/jooq/TableRecord.java b/jOOQ/src/main/java/org/jooq/TableRecord.java index fffd91ed20..7af986e5cc 100644 --- a/jOOQ/src/main/java/org/jooq/TableRecord.java +++ b/jOOQ/src/main/java/org/jooq/TableRecord.java @@ -39,6 +39,7 @@ package org.jooq; import java.util.Collection; +import org.jooq.conf.RecordDirtyTracking; import org.jooq.conf.Settings; import org.jooq.exception.DataAccessException; @@ -70,7 +71,9 @@ public interface TableRecord> extends QualifiedRecord1 if the record was stored to the database. 0 * if storing was not necessary and diff --git a/jOOQ/src/main/java/org/jooq/UpdateSetStep.java b/jOOQ/src/main/java/org/jooq/UpdateSetStep.java index 5d2b6b4399..b4a49ed45d 100644 --- a/jOOQ/src/main/java/org/jooq/UpdateSetStep.java +++ b/jOOQ/src/main/java/org/jooq/UpdateSetStep.java @@ -37,6 +37,8 @@ */ package org.jooq; +import org.jooq.conf.Settings; + import org.jetbrains.annotations.*; @@ -128,9 +130,11 @@ public interface UpdateSetStep { * Set a value for a field in the UPDATE statement. *

* This is the same as calling {@link #set(Map)} with the argument record - * treated as a Map<Field<?>, Object>, except that the - * {@link Record#touched()} flags are taken into consideration in order to - * update only touched values. + * treated as a Map<Field<?>, Object>, except that + * the {@link Record#touched()} flags (or {@link Record#modified()} flags, + * depending on the query's {@link Settings#getRecordDirtyTracking()} + * configuration) are taken into consideration in order to update only + * touched (or modified) values. * * @see #set(Map) */ diff --git a/jOOQ/src/main/java/org/jooq/conf/RecordDirtyTracking.java b/jOOQ/src/main/java/org/jooq/conf/RecordDirtyTracking.java new file mode 100644 index 0000000000..83d5fc839b --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/conf/RecordDirtyTracking.java @@ -0,0 +1,37 @@ + +package org.jooq.conf; + +import jakarta.xml.bind.annotation.XmlEnum; +import jakarta.xml.bind.annotation.XmlType; + + +/** + *

Java class for RecordDirtyTracking. + * + *

The following schema fragment specifies the expected content contained within this class. + *

+ * <simpleType name="RecordDirtyTracking">
+ *   <restriction base="{http://www.w3.org/2001/XMLSchema}string">
+ *     <enumeration value="TOUCHED"/>
+ *     <enumeration value="MODIFIED"/>
+ *   </restriction>
+ * </simpleType>
+ * 
+ * + */ +@XmlType(name = "RecordDirtyTracking") +@XmlEnum +public enum RecordDirtyTracking { + + TOUCHED, + MODIFIED; + + public String value() { + return name(); + } + + public static RecordDirtyTracking fromValue(String v) { + return valueOf(v); + } + +} diff --git a/jOOQ/src/main/java/org/jooq/conf/Settings.java b/jOOQ/src/main/java/org/jooq/conf/Settings.java index d750061c8e..7ff4a73bbe 100644 --- a/jOOQ/src/main/java/org/jooq/conf/Settings.java +++ b/jOOQ/src/main/java/org/jooq/conf/Settings.java @@ -378,6 +378,9 @@ public class Settings @XmlElement(defaultValue = "NEVER") @XmlSchemaType(name = "string") protected UpdateUnchangedRecords updateUnchangedRecords = UpdateUnchangedRecords.NEVER; + @XmlElement(defaultValue = "TOUCHED") + @XmlSchemaType(name = "string") + protected RecordDirtyTracking recordDirtyTracking = RecordDirtyTracking.TOUCHED; @XmlElement(defaultValue = "false") protected Boolean updatablePrimaryKeys = false; @XmlElement(defaultValue = "true") @@ -5175,6 +5178,22 @@ public class Settings this.updateUnchangedRecords = value; } + /** + * Whether {@link org.jooq.UpdatableRecord#store()} and related calls should be based on {@link org.jooq.Record#touched()} or {@link org.jooq.Record#modified()} semantics. This also affects copying records into explicit statements. + * + */ + public RecordDirtyTracking getRecordDirtyTracking() { + return recordDirtyTracking; + } + + /** + * Whether {@link org.jooq.UpdatableRecord#store()} and related calls should be based on {@link org.jooq.Record#touched()} or {@link org.jooq.Record#modified()} semantics. This also affects copying records into explicit statements. + * + */ + public void setRecordDirtyTracking(RecordDirtyTracking value) { + this.recordDirtyTracking = value; + } + /** * Whether primary key values are deemed to be "updatable" in jOOQ. *

@@ -8865,6 +8884,15 @@ public class Settings return this; } + /** + * Whether {@link org.jooq.UpdatableRecord#store()} and related calls should be based on {@link org.jooq.Record#touched()} or {@link org.jooq.Record#modified()} semantics. This also affects copying records into explicit statements. + * + */ + public Settings withRecordDirtyTracking(RecordDirtyTracking value) { + setRecordDirtyTracking(value); + return this; + } + /** * Whether primary key values are deemed to be "updatable" in jOOQ. *

@@ -9810,6 +9838,7 @@ public class Settings builder.append("attachRecords", attachRecords); builder.append("insertUnchangedRecords", insertUnchangedRecords); builder.append("updateUnchangedRecords", updateUnchangedRecords); + builder.append("recordDirtyTracking", recordDirtyTracking); builder.append("updatablePrimaryKeys", updatablePrimaryKeys); builder.append("reflectionCaching", reflectionCaching); builder.append("cacheRecordMappers", cacheRecordMappers); @@ -11270,6 +11299,15 @@ public class Settings return false; } } + if (recordDirtyTracking == null) { + if (other.recordDirtyTracking!= null) { + return false; + } + } else { + if (!recordDirtyTracking.equals(other.recordDirtyTracking)) { + return false; + } + } if (updatablePrimaryKeys == null) { if (other.updatablePrimaryKeys!= null) { return false; @@ -12148,6 +12186,7 @@ public class Settings result = ((prime*result)+((attachRecords == null)? 0 :attachRecords.hashCode())); result = ((prime*result)+((insertUnchangedRecords == null)? 0 :insertUnchangedRecords.hashCode())); result = ((prime*result)+((updateUnchangedRecords == null)? 0 :updateUnchangedRecords.hashCode())); + result = ((prime*result)+((recordDirtyTracking == null)? 0 :recordDirtyTracking.hashCode())); result = ((prime*result)+((updatablePrimaryKeys == null)? 0 :updatablePrimaryKeys.hashCode())); result = ((prime*result)+((reflectionCaching == null)? 0 :reflectionCaching.hashCode())); result = ((prime*result)+((cacheRecordMappers == null)? 0 :cacheRecordMappers.hashCode())); diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractResult.java b/jOOQ/src/main/java/org/jooq/impl/AbstractResult.java index 7e091e9ec2..a64ff7c732 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractResult.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractResult.java @@ -47,6 +47,7 @@ import static org.jooq.conf.SettingsTools.renderLocale; import static org.jooq.impl.DSL.insertInto; import static org.jooq.impl.DSL.name; import static org.jooq.impl.DSL.table; +import static org.jooq.impl.Tools.recordDirtyTrackingPredicate; import static org.jooq.tools.StringUtils.abbreviate; import static org.jooq.tools.StringUtils.leftPad; import static org.jooq.tools.StringUtils.rightPad; @@ -163,6 +164,7 @@ abstract class AbstractResult extends AbstractFormattable impl int size = fields.size(); final int[] decimalPlaces = new int[size]; final int[] widths = new int[size]; + ObjIntPredicate dirty = recordDirtyTrackingPredicate(this); for (int index = 0; index < size; index++) { if (Number.class.isAssignableFrom(fields.field(index).getType())) { @@ -173,7 +175,7 @@ abstract class AbstractResult extends AbstractFormattable impl // Collect all decimal places for the column values for (R record : buffer) - decimalPlacesList.add(decimalPlaces(format0(record.get(index), record.touched(index), true))); + decimalPlacesList.add(decimalPlaces(format0(record.get(index), dirty.test(record, index), true))); // Find max decimalPlaces[index] = Collections.max(decimalPlacesList); @@ -197,7 +199,7 @@ abstract class AbstractResult extends AbstractFormattable impl // Add column values width for (R record : buffer) { - String value = format0(record.get(index), record.touched(index), true); + String value = format0(record.get(index), dirty.test(record, index), true); // Align number values before width is calculated if (isNumCol) @@ -283,7 +285,7 @@ abstract class AbstractResult extends AbstractFormattable impl StringUtils.replace( StringUtils.replace( StringUtils.replace( - format0(record.get(index), record.touched(index), true), "\n", "{lf}" + format0(record.get(index), dirty.test(record, index), true), "\n", "{lf}" ), "\r", "{cr}" ), "\t", "{tab}" ); diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractStoreQuery.java b/jOOQ/src/main/java/org/jooq/impl/AbstractStoreQuery.java index abf3f3d2a7..851de00563 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractStoreQuery.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractStoreQuery.java @@ -37,6 +37,8 @@ */ package org.jooq.impl; +import static org.jooq.impl.Tools.recordDirtyTrackingPredicate; + import java.util.Map; import org.jooq.Configuration; @@ -72,8 +74,10 @@ implements @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public final void setRecord(R record) { + ObjIntPredicate dirty = recordDirtyTrackingPredicate(this); + for (int i = 0; i < record.size(); i++) - if (record.touched(i)) + if (dirty.test(record, i)) addValue((Field) record.field(i), record.get(i)); } diff --git a/jOOQ/src/main/java/org/jooq/impl/InsertImpl.java b/jOOQ/src/main/java/org/jooq/impl/InsertImpl.java index 8eae146c2c..23a22dae04 100644 --- a/jOOQ/src/main/java/org/jooq/impl/InsertImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/InsertImpl.java @@ -1239,7 +1239,7 @@ final class InsertImpl dirty = recordDirtyTrackingPredicate(this); + for (int i = 0; i < record.size(); i++) - if (record.touched(i)) + if (dirty.test(record, i)) addValueForUpdate((Field) record.field(i), record.get(i)); } diff --git a/jOOQ/src/main/java/org/jooq/impl/MergeImpl.java b/jOOQ/src/main/java/org/jooq/impl/MergeImpl.java index 98189c6c94..17b059d366 100644 --- a/jOOQ/src/main/java/org/jooq/impl/MergeImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/MergeImpl.java @@ -1020,7 +1020,7 @@ implements @Override public final MergeImpl set(Record record) { - return set(Tools.mapOfTouchedValues(record)); + return set(Tools.mapOfTouchedValues(this, record)); } @Override diff --git a/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java b/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java index 0460727a1f..189699c28f 100644 --- a/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java @@ -65,6 +65,7 @@ import static org.jooq.impl.Tools.filter; import static org.jooq.impl.Tools.indexOrFail; import static org.jooq.impl.Tools.isEmpty; import static org.jooq.impl.Tools.let; +import static org.jooq.impl.Tools.recordDirtyTrackingPredicate; import static org.jooq.impl.Tools.settings; import static org.jooq.tools.StringUtils.defaultIfNull; @@ -308,9 +309,10 @@ implements final List> addTouchedValues(Field[] storeFields, StoreQuery query, boolean forUpdate) { FieldsImpl f = new FieldsImpl<>(storeFields); List> result = new ArrayList<>(); + ObjIntPredicate dirty = recordDirtyTrackingPredicate(query); for (Field field : fields.fields.fields) { - if (touched(field) && f.field(field) != null && writable(field, forUpdate)) { + if (dirty.test(this, indexOf(field)) && f.field(field) != null && writable(field, forUpdate)) { addValue(query, field, forUpdate); result.add(field); } diff --git a/jOOQ/src/main/java/org/jooq/impl/Tools.java b/jOOQ/src/main/java/org/jooq/impl/Tools.java index 06913706b9..db4c7c2fbd 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Tools.java +++ b/jOOQ/src/main/java/org/jooq/impl/Tools.java @@ -268,6 +268,7 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.IntFunction; +import java.util.function.IntPredicate; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.regex.MatchResult; @@ -354,6 +355,7 @@ import org.jooq.conf.BackslashEscaping; import org.jooq.conf.NestedCollectionEmulation; import org.jooq.conf.ParamType; import org.jooq.conf.ParseNameCase; +import org.jooq.conf.RecordDirtyTracking; import org.jooq.conf.RenderDefaultNullability; import org.jooq.conf.RenderMapping; import org.jooq.conf.RenderQuotedNames; @@ -2790,17 +2792,26 @@ final class Tools { /** * Turn a {@link Record} into a {@link Map} */ - static final Map, Object> mapOfTouchedValues(Record record) { + static final Map, Object> mapOfTouchedValues(Attachable attachable, Record record) { Map, Object> result = new LinkedHashMap<>(); int size = record.size(); + ObjIntPredicate dirty = recordDirtyTrackingPredicate(attachable); for (int i = 0; i < size; i++) - if (record.touched(i)) + if (dirty.test(record, i)) result.put(record.field(i), record.get(i)); return result; } + static final ObjIntPredicate recordDirtyTrackingPredicate(Attachable attachable) { + return RecordDirtyTracking.MODIFIED.equals(configuration(attachable).settings().getRecordDirtyTracking()) + ? Record::touched + : Record::modified; + } + + + /** * Extract the first item from an iterable or null, if there is * no such item, or if iterable itself is null diff --git a/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java b/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java index c2f70af4f5..d490847413 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java @@ -60,6 +60,7 @@ import static org.jooq.impl.RecordDelegate.RecordLifecycleType.STORE; import static org.jooq.impl.RecordDelegate.RecordLifecycleType.UPDATE; import static org.jooq.impl.Tools.EMPTY_FIELD; import static org.jooq.impl.Tools.EMPTY_TABLE_FIELD; +import static org.jooq.impl.Tools.recordDirtyTrackingPredicate; import static org.jooq.impl.Tools.settings; import java.math.BigInteger; @@ -204,10 +205,11 @@ public class UpdatableRecordImpl> extends TableReco executeUpdate = fetched; } else { + ObjIntPredicate dirty = recordDirtyTrackingPredicate(this); for (TableField field : keys) { // If any primary key value is null or touched - if (touched(field) || + if (dirty.test(this, indexOf(field)) || // [#3237] or if a NOT NULL primary key value is null, then execute an INSERT (field.getDataType().nullable() == false && get(field) == null)) { diff --git a/jOOQ/src/main/java/org/jooq/impl/UpdateImpl.java b/jOOQ/src/main/java/org/jooq/impl/UpdateImpl.java index 15f2acee62..643e00d461 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UpdateImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/UpdateImpl.java @@ -178,7 +178,7 @@ implements @Override public final UpdateImpl set(Record record) { - return set(Tools.mapOfTouchedValues(record)); + return set(Tools.mapOfTouchedValues(this, record)); } diff --git a/jOOQ/src/main/resources/org/jooq/xsd/jooq-runtime-3.20.0.xsd b/jOOQ/src/main/resources/org/jooq/xsd/jooq-runtime-3.20.0.xsd index b56bb435b8..909fb7af3d 100644 --- a/jOOQ/src/main/resources/org/jooq/xsd/jooq-runtime-3.20.0.xsd +++ b/jOOQ/src/main/resources/org/jooq/xsd/jooq-runtime-3.20.0.xsd @@ -1328,6 +1328,10 @@ This flag has no effect when "executeWithOptimisticLocking" is turned off.]]> UPDATE part of {@link org.jooq.UpdatableRecord#store()} and {@link org.jooq.UpdatableRecord#merge()} calls.]]> + + + + + + + + + + + + + + +