diff --git a/builds/install/misc/firebird.conf b/builds/install/misc/firebird.conf index b9813c36ece..c351e34fcc5 100644 --- a/builds/install/misc/firebird.conf +++ b/builds/install/misc/firebird.conf @@ -523,6 +523,23 @@ #SubQueryConversion = false +# ---------------------------- +# Defines how UPDATE should handle the case when a record was updated by trigger. +# +# It is possible that BEFORE or AFTER UPDATE trigger was fired previously by the +# current UPDATE statement for the other record and has already changed the +# current record being updated. Due to cursor stability, UPDATE should not see +# such changes, and could silently overwrite them. +# +# If set to 0 (default), trigger changes will be overwritten by the UPDATE. +# If set to 1, error "UPDATE will overwrite changes made by trigger" will be raised. +# +# Per-database configurable. +# +# Type: integer +# +#UpdateOverwriteMode = 0 + # ============================ # Plugin settings # ============================ diff --git a/src/common/config/config.cpp b/src/common/config/config.cpp index 9c7e0e158c4..480ad49facd 100644 --- a/src/common/config/config.cpp +++ b/src/common/config/config.cpp @@ -434,6 +434,9 @@ void Config::checkValues() checkIntForLoBound(KEY_PARALLEL_WORKERS, 1, true); checkIntForHiBound(KEY_PARALLEL_WORKERS, values[KEY_MAX_PARALLEL_WORKERS].intVal, false); + + checkIntForLoBound(KEY_UPDATE_OVERWRITE_MODE, 0, true); + checkIntForHiBound(KEY_UPDATE_OVERWRITE_MODE, 1, false); } diff --git a/src/common/config/config.h b/src/common/config/config.h index b41fb02bd55..76ddf460c0d 100644 --- a/src/common/config/config.h +++ b/src/common/config/config.h @@ -194,6 +194,7 @@ enum ConfigKey KEY_OPTIMIZE_FOR_FIRST_ROWS, KEY_OUTER_JOIN_CONVERSION, KEY_SUBQUERY_CONVERSION, + KEY_UPDATE_OVERWRITE_MODE, MAX_CONFIG_KEY // keep it last }; @@ -314,7 +315,8 @@ constexpr ConfigEntry entries[MAX_CONFIG_KEY] = {TYPE_INTEGER, "MaxParallelWorkers", true, 1}, {TYPE_BOOLEAN, "OptimizeForFirstRows", false, false}, {TYPE_BOOLEAN, "OuterJoinConversion", false, true}, - {TYPE_BOOLEAN, "SubQueryConversion", false, false} + {TYPE_BOOLEAN, "SubQueryConversion", false, false}, + {TYPE_INTEGER, "UpdateOverwriteMode", false, 0} }; @@ -652,6 +654,8 @@ class Config : public RefCounted, public GlobalStorage CONFIG_GET_PER_DB_BOOL(getOuterJoinConversion, KEY_OUTER_JOIN_CONVERSION); CONFIG_GET_PER_DB_BOOL(getSubQueryConversion, KEY_SUBQUERY_CONVERSION); + + CONFIG_GET_PER_DB_INT(getUpdateOverwriteMode, KEY_UPDATE_OVERWRITE_MODE); }; // Implementation of interface to access master configuration file diff --git a/src/jrd/vio.cpp b/src/jrd/vio.cpp index 807dd8a84cb..2de1fc6eeb3 100644 --- a/src/jrd/vio.cpp +++ b/src/jrd/vio.cpp @@ -6617,31 +6617,31 @@ static void refresh_fk_fields(thread_db* tdbb, Record* old_rec, record_param* cu * Functional description * Update new_rpb with foreign key fields values changed by cascade triggers. * Consider self-referenced foreign keys only. + * Also, if UpdateOverwriteMode is set to 1, raise error when foreign key fields + * were changed by user triggers. * * old_rec - old record before modify - * cur_rpb - just read record with possibly changed FK fields + * cur_rpb - just read record with possibly changed fields * new_rpb - new record evaluated by modify statement and before-triggers * **************************************/ + const Database* dbb = tdbb->getDatabase(); + const auto overwriteMode = dbb->dbb_config->getUpdateOverwriteMode(); + jrd_rel* relation = cur_rpb->rpb_relation; MET_scan_partners(tdbb, relation); - if (!(relation->rel_foreign_refs.frgn_relations)) - return; - - const FB_SIZE_T frgnCount = relation->rel_foreign_refs.frgn_relations->count(); - if (!frgnCount) - return; + const FB_SIZE_T frgnCount = relation->rel_foreign_refs.frgn_relations ? + relation->rel_foreign_refs.frgn_relations->count() : 0; RelationPages* relPages = cur_rpb->rpb_relation->getPages(tdbb); - // Collect all fields of all foreign keys + // Collect all fields of self-referenced foreign keys SortedArray > fields; for (FB_SIZE_T i = 0; i < frgnCount; i++) { - // We need self-referenced FK's only if ((*relation->rel_foreign_refs.frgn_relations)[i] == relation->rel_id) { index_desc idx; @@ -6663,26 +6663,63 @@ static void refresh_fk_fields(thread_db* tdbb, Record* old_rec, record_param* cu } if (fields.isEmpty()) - return; + { + if (overwriteMode == 0) + return; - DSC desc1, desc2; - for (FB_SIZE_T idx = 0; idx < fields.getCount(); idx++) + if (cur_rpb->rpb_record->getFormat() == old_rec->getFormat()) + { + if (memcmp(cur_rpb->rpb_address, old_rec->getData(), cur_rpb->rpb_length) == 0) + return; + + fb_assert(overwriteMode == 1); + ERR_post(Arg::Gds(isc_random) << "UPDATE will overwrite changes made by trigger"); + } + // Else compare field-by-field + } + + for (FB_SIZE_T fld = 0, frn = 0; fld < relation->rel_current_format->fmt_count; fld++) { - // Detect if user changed FK field by himself. - const int fld = fields[idx]; - const bool flag_old = EVL_field(relation, old_rec, fld, &desc1); - const bool flag_new = EVL_field(relation, new_rpb->rpb_record, fld, &desc2); + dsc dsc_old; + const bool flag_old = EVL_field(relation, old_rec, fld, &dsc_old); - // If field was not changed by user - pick up possible modification by - // system cascade trigger - if (flag_old == flag_new && - (!flag_old || (flag_old && !MOV_compare(tdbb, &desc1, &desc2)))) + const bool is_fk = (frn < fields.getCount() && fields[frn] == fld); + if (!is_fk) { - const bool flag_tmp = EVL_field(relation, cur_rpb->rpb_record, fld, &desc1); - if (flag_tmp) - MOV_move(tdbb, &desc1, &desc2); - else - new_rpb->rpb_record->setNull(fld); + if (overwriteMode == 0) + continue; + + dsc dsc_cur; + const bool flag_cur = EVL_field(relation, cur_rpb->rpb_record, fld, &dsc_cur); + + // Check if current record differs from old record + if ((flag_cur != flag_old) || + (flag_cur && flag_old && MOV_compare(tdbb, &dsc_old, &dsc_cur) != 0)) + { + // Record was modified by trigger. + fb_assert(overwriteMode == 1); + ERR_post(Arg::Gds(isc_random) << "UPDATE will overwrite changes made by trigger"); + } + } + else + { + dsc dsc_new; + const bool flag_new = EVL_field(relation, new_rpb->rpb_record, fld, &dsc_new); + + // If field was not changed by user - pick up possible modification by + // system cascade trigger + if (flag_old == flag_new && + (!flag_old || (flag_old && !MOV_compare(tdbb, &dsc_old, &dsc_new)))) + { + dsc dsc_cur; + const bool flag_cur = EVL_field(relation, cur_rpb->rpb_record, fld, &dsc_cur); + if (flag_cur) + MOV_move(tdbb, &dsc_cur, &dsc_new); + else + new_rpb->rpb_record->setNull(fld); + } + + frn++; } } }