Skip to content

Typed atomic expressions

Version availability

These APIs are implemented on the Core development branch for the next 3.2 release; 3.1.0 does not include them.

Expr constructs a closed, typed SQL expression tree. The SQL adapters validate field names against ORM mappings, bind every scalar value, and emit MySQL- or PostgreSQL-specific expressions.

Atomic counters and compare-and-swap

use Small\SwooleEntityManager\QueryBuilder\RelationalQueryBuilder\Expression\Expr;
use Small\SwooleEntityManager\QueryBuilder\RelationalQueryBuilder\Enum\ConditionOperatorType;

$update = $jobManager->createUpdateBuilder()
    ->setExpression(
        'revision',
        Expr::add(Expr::field('revision'), Expr::value(1)),
    )
    ->setExpression('leaseUntil', Expr::addSeconds(Expr::utcNow(), Expr::value(30)));

$update->where()
    ->firstCondition(
        $update->getFieldForCondition('id'),
        ConditionOperatorType::equal,
        ':id',
    )
    ->andCondition(
        $update->getFieldForCondition('revision'),
        ConditionOperatorType::equal,
        ':expectedRevision',
    );

$update->setParameter('id', $jobUuid);
$update->setParameter('expectedRevision', $expectedRevision);

$affected = $jobManager->executeUpdateAffectedRows($update, $pdo);

if ($affected !== 1) {
    throw new LogicException('Optimistic concurrency conflict.');
}

This executes as one conditional database statement. It does not read the current revision into PHP and then write back a stale value. The native $pdo must be the same connection used by other operations in the workflow transaction.

Expression catalog

Expression Purpose
Expr::field('revision') Validate and reference a mapped field
Expr::value(5) Bind a scalar value
Expr::add(...) / subtract(...) Arithmetic without a read/modify/write race
Expr::multiply(...) / divide(...) Arithmetic expressions
Expr::utcNow() UTC time using the database clock
Expr::addSeconds(...) / subtractSeconds(...) Generate lease deadlines
Expr::coalesce(...) / greatest(...) Null fallback and maximum
Expr::equal(...) / greater(...) / less(...) Typed conditions
Expr::and(...) / or(...) / isNull(...) Boolean combinations
Expr::caseWhen(...) Conditional values
Expr::countIf(...) / sumIf(...) Expressions for conditional aggregates

The query compiler uses dialect-specific SQL for the time functions. Never pass SQL fragments to Expr::value(); they are treated as data, not executable SQL. A field outside the mapped entity causes an exception.

Conditional metrics without PHP iteration

$stats = $jobManager->createStatsQueryBuilder('job')
    ->countIf(
        Expr::equal(Expr::field('status'), Expr::value('pending')),
        'ready',
    )
    ->sumIf(
        Expr::greater(Expr::field('attempts'), Expr::value(0)),
        Expr::field('attempts'),
        'totalAttempts',
    );

$raw = $jobManager->getRawResult($stats, $pdo);

The SQL is generated using conditional SUM(CASE WHEN ... THEN ... ELSE ... END) constructs, with COALESCE returning zero when there is no match. This can replace multi-pass PHP counting for recovery dashboards.

Transactional and concurrency constraints

  • To update only one revision, always include an expected revision in WHERE and check executeUpdateAffectedRows().
  • For a multi-statement workflow, keep the same native connection through the entire transaction.
  • Use forUpdate(skipLocked: true) when claiming concurrent work. This is an existing 3.1 feature.
  • After a communication failure, do not silently retry a statement whose commit outcome is ambiguous.
  • setExpression() is for structured expressions, not arbitrary SQL text. Previous addBoundFieldToUpdate() support is unchanged.

Related: atomic upserts, update builder, statistics, transactions.