Full-Stack Development
What changed in Laravel 13.36: DB::olap() and fluent rule builders
Adopt DB::olap() for large PlanetScale and Vitess queries and replace string validation rules with fluent Rule builders in Laravel 13.36.
By Tanvir Ahamed, published . 7 min read.
Researched and written with AI assistance; sources are linked below.
Running a multi-year aggregation or CSV export against a PlanetScale or Vitess database fails when the query scans or returns more than 100,000 rows, while string-based validation rules in FormRequest classes break whenever a target parameter value contains a comma. Laravel v13.36.0, released in October 2026, addresses both problems with two additions: DB::olap() for switching Vitess connections into OLAP mode safely, and fluent Rule builders for every remaining built-in validation rule. After walking through how both features work under the hood, you will be able to wrap analytical queries without leaking session state and migrate brittle string validation rules to type-safe builders.
Why Vitess and PlanetScale cap OLTP queries at 100,000 rows
PlanetScale databases run on Vitess, which defaults every connection to the OLTP (Online Transaction Processing) workload mode. In OLTP mode, PlanetScale enforces two hard guardrails on every query: a single statement can return, update, or delete at most 100,000 rows, and the result set cannot exceed 64 MiB (Laravel News).
These limits protect transactional throughput from runaway queries, but they create a discrepancy between local development and production. A reporting query or backfill that runs cleanly against a local MySQL or MariaDB instance will fail on PlanetScale as soon as the dataset grows past 100,000 rows.
Vitess provides a second session workload mode called OLAP (Online Analytical Processing), which removes both the 100,000-row cap and the 64 MiB result set limit (Laravel News). Previously, switching to OLAP mode from Laravel required executing raw session statements around your query:
<?php
use App\Models\Order;
use Illuminate\Support\Facades\DB;
// Problematic manual approach prior to Laravel 13.36
DB::statement("set workload = 'olap'");
$revenue = Order::query()
->selectRaw("customer_id, date_format(created_at, '%Y-%m') as month, sum(total) as revenue")
->where('created_at', '>=', now()->subYears(3))
->groupBy('customer_id', 'month')
->get();
DB::statement("set workload = 'oltp'");Managing the session mode manually introduces three failure modes:
- Read replica routing: Calling
DB::statement("set workload = 'olap'")modifies the write connection. If your database configuration uses read and write connections, subsequentSELECTqueries run against the read connection, which is still inoltpmode (Laravel News). - Leaked session state on exceptions: Because
workloadis a session-level setting, any exception thrown during the query skips the trailingset workload = 'oltp'statement. In long-lived processes such as queue workers or Laravel Octane, the connection remains stuck inolapmode for subsequent jobs or requests (Laravel News). - Local environment failures: Standard MySQL and MariaDB servers do not recognise the
set workloadstatement, forcing you to wrap the raw statement in environment checks so local development and CI tests do not crash (Laravel News).
Running large analytical queries with DB::olap()
Contributed by Jack Bayliss in pull request #61932 (GitHub release notes), Laravel 13.36 adds isVitess() and olap() directly to Illuminate\Database\MySqlConnection.
First, isVitess() inspects the database server version string and returns true when the version contains Vitess or PlanetScale (Laravel Database documentation). You can call it directly if you ever need to branch on the underlying engine:
<?php
use Illuminate\Support\Facades\DB;
$isVitess = DB::connection('mysql')->isVitess();When you pass a closure to DB::olap(), MySqlConnection uses isVitess() to decide whether to switch workloads, and wraps the callback in a try / finally block:
public function olap(Closure $callback)
{
if (! $this->isVitess()) {
return $callback($this);
}
$this->setWorkload('olap');
try {
return $callback($this);
} finally {
$this->setWorkload('oltp');
}
}Under the hood, MySqlConnection::setWorkload() issues the set workload statement on both the write PDO connection and the read PDO connection, initializing the read connection first if it is still a lazy Closure (MySqlConnection.php, Laravel News). That guarantees SELECT queries routed to a read replica execute in OLAP mode, and the finally block guarantees both connections return to OLTP mode even if the query throws an exception. On standard MySQL or MariaDB in local development or CI, isVitess() returns false and DB::olap() executes the closure directly as a no-op (Laravel Database documentation).
Here is how to wrap a multi-year reporting query with DB::olap() in an export action:
<?php
namespace App\Actions;
use App\Models\Order;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class ExportThreeYearRevenueAction
{
public function execute(): Collection
{
return DB::olap(fn () => Order::query()
->selectRaw("customer_id, date_format(created_at, '%Y-%m') as month, sum(total) as revenue")
->where('created_at', '>=', now()->subYears(3))
->groupBy('customer_id', 'month')
->get()
);
}
}When to use DB::olap() versus chunking or dedicated analytics connections
While DB::olap() removes the 100,000-row and 64 MiB ceilings, it should not be used as a default wrapper for routine application queries. PlanetScale's OLTP limits exist to prevent long-running queries from exhausting database CPU and memory (Laravel News). In addition, PlanetScale warns that OLAP queries can fail with errors if the underlying connection is interrupted while the query is running (Laravel News).
I'd reserve DB::olap() for three specific workloads (Laravel News):
- Occasional CSV or data exports triggered by staff or background jobs.
- One-off data backfills during deployments or maintenance tasks.
- Internal admin or reporting queries that must scan or return more than 100,000 rows in a single statement.
For regular application traffic, stick to standard OLTP mode and paginate or chunk large queries, or replicate data to a dedicated analytics database when heavy reporting runs frequently (Laravel News).
Replacing string validation rules with fluent Rule builders in Laravel 13.36
Prior to Laravel 13.36, Illuminate\Validation\Rule offered fluent builders for a subset of rules (in, notIn, unique, exists, enum, date, email, file, imageFile, dimensions, numeric, array, contains, anyOf, can, prohibitedIf, requiredIf, and excludeIf), leaving the rest of your FormRequest arrays reliant on colon-and-comma string syntax (Laravel News).
String rules have two drawbacks: they lack IDE autocomplete and static analysis support, and Laravel's string parser splits rule parameters on commas. If a rule parameter itself contains a comma, such as checking 'accepted_if:plan,Team, Annual', the validator splits 'Team, Annual' into two separate parameters ('Team' and 'Annual'), causing validation to never match 'Team, Annual' (Laravel News).
Contributed by Eric (@ericmp33) in pull request #61833 (GitHub release notes, Laravel News), Laravel 13.36 adds fluent Rule:: methods for every remaining built-in validation rule. The new methods fall into three groups.
Parameterless rule methods
Nineteen new methods take no required parameters and return the underlying validation rule string directly (with Rule::boolean() accepting an optional $strict flag) (Laravel News, HowToShipIt):
Rule::accepted()returns'accepted'Rule::activeUrl()returns'active_url'Rule::bail()returns'bail'Rule::base64()returns'base64'Rule::boolean(bool $strict = false)returns'boolean', or'boolean:strict'when passedtrueso onlytrueandfalsepassRule::declined()returns'declined'Rule::exclude()returns'exclude'Rule::filled()returns'filled'Rule::hexColor()returns'hex_color'Rule::json()returns'json'Rule::list()returns'list'Rule::macAddress()returns'mac_address'Rule::missing()returns'missing'Rule::nullable()returns'nullable'Rule::present()returns'present'Rule::prohibited()returns'prohibited'Rule::required()returns'required'Rule::sometimes()returns'sometimes'Rule::ulid()returns'ulid'
Parameterized rules for fields, keys, and values
Rules that accept field names, array keys, patterns, or comparison values now return objects implementing the ParameterizedRule contract (Laravel News). These methods accept variadic arguments or arrays:
- Conditional requirement rules:
Rule::requiredWith(...),Rule::requiredWithout(...),Rule::requiredWithoutAll(...),Rule::requiredIfAccepted($field), andRule::requiredArrayKeys(...)(Laravel News, HowToShipIt). - Acceptance and prohibition rules:
Rule::acceptedIf($field, $value),Rule::declinedIf($field, $value),Rule::prohibitedIfAccepted($field),Rule::prohibitedIfDeclined($field), andRule::prohibits(...)(Laravel News). - Presence, missing, and exclusion rules:
Rule::presentIf($field, $value),Rule::presentUnless($field, $value),Rule::presentWith(...),Rule::presentWithAll(...),Rule::missingIf($field, $value),Rule::missingUnless($field, $value),Rule::missingWith(...),Rule::missingWithAll(...),Rule::excludeWith($field), andRule::excludeWithout($field)(Laravel News). - Pattern, array, and timezone rules:
Rule::regex($pattern),Rule::notRegex($pattern),Rule::inArray($otherField), andRule::timezone(...)(for example,Rule::timezone(['per_country', 'US'])replaces'timezone:per_country,US') (Laravel News, HowToShipIt).
Because ParameterizedRule exposes a toArray() method that returns the rule name and its parameters as a structured array, the validator consumes those parameters directly without splitting on colons or commas (Laravel News). Commas, quotes, pipes, and backslashes in values or array keys are preserved intact. Non-string scalar parameters such as true, false, and null are converted to 'true', 'false', and 'null' automatically (Rule::presentIf('is_company', true) produces present_if:is_company,true), and casting a builder to a string for logging wraps any parameter containing a comma or double quote in double quotes (Laravel News).
Rules with chainable options
Six rules return dedicated builder objects with chainable methods, alongside a new equals() method on the existing Rule::date() builder (Laravel News):
Rule::url()->protocols(['https'])produces'url:https'.Rule::uuid()->version(4)orRule::uuid()->version(7)produces'uuid:4'or'uuid:7', andversion()also accepts'nil'and'max'.Rule::ip()->version(6)produces'ipv6'(accepting4or6, and throwing anInvalidArgumentExceptionfor any other number).Rule::distinct()->strict()->ignoreCase()produces'distinct:strict,ignore_case'.Rule::confirmed()->customField('repeat_pass')produces'confirmed:repeat_pass'(or'confirmed'when called without arguments).Rule::currentPassword()->guard('admin')produces'current_password:admin'.Rule::date()->equals('2026-12-31')orRule::date()->equals(today())adds adate_equalsrule.
Migrating existing FormRequest classes and testing the upgrade
To upgrade an existing Laravel 13 project to 13.36 or newer, raise the constraint or update the framework via Composer (HowToShipIt):
composer require laravel/framework:^13.36You do not need to rewrite every FormRequest at once. String rules remain supported and can be mixed with both existing and new Rule:: builders in the same rules() array (Laravel News). Here is a CheckoutRequest class that combines the new parameterless, parameterized, and chainable builders while safely handling a plan name containing a comma:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class CheckoutRequest extends FormRequest
{
public function rules(): array
{
return [
'plan' => [Rule::required(), Rule::in(['starter', 'Team, Annual'])],
'terms' => [Rule::acceptedIf('plan', 'Team, Annual')],
'company_name' => [Rule::requiredIfAccepted('is_business')],
'vat_number' => [Rule::nullable(), Rule::requiredWith('company_name')],
'billing' => [Rule::required(), Rule::requiredArrayKeys('street', 'city', 'postcode')],
'coupon_code' => [Rule::nullable(), Rule::prohibits('gift_card')],
'website' => [Rule::nullable(), Rule::url()->protocols(['https'])],
'emails.*' => [Rule::distinct()->ignoreCase(), 'email'],
];
}
}I prefer starting the migration with rules that accept user-facing strings, regex patterns, or array keys, such as Rule::acceptedIf(), Rule::regex(), and Rule::requiredArrayKeys(), because those are the rules where commas and pipes in string syntax cause silent validation bugs. Next, check your codebase for raw set workload statements and replace them with DB::olap() closures so read replicas and exception handling are covered automatically.
Sources
- Laravel
- PHP
- PlanetScale
- MySQL
- Validation
Related articles
Upgrading from Next.js 15 to 16.4 before end of life
Migrate from Next.js 15 to 16.4.2, apply October 2026 security patches, update async request APIs, and adopt Partial Prefetching.
Full-Stack Development, 9 min read
Shipping a static Next.js site on Firebase Hosting
How this site is built: a Next.js static export served from Firebase Hosting, with URLs, caching and SEO metadata that agree with each other.
Full-Stack Development, 4 min read