diff --git a/src/Batch.php b/src/Batch.php index cd1115e..6512e3b 100644 --- a/src/Batch.php +++ b/src/Batch.php @@ -57,6 +57,7 @@ public function update(Model $table, array $values, string|null $index = null, b { $final = []; $ids = []; + $timestampConditions = []; // Track conditions for timestamp updates if (!count($values)) { return false; @@ -67,20 +68,23 @@ public function update(Model $table, array $values, string|null $index = null, b } $driver = $table->getConnection()->getDriverName(); + $updatedAtColumn = null; + $timestampValue = null; + + if ($table->usesTimestamps()) { + $updatedAtColumn = $table->getUpdatedAtColumn(); + $timestampValue = Carbon::now()->format($table->getDateFormat()); + } foreach ($values as $key => $val) { $ids[] = $val[$index]; - - if ($table->usesTimestamps()) { - $updatedAtColumn = $table->getUpdatedAtColumn(); - - if (!isset($val[$updatedAtColumn])) { - $val[$updatedAtColumn] = Carbon::now()->format($table->getDateFormat()); - } - } + $hasChanges = false; + $changeConditions = []; foreach (array_keys($val) as $field) { - if ($field !== $index) { + if ($field !== $index && $field !== $updatedAtColumn) { + $hasChanges = true; + // If increment / decrement if (gettype($val[$field]) == 'array') { // If array has two values @@ -96,15 +100,33 @@ public function update(Model $table, array $values, string|null $index = null, b throw new \TypeError('Second value in Increment/Decrement array needs to be numeric'); } // Increment / decrement + // For increment/decrement, always consider it a change if (Common::disableBacktick($driver)) { $value = $field . $val[$field][0] . $val[$field][1]; + $changeConditions[] = '(' . $field . ' IS NOT NULL)'; } else { $value = '`' . $field . '`' . $val[$field][0] . $val[$field][1]; + $changeConditions[] = '(`' . $field . '` IS NOT NULL)'; } } else { // Only update $finalField = $raw ? Common::mysql_escape($val[$field]) : "'" . Common::mysql_escape($val[$field]) . "'"; $value = (is_null($val[$field]) ? 'NULL' : $finalField); + + // Build condition to check if value actually changes + if (is_null($val[$field])) { + if (Common::disableBacktick($driver)) { + $changeConditions[] = '(' . $field . ' IS NOT NULL)'; + } else { + $changeConditions[] = '(`' . $field . '` IS NOT NULL)'; + } + } else { + if (Common::disableBacktick($driver)) { + $changeConditions[] = '(' . $field . ' != ' . $finalField . ')'; + } else { + $changeConditions[] = '(`' . $field . '` != ' . $finalField . ')'; + } + } } if (Common::disableBacktick($driver)) @@ -113,6 +135,34 @@ public function update(Model $table, array $values, string|null $index = null, b $final[$field][] = 'WHEN `' . $index . '` = \'' . $val[$index] . '\' THEN ' . $value . ' '; } } + + // Handle explicit updated_at in values + if (isset($val[$updatedAtColumn])) { + $timestampFieldValue = $raw ? Common::mysql_escape($val[$updatedAtColumn]) : "'" . Common::mysql_escape($val[$updatedAtColumn]) . "'"; + $timestampFieldValue = (is_null($val[$updatedAtColumn]) ? 'NULL' : $timestampFieldValue); + + if (Common::disableBacktick($driver)) + $final[$updatedAtColumn][] = 'WHEN ' . $index . ' = \'' . $val[$index] . '\' THEN ' . $timestampFieldValue . ' '; + else + $final[$updatedAtColumn][] = 'WHEN `' . $index . '` = \'' . $val[$index] . '\' THEN ' . $timestampFieldValue . ' '; + } elseif ($hasChanges && $updatedAtColumn && count($changeConditions) > 0) { + // Only add timestamp update if there are actual field changes + $indexCondition = Common::disableBacktick($driver) ? + $index . ' = \'' . $val[$index] . '\'' : + '`' . $index . '` = \'' . $val[$index] . '\''; + + $combinedCondition = $indexCondition . ' AND (' . implode(' OR ', $changeConditions) . ')'; + + if (Common::disableBacktick($driver)) + $timestampConditions[] = 'WHEN ' . $combinedCondition . ' THEN \'' . $timestampValue . '\' '; + else + $timestampConditions[] = 'WHEN ' . $combinedCondition . ' THEN \'' . $timestampValue . '\' '; + } + } + + // Add timestamp conditions to final array if we have any + if (!empty($timestampConditions) && $updatedAtColumn) { + $final[$updatedAtColumn] = $timestampConditions; } if (Common::disableBacktick($driver)) { @@ -174,8 +224,9 @@ public function updateWithTwoIndex(Model $table, array $values, string|null $ind { $final = []; $ids = []; - $driver = $table->getConnection()->getDriverName(); - + $ids2 = []; + $timestampConditions = []; // Track conditions for timestamp updates + if (!count($values)) { return false; } @@ -184,13 +235,68 @@ public function updateWithTwoIndex(Model $table, array $values, string|null $ind $index = $table->getKeyName(); } + $driver = $table->getConnection()->getDriverName(); + $updatedAtColumn = null; + $timestampValue = null; + + if ($table->usesTimestamps()) { + $updatedAtColumn = $table->getUpdatedAtColumn(); + $timestampValue = Carbon::now()->format($table->getDateFormat()); + } + foreach ($values as $key => $val) { $ids[] = $val[$index]; $ids2[] = $val[$index2]; + $hasChanges = false; + $changeConditions = []; + foreach (array_keys($val) as $field) { - if ($field !== $index || $field !== $index2) { - $finalField = $raw ? Common::mysql_escape($val[$field]) : "'" . Common::mysql_escape($val[$field]) . "'"; - $value = (is_null($val[$field]) ? 'NULL' : $finalField); + if ($field !== $index && $field !== $index2 && $field !== $updatedAtColumn) { + $hasChanges = true; + + // If increment / decrement + if (gettype($val[$field]) == 'array') { + // If array has two values + if (!array_key_exists(0, $val[$field]) || !array_key_exists(1, $val[$field])) { + throw new \ArgumentCountError('Increment/Decrement array needs to have 2 values, a math operator (+, -, *, /, %) and a number'); + } + // Check first value + if (gettype($val[$field][0]) != 'string' || !in_array($val[$field][0], ['+', '-', '*', '/', '%'])) { + throw new \TypeError('First value in Increment/Decrement array needs to be a string and a math operator (+, -, *, /, %)'); + } + // Check second value + if (!is_numeric($val[$field][1])) { + throw new \TypeError('Second value in Increment/Decrement array needs to be numeric'); + } + // Increment / decrement + // For increment/decrement, always consider it a change + if (Common::disableBacktick($driver)) { + $value = $field . $val[$field][0] . $val[$field][1]; + $changeConditions[] = '(' . $field . ' IS NOT NULL)'; + } else { + $value = '`' . $field . '`' . $val[$field][0] . $val[$field][1]; + $changeConditions[] = '(`' . $field . '` IS NOT NULL)'; + } + } else { + // Only update + $finalField = $raw ? Common::mysql_escape($val[$field]) : "'" . Common::mysql_escape($val[$field]) . "'"; + $value = (is_null($val[$field]) ? 'NULL' : $finalField); + + // Build condition to check if value actually changes + if (is_null($val[$field])) { + if (Common::disableBacktick($driver)) { + $changeConditions[] = '(' . $field . ' IS NOT NULL)'; + } else { + $changeConditions[] = '(`' . $field . '` IS NOT NULL)'; + } + } else { + if (Common::disableBacktick($driver)) { + $changeConditions[] = '(' . $field . ' != ' . $finalField . ')'; + } else { + $changeConditions[] = '(`' . $field . '` != ' . $finalField . ')'; + } + } + } if (Common::disableBacktick($driver)) { $final[$field][] = 'WHEN (' . $index . ' = \'' . Common::mysql_escape($val[$index]) . '\' AND ' . $index2 . ' = \'' . $val[$index2] . '\') THEN ' . $value . ' '; @@ -199,6 +305,35 @@ public function updateWithTwoIndex(Model $table, array $values, string|null $ind } } } + + // Handle explicit updated_at in values + if (isset($val[$updatedAtColumn])) { + $timestampFieldValue = $raw ? Common::mysql_escape($val[$updatedAtColumn]) : "'" . Common::mysql_escape($val[$updatedAtColumn]) . "'"; + $timestampFieldValue = (is_null($val[$updatedAtColumn]) ? 'NULL' : $timestampFieldValue); + + if (Common::disableBacktick($driver)) { + $final[$updatedAtColumn][] = 'WHEN (' . $index . ' = \'' . Common::mysql_escape($val[$index]) . '\' AND ' . $index2 . ' = \'' . $val[$index2] . '\') THEN ' . $timestampFieldValue . ' '; + } else { + $final[$updatedAtColumn][] = 'WHEN (`' . $index . '` = "' . Common::mysql_escape($val[$index]) . '" AND `' . $index2 . '` = "' . $val[$index2] . '") THEN ' . $timestampFieldValue . ' '; + } + } elseif ($hasChanges && $updatedAtColumn && count($changeConditions) > 0) { + // Only add timestamp update if there are actual field changes + $indexCondition = Common::disableBacktick($driver) ? + '(' . $index . ' = \'' . Common::mysql_escape($val[$index]) . '\' AND ' . $index2 . ' = \'' . $val[$index2] . '\')' : + '(`' . $index . '` = "' . Common::mysql_escape($val[$index]) . '" AND `' . $index2 . '` = "' . $val[$index2] . '")'; + + $combinedCondition = $indexCondition . ' AND (' . implode(' OR ', $changeConditions) . ')'; + + if (Common::disableBacktick($driver)) + $timestampConditions[] = 'WHEN ' . $combinedCondition . ' THEN \'' . $timestampValue . '\' '; + else + $timestampConditions[] = 'WHEN ' . $combinedCondition . ' THEN \'' . $timestampValue . '\' '; + } + } + + // Add timestamp conditions to final array if we have any + if (!empty($timestampConditions) && $updatedAtColumn) { + $final[$updatedAtColumn] = $timestampConditions; } @@ -296,8 +431,18 @@ public function updateMultipleCondition(Model $table, array $arrays, string|null } } + $updatedAtColumn = null; + $timestampValue = null; + + if ($timestamp) { + $updatedAtColumn = $table->getUpdatedAtColumn(); + $timestampValue = Carbon::now()->format($table->getDateFormat()); + } + $arraysNew = []; + $timestampConditions = []; // Track conditions for timestamp updates $keys = array_keys($columns); + foreach ($keys as $key) { $arraysMixed = collect($arrays)->filter(function ($rows) use ($key) { return in_array($key, array_keys($rows['columns'])); @@ -308,15 +453,63 @@ public function updateMultipleCondition(Model $table, array $arrays, string|null $arraysNew[$key][] = [ 'conditions' => $item['conditions'], 'value' => is_null($item['columns'][$key]) ? "NULL" : $value, + 'originalValue' => $item['columns'][$key], // Store original value for change detection ]; + } + } - if ($timestamp) { - $arraysNew['updated_at'][] = [ - 'conditions' => $item['conditions'], - 'value' => "'".(now())."'", + // Handle timestamp updates with change detection + if ($timestamp && $updatedAtColumn) { + foreach ($arrays as $item) { + $hasChanges = false; + $changeConditions = []; + + // Check if this item has any actual field changes + foreach ($item['columns'] as $fieldName => $newValue) { + if ($fieldName !== $updatedAtColumn) { + $hasChanges = true; + + // Build condition to check if value actually changes + if (is_null($newValue)) { + $changeConditions[] = " {$backtick}{$fieldName}{$backtick} IS NOT NULL "; + } else { + $escapedValue = $raw ? Common::mysql_escape($newValue) : "'" . Common::mysql_escape($newValue) . "'"; + $changeConditions[] = " {$backtick}{$fieldName}{$backtick} != {$escapedValue} "; + } + } + } + + // Handle explicit updated_at in columns + if (isset($item['columns'][$updatedAtColumn])) { + $timestampFieldValue = $raw ? Common::mysql_escape($item['columns'][$updatedAtColumn]) : "'" . Common::mysql_escape($item['columns'][$updatedAtColumn]) . "'"; + $timestampFieldValue = (is_null($item['columns'][$updatedAtColumn]) ? 'NULL' : $timestampFieldValue); + + $arraysNew[$updatedAtColumn][] = [ + 'conditions' => $item['conditions'], + 'value' => $timestampFieldValue, + 'originalValue' => $item['columns'][$updatedAtColumn], + ]; + } elseif ($hasChanges && count($changeConditions) > 0) { + // Only add timestamp update if there are actual field changes + $conditionsWithChanges = $item['conditions']; + $timestampConditions[] = [ + 'conditions' => $conditionsWithChanges, + 'value' => "'" . $timestampValue . "'", + 'changeConditions' => $changeConditions, // Store change conditions for SQL generation ]; } } + + // Add timestamp conditions with change detection to arraysNew + if (!empty($timestampConditions)) { + if (!isset($arraysNew[$updatedAtColumn])) { + $arraysNew[$updatedAtColumn] = []; + } + + foreach ($timestampConditions as $timestampCondition) { + $arraysNew[$updatedAtColumn][] = $timestampCondition; + } + } } $cases = []; @@ -332,7 +525,15 @@ public function updateMultipleCondition(Model $table, array $arrays, string|null } $conditionContext = join(' and ', $conditionContext); - $caseSql .= " WHEN $conditionContext THEN {$value} "; + + // Handle timestamp update with change conditions + if ($key === $updatedAtColumn && isset($item['changeConditions']) && !empty($item['changeConditions'])) { + // Add change detection conditions to only update timestamp when fields actually change + $changeConditionsSql = implode(' OR ', $item['changeConditions']); + $caseSql .= " WHEN ($conditionContext) AND ($changeConditionsSql) THEN {$value} "; + } else { + $caseSql .= " WHEN $conditionContext THEN {$value} "; + } } $caseSql .= " ELSE {$backtick}{$key}{$backtick} END)"; $cases[] = $caseSql;