Tempest version
3.19
PHP version
8.5
Operating system
Linux
Description
Summary
When a model uses #[Uuid] as its primary key, relation auto-inserts performed by InsertQueryBuilder's after callbacks (BelongsToMany pivot rows, HasOne / HasMany child rows) are persisted with the database's internal last-insert id as the parent foreign key instead of the actual generated UUID.
On SQLite this silently corrupts the pivot table:
user_roles: user_id = "1", role_id = "01a09f0e-d128-7c50-abf1-3e861d348a9e"
-- ^^^ the SQLite rowid, not the user's UUID
Environment
- SQLite (reproducible on MySQL as well; PostgreSQL is unaffected because
InsertStatement compiles RETURNING *)
Reproduction
final class User
{
use IsDatabaseModel;
#[Uuid]
public PrimaryKey $id;
public string $name;
/** @var Role[] */
#[BelongsToMany]
public array $roles = [];
}
final class Role
{
use IsDatabaseModel;
#[Uuid]
public PrimaryKey $id;
public string $name;
}
$role = query(Role::class)->create(name: 'admin');
$user = query(User::class)->create(
name: 'Jane',
roles: [$role],
);
$user->id->value; // "01a09f0e-d13a-7e15-9190-f4a58d51e0aa" (correct UUID)
query('user_roles')->select()->all();
// [ ['user_id' => "1", 'role_id' => "01a09f0e-..."] ]
// ^^^
// Expected: $user->id. Actual: the internal rowid.
The same happens for HasMany/HasOne children: their owner foreign key column receives the last-insert id instead of the parent's UUID.
Expected behavior
The pivot row's owner foreign key (and child rows' foreign keys) contain the actual generated UUID primary key of the inserted parent model.
Actual behavior
The owner foreign key contains getLastInsertId() — for SQLite with a TEXT primary key this is the internal rowid ("1", "2", ...), for MySQL the value of last_insert_id(). Both are meaningless for an explicit string primary key.
Root cause
packages/database/src/Query.php, Query::execute():
return isset($query->bindings[$this->primaryKeyColumn])
? new PrimaryKey($query->bindings[$this->primaryKeyColumn])
: $database->getLastInsertId();
Insert bindings are positional ([0 => ..., 1 => ...]), so isset($query->bindings[$this->primaryKeyColumn]) (e.g. $bindings['id']) is always false and the code always falls back to getLastInsertId().
This works for auto-increment primary keys only because the last insert id is the primary key there. With #[Uuid] (an explicit string primary key generated in PHP before the insert) it is not.
Known limitation (out of scope for a fix)
Batch inserts (insert($modelA, $modelB, ...)) with relation data only attach relations to a single parent — execute() returns one PrimaryKey and after callbacks run once. This is pre-existing behavior for all primary key types and would require a larger redesign (per-row parent ids); it is not part of this issue.
Tempest version
3.19
PHP version
8.5
Operating system
Linux
Description
Summary
When a model uses
#[Uuid]as its primary key, relation auto-inserts performed byInsertQueryBuilder'saftercallbacks (BelongsToManypivot rows,HasOne/HasManychild rows) are persisted with the database's internal last-insert id as the parent foreign key instead of the actual generated UUID.On SQLite this silently corrupts the pivot table:
Environment
InsertStatementcompilesRETURNING *)Reproduction
The same happens for
HasMany/HasOnechildren: their owner foreign key column receives the last-insert id instead of the parent's UUID.Expected behavior
The pivot row's owner foreign key (and child rows' foreign keys) contain the actual generated UUID primary key of the inserted parent model.
Actual behavior
The owner foreign key contains
getLastInsertId()— for SQLite with aTEXTprimary key this is the internal rowid ("1","2", ...), for MySQL the value oflast_insert_id(). Both are meaningless for an explicit string primary key.Root cause
packages/database/src/Query.php,Query::execute():Insert bindings are positional (
[0 => ..., 1 => ...]), soisset($query->bindings[$this->primaryKeyColumn])(e.g.$bindings['id']) is alwaysfalseand the code always falls back togetLastInsertId().This works for auto-increment primary keys only because the last insert id is the primary key there. With
#[Uuid](an explicit string primary key generated in PHP before the insert) it is not.Known limitation (out of scope for a fix)
Batch inserts (
insert($modelA, $modelB, ...)) with relation data only attach relations to a single parent —execute()returns onePrimaryKeyandaftercallbacks run once. This is pre-existing behavior for all primary key types and would require a larger redesign (per-row parent ids); it is not part of this issue.