Elyra
Elyra The coding agent e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Elyra

Export

Export the entire filtered result — not just the current page — streamed to the client in bounded key-range windows. Memory stays flat regardless of size, provided the grid declares a key (see Grids without a key). Measured through the HTTP endpoint: 1.67M rows (6 columns) in ~52 s at constant memory.

Server

GridDefinition::export() returns a StreamedResponse:

use Elyra\DataGrid\Protocol\GridRequest;

Route::post('/api/sales/export', fn (Request $r) => salesGrid()->export(
    GridRequest::fromArray(json_decode($r->input('request'), true) ?: []),
    'csv',        // 'csv' (default) | 'xlsx'
    'sales',      // filename (without extension)
));

The export honors the request's columns, filter and search, ignores paging (it streams everything that matches), and orders by the key column for a stable, single-pass scan.

Formats

Format Notes
csv (default) UTF-8 with BOM + ; delimiter (opens cleanly in Excel). No dependencies.
xlsx Rich styling (bold header band, right-aligned numeric columns with a #,##0.00 number format, sensible column widths). Requires openspout/openspout; falls back to CSV if not installed.

How it streams

By default the exporter uses keyset pagination (WHERE key > ? ORDER BY key ASC LIMIT n, 5000 rows per batch): it works for numeric and string/UUID keys, skips gaps in the key space (no empty queries), and keeps memory bounded to one batch — the standard, index-friendly approach for MySQL, ClickHouse and SQLite / SQL Anywhere.

ElyraSQL is the exception (exportsByKeyWindow()): it can't stream an unbuffered cursor, a huge buffered transfer fails, and open-ended key > ? seeks were full scans, so for a numeric key it walks double-bounded key windows (key >= a AND key < b) instead. The streamed response sets set_time_limit(0) and works across SAPIs (built-in server, fpm, CLI).

Windows walk the key's value range, which only works while ids are reasonably dense. With Snowflake/ULID-style ids (~1e18) the range would need on the order of 1e12 windows, so past a bounded window count the exporter switches to keyset pagination instead — slower per query on ElyraSQL, but bounded in both memory and query count for any key distribution.

Add a compound (key)/(filterColumn, key) index for the fastest windows on very large tables.

Grids without a key

A grid with no ->key(...) cannot be paginated: there is no stable order to page by, so LIMIT/OFFSET could duplicate or skip rows. The only deterministic option is a single read, which puts the whole result set in PHP memory — so export() refuses unless you have set a row cap:

// Either give the grid a key so the export can be paginated…
GridDefinition::table('sales')->key('id')->columns([...]);

// …or accept a bounded export and cap it explicitly.
// config/datagrid.php → 'limits' => ['max_export_rows' => 100_000]
// or ELYRA_DATAGRID_MAX_EXPORT_ROWS=100000

With a cap set, the limit is applied in SQL (LIMIT n), so memory is bounded by the cap rather than by the table.

Spreadsheet formula injection

CSV is written for Excel, which evaluates any cell whose text begins with =, +, -, @, a tab or a carriage return — so a stored =WEBSERVICE(...) would run on the machine of whoever opens the download. Such values are exported with a leading apostrophe, which Excel reads as literal text and does not display. Numbers are untouched, so numeric columns still import as numbers. XLSX needs no equivalent: openspout writes inline strings, which are never evaluated.

composer require openspout/openspout   # optional, for real .xlsx

Queued exports

A streamed export holds the HTTP response open for its whole duration. That runs into infrastructure long before it runs into the database: 60 s is the default idle timeout on an AWS ALB and on nginx's proxy_read_timeout, 100 s on Cloudflare — and the 1.67M-row benchmark above takes ~52 s. A buffering proxy is worse still: it collects the whole response before forwarding, which defeats the streaming and moves the memory pressure into the proxy. Meanwhile each export occupies one PHP-FPM worker from first byte to last.

Queue it instead: the toolbar shows progress and then a signed download link.

// A queued export rebuilds its grid by name, because authorize() holds a closure
// and cannot be serialized. Register it (AppServiceProvider::boot):
Grid::define('sales', fn () => salesGrid());

// Mount the status/download routes with YOUR middleware. Nothing is registered
// unless you ask — this package ships no routes by default.
Route::middleware(['web', 'auth'])->group(function () {
    Grid::exportRoutes();
});

// Your own enqueue endpoint decides who may export, then hands off:
Route::post('/api/sales/export/queue', function (Request $r) {
    $export = app(GridExportManager::class)->queue(
        grid: 'sales',
        request: GridRequest::fromArray(json_decode($r->input('request'), true) ?: []),
        format: 'csv',
        filename: 'sales',
        userId: (string) $r->user()->id,
    );

    return [
        'id' => $export->id,
        'statusUrl' => URL::temporarySignedRoute(
            'elyra-datagrid.exports.status',
            now()->addHour(),
            ['id' => $export->id],
        ),
    ];
})->middleware(['web', 'auth']);

Then point the client at it — queueExportUrl in Vue/Svelte, or public bool $queuedExport = true; on a Livewire grid.

Needs a queue worker. On the sync driver the job runs inline, which puts you back in a long request.

What it does not solve

Queueing fixes transport, not the read. The exporter's bounds still do that, and they matter more here: a queue worker usually has a laxer memory_limit and no max_execution_time, so an unbounded read fails harder and less visibly than it would in a web request. Keyless grids are still refused without a cap.

The file at rest

This is the part streaming never had. A queued export writes customer data to a disk and hands out a URL, so four things guard the download — a signed link is a bearer capability, and Laravel's signed URLs do not authenticate:

  1. the export exists and finished
  2. it has not expired (export.link_ttl, export.retain_for)
  3. the caller is the user who requested it
  4. the grid's export policy still allows it, re-evaluated at download time, not trusted from when the job was queued

And nothing else cleans up, so schedule the prune command:

Schedule::command('elyra-datagrid:prune-exports')->hourly();
Setting Default Meaning
export.disk local Laravel disk the files live on
export.directory elyra-datagrid-exports Directory on that disk
export.queue default Queue the job goes on
export.link_ttl 3600 How long a signed link stays valid
export.retain_for 86400 How long the file exists at all

Progress

Rows written are always reported. Percent needs the row count up front, which is a full scan — so it appears only when the request asked for total in meta. This package does not run a COUNT behind your back.

Livewire

Enable the toolbar button; it calls export() directly:

class SalesGrid extends DataGrid
{
    public bool $exportable = true;
    public string $exportFormat = 'csv';   // or 'xlsx'
}

Vue / Svelte

Set exportUrl; the toolbar button submits the current request to it (a form POST that triggers a browser download):

const options = { /* ... */ exportUrl: '/api/sales/export' };

grid.exportData();   // programmatic

The client posts a request field (JSON of the current GridRequest) plus the CSRF token. Make sure the endpoint is reachable — for a plain POST route, add it to the CSRF exceptions (see Inertia integration).

Why this beats client-side export

Client-only grids can only export the rows they have loaded. Because Elyra DataGrid streams from the database, you can export the full result set — millions of rows — with constant client and server memory.