Skip to content

UUID primary keys: relation auto-inserts persist the internal last-insert id instead of the generated UUID #2300

Description

@radoslav-grencik

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions