Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 14 additions & 46 deletions lib/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -1013,7 +1013,7 @@ private function insert($validate = true)
*
* @return bool True if the model was saved to the database otherwise false
*/
private function update($validate = true)
private function update(bool $validate = true)
{
$this->verify_not_readonly('update');

Expand All @@ -1031,8 +1031,15 @@ private function update($validate = true)
return false;
}

$dirty = $this->dirty_attributes();
static::table()->update($dirty, $pk);
$attributes = $this->dirty_attributes();

$options = [
'conditions' => [
new WhereClause([$this->table()->pk[0] => $pk], [])
]
];

static::table()->update($attributes, $options);
$this->invoke_callback('after_update', false);
$this->update_cache();
}
Expand Down Expand Up @@ -1076,52 +1083,13 @@ public static function delete_all(): int
}

/**
* Updates records using set in $options
*
* Does not instantiate models and therefore does not invoke callbacks
*
* Update all using a hash:
*
* ```
* YourModel::update_all(['set' => ['name' => "Bob"]]);
* ```
*
* Update all using a string:
*
* ```
* YourModel::update_all(['set' => 'name = "Bob"']);
* ```
*
* An options array takes the following parameters:
* @param string|Attributes $attributes
*
* @param QueryOptions $options
*
* @return int Number of rows affected
* @see Relation::update_all()
*/
public static function update_all(array $options = []): int
public static function update_all(string|array $attributes): int
{
$table = static::table();
$conn = static::connection();
$sql = new SQLBuilder($conn, $table->get_fully_qualified_table_name());

isset($options['set']) && $sql->update($options['set']);

if (isset($options['conditions']) && ($conditions = $options['conditions'])) {
$sql->where([WhereClause::from_arg($conditions)]);
}

if (isset($options['limit'])) {
$sql->limit($options['limit']);
}

if (isset($options['order'])) {
$sql->order($options['order']);
}

$values = $sql->bind_values();
$ret = $conn->query($table->last_sql = $sql->to_s(), $values);

return $ret->rowCount();
return static::Relation()->update_all($attributes);
}

/**
Expand Down
39 changes: 39 additions & 0 deletions lib/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* @template TModel of Model
*
* @phpstan-import-type RelationOptions from Types
* @phpstan-import-type Attributes from Types
*/
class Relation implements \Iterator
{
Expand Down Expand Up @@ -846,6 +847,44 @@ public function to_a(): array
return $this->_to_a($this->options);
}

/**
* Updates all records in the current relation with provided attributes.
* This method constructs a single SQL UPDATE statement and sends it
* straight to the database. It does not instantiate the involved models
* and it does not trigger Active Record callbacks or validations.
* However, values passed to #update_all will still go through Active
* Record's normal type casting and serialization. Returns the number of
* rows affected.
*
* Note: As Active Record callbacks are not triggered, this method will
* not automatically update updated_at+/+updated_on columns.
*
* // Update all customers with the given attributes
* Customer::update_all( ['wants_email'] => true)
*
* // Update all books with 'Rails' in their title
* Book::where('title LIKE ?', '%Rails%')
* ->update_all(['author', 'David'])
*
* // Update all books that match conditions, but limit it to 5 ordered by date
* Book::where('title LIKE ?', '%Rails%')
* ->order('created_at')
* ->limit(5)
* ->update_all(['author'=> 'David'])
*
* // Update all invoices and set the number column to its id value.
* Invoice::update_all('number = id')
*
* @param string|Attributes $attributes a string or hash representing the SET part of
* an SQL statement
*
* @return int number of affected records
*/
public function update_all(array|string $attributes): int
{
return $this->table()->update($attributes, $this->options);
}

/**
* Deletes the records without instantiating the records
* first, and without invoking callbacks.
Expand Down
12 changes: 6 additions & 6 deletions lib/SQLBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,18 +216,18 @@ public function insert(array $hash, mixed $pk = null, string $sequence_name = nu
}

/**
* @param Attributes|string $mixed
* @param string|Attributes $data
*
* @throws ActiveRecordException
*/
public function update(array|string $mixed): static
public function update(array|string $data): static
{
$this->operation = 'UPDATE';

if (is_hash($mixed)) {
$this->data = $mixed;
} elseif (is_string($mixed)) {
$this->update = $mixed;
if (is_hash($data)) {
$this->data = $data;
} elseif (is_string($data)) {
$this->update = $data;
}

return $this;
Expand Down
19 changes: 10 additions & 9 deletions lib/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -422,21 +422,22 @@ public function insert(array &$data, string|int $pk = null, string $sequence_nam
}

/**
* @param Attributes $data
* @param Attributes $where
* @param string|Attributes $attributes
* @param Attributes $options
*
* @throws Exception\ActiveRecordException
*/
public function update(array &$data, array $where): \PDOStatement
public function update(string|array $attributes, array $options): int
{
$data = $this->process_data($data);

$sql = new SQLBuilder($this->conn, $this->get_fully_qualified_table_name());
$sql->update($data)->where([new WhereClause($where, [])], []);

$sql = $this->options_to_sql($options);
if (is_hash($attributes)) {
$attributes = $this->process_data($attributes);
}
$sql->update($attributes);
$values = $sql->bind_values();
$ret = $this->conn->query($this->last_sql = $sql->to_s(), $values);

return $this->conn->query($this->last_sql = $sql->to_s(), $values);
return $ret->rowCount();
}

/**
Expand Down
29 changes: 13 additions & 16 deletions test/ActiveRecordWriteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,18 +395,18 @@ public function testDeleteAllWithDistinct()
Author::distinct()->where('parent_author_id = 2')->delete_all();
}

public function testUpdateAllWithSetAsString()
public function testUpdateAllWithString()
{
Author::update_all(['set' => 'parent_author_id = 2']);
Author::update_all('parent_author_id = 2');
$ids = Author::pluck('parent_author_id');
foreach ($ids as $id) {
$this->assertEquals(2, $id);
}
}

public function testUpdateAllWithSetAsHash()
public function testUpdateAllWithHash()
{
Author::update_all(['set' => ['parent_author_id' => 2]]);
Author::update_all(['parent_author_id' => 2]);
$ids = Author::pluck('parent_author_id');
foreach ($ids as $id) {
$this->assertEquals(2, $id);
Expand All @@ -418,27 +418,21 @@ public function testUpdateAllWithSetAsHash()
*/
public function testUpdateAllWithConditionsAsString()
{
$num_affected = Author::update_all([
'set' => 'parent_author_id = 2',
'conditions' => ['name = ?', 'Tito']
]);
$num_affected = Author::where(['name = ?', 'Tito'])->update_all('parent_author_id = 2');
$this->assertEquals(2, $num_affected);
}

public function testUpdateAllWithConditionsAsHash()
{
$num_affected = Author::update_all([
'set' => 'parent_author_id = 2',
'conditions' => [
'name' => 'Tito'
]
]);
$num_affected = Author::where([
'name' => 'Tito'
])->update_all('parent_author_id = 2');
$this->assertEquals(2, $num_affected);
}

public function testUpdateAllWithConditionsAsArray()
{
$num_affected = Author::update_all(['set' => 'parent_author_id = 2', 'conditions' => ['name = ?', 'Tito']]);
$num_affected = Author::where(['name = ?', 'Tito'])->update_all('parent_author_id = 2');
$this->assertEquals(2, $num_affected);
}

Expand All @@ -448,7 +442,10 @@ public function testUpdateAllWithLimitAndOrder()
$this->markTestSkipped('Only MySQL & Sqlite accept limit/order with UPDATE clause');
}

$num_affected = Author::update_all(['set' => 'parent_author_id = 2', 'limit' => 1, 'order' => 'name asc']);
$num_affected = Author::limit(1)
->order('name asc')
->update_all('parent_author_id = 2');

$this->assertEquals(1, $num_affected);
$this->assertTrue(false !== strpos(Author::table()->last_sql, 'ORDER BY name asc LIMIT 1'));
}
Expand Down