Production Notes

Routing fresh-aggregate reads to a read replica

withFreshAggregates() runs an aggregation per outer row — on a balanced-fanout tree at N=10K it's the most expensive read the package emits. If you have read replicas, route these reads off the primary:

Category::query()
    ->withFreshAggregates()
    ->useReadPdo()        // ← stays on Laravel's read connection
    ->get();

Caveat: Eloquent automatically routes any query inside an open transaction to the write PDO regardless of useReadPdo(), to avoid replication-lag visibility issues. If you wrap the read in a transaction (or call it from inside one), it lands on the primary anyway. For genuine replica routing, the read needs to live outside a transaction boundary.

Pair with the nestedset.aggregate_locking config flag — 'never' is safe on a read-only path; the locking modes only matter for the write path.

MariaDB: disabling split_materialized

The fresh-aggregate read path uses a derived-table JOIN on MariaDB so the subquery is materialised once per outer query rather than once per row. MariaDB's optimizer can convert that derived JOIN into a LATERAL DERIVED via split_materialized, which collapses the materialise-once advantage and runs ~3× slower in practice. withMariaDbSplitMaterializedOff() prepends a SET STATEMENT optimizer_switch='split_materialized=off' FOR … to the next compiled SQL — scoped to the one statement, no session-state mutation:

Category::query()
    ->withFreshAggregates()
    ->withMariaDbSplitMaterializedOff()
    ->get();

No-op on MySQL/PostgreSQL/SQLite — the SET STATEMENT prefix is MariaDB-specific syntax. Only reach for it if profiling shows the fresh-aggregate path running unexpectedly slow on MariaDB.

Telemetry

The package fires typed events on Laravel's event bus around its meaningful operations. Listen via standard Event::listen() to wire metrics (Datadog, New Relic, OpenTelemetry), errors (Sentry, Bugsnag), or audit logs.

Events (all in Vusys\NestedSet\Events\):

Event Fires when
FixTreeCompleted Model::fixTree() finishes
FixAggregatesCompleted Model::fixAggregates() finishes (sync, single-shot or chunked)
FixAggregatesChunkCompleted per chunk in sync chunked + per dispatch in queued chunked
FixAggregatesJobDispatched Model::queueFixAggregates() hands a job to the dispatcher
BulkInsertTreeCompleted Model::bulkInsertTree() finishes
DeferredAggregateMaintenanceCompleted outermost exit of withDeferredAggregateMaintenance() after the closing repair
NodeMoved structural mutation of an existing node (appendToNode, makeRoot, etc.) — new-node placements use Eloquent's created instead
AggregateMaintenanceFailed exception escapes one of the trait's aggregate-maintenance hooks — propagates the original, but lets observers see the failure

Example wirings

use Vusys\NestedSet\Events\FixAggregatesCompleted;
use Vusys\NestedSet\Events\FixAggregatesChunkCompleted;
use Vusys\NestedSet\Events\AggregateMaintenanceFailed;

// Datadog histogram for repair latency
Event::listen(FixAggregatesCompleted::class, function (FixAggregatesCompleted $e): void {
    Datadog::histogram('nestedset.fix_aggregates.duration_ms', $e->durationMs, [
        'model' => $e->modelClass,
        'rows' => $e->totalRowsUpdated,
        'chunks' => $e->totalChunks,
    ]);
});

// Streaming progress to logs for long-running chunked repairs
Event::listen(FixAggregatesChunkCompleted::class, function (FixAggregatesChunkCompleted $e): void {
    Log::info("nestedset chunk {$e->chunkIndex}: {$e->rowsUpdated} rows in {$e->durationMs}ms");
});

// Sentry for hook failures
Event::listen(AggregateMaintenanceFailed::class, function (AggregateMaintenanceFailed $e): void {
    Sentry::captureException($e->exception, [
        'tags' => ['nestedset_stage' => $e->stage, 'nestedset_model' => $e->modelClass],
    ]);
});

All event classes are simple readonly value objects with scalar / array fields, so queued listeners (ShouldQueue) are safe — with one exception: AggregateMaintenanceFailed::$exception is a Throwable and won't serialise cleanly across most queue drivers. If you need to queue listeners on that event, capture the scalar fields you care about synchronously and forward those.

To disable every firing site (e.g. in a very-hot path), set nestedset.events_enabled => false in the published config. Default is true.