Download the PHP package vuthaihoc/laravel-db-portable without Composer
On this page you can find all versions of the php package vuthaihoc/laravel-db-portable. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download vuthaihoc/laravel-db-portable
More information about vuthaihoc/laravel-db-portable
Files in vuthaihoc/laravel-db-portable
Package laravel-db-portable
Short Description Portable query builder helpers for JSON numbers, NULLS LAST and JSON counters across PostgreSQL/CockroachDB, MySQL/MariaDB/MatrixOne and SQLite, plus scan, audit and copy tools for switching database drivers
License MIT
Homepage https://github.com/vuthaihoc/laravel-db-portable
Informations about the package laravel-db-portable
Laravel DB Portable
Query builder helpers that compile for PostgreSQL / CockroachDB, MySQL / MariaDB / MatrixOne and SQLite, plus three Artisan commands for switching an application from one database to another:
db-portable:scanfinds database-specific SQL in your code;db-portable:auditchecks that the data of one connection fits the schema of another;db-portable:copycopies the rows.
It grew out of moving a Laravel application from CockroachDB to MatrixOne. Most of the changes that move needed were raw SQL written for one database (plan_data->>'amount', ::bigint, NULLS LAST, jsonb_set), and integer columns that were wider on the old database than on the new one.
Installation
Requires PHP 8.2+ and Laravel 12 or 13. The service provider is discovered automatically. The package works with Laravel's own drivers (PostgreSQL, MySQL, MariaDB, SQLite) and with these third-party drivers:
| Database | Driver | Install |
|---|---|---|
| MatrixOne | vuthaihoc/laravel-matrixone | composer require vuthaihoc/laravel-matrixone |
| CockroachDB | vuthaihoc/cockroachdb-laravel | composer require vuthaihoc/cockroachdb-laravel |
The API may still change before 1.0: pin a minor version (^0.3).
Laravel already covers a lot
Prefer Laravel's own methods where they exist. They compile for every database:
| Instead of | Write |
|---|---|
whereRaw("flags->>'device' = ?", [$d]) |
where('flags->device', $d) |
whereRaw("(flags->>'sync')::bool is true") |
where('flags->sync', true) |
whereRaw("jsonb_array_length(meta->'tags') > 0") |
whereJsonLength('meta->tags', '>', 0) |
whereRaw("meta @> ?", [...]) |
whereJsonContains('meta', [...]) |
update(['meta' => DB::raw("jsonb_set(...)")]) with a fixed value |
update(['meta->key' => $value]) |
whereRaw('tags::text like ?'), ilike |
whereLike('tags', $pattern) (case-insensitive by default) |
Query builder macros
Laravel reads JSON keys as text, so numbers compare and sort as strings on PostgreSQL ("10" < "2", sum(text) fails). These macros cast per database:
The macros are registered on the query builder and on Eloquent builders. Through Eloquent, incrementJson() also updates updated_at, like increment().
What they compile to:
| Macro | PostgreSQL / CockroachDB | MySQL / MariaDB / MatrixOne | SQLite |
|---|---|---|---|
| JSON number | (col->>'k')::numeric |
cast(json_unquote(json_extract(col, '$."k"')) as double) |
cast(json_extract(col, '$."k"') as real) |
| JSON boolean | (col->>'k')::boolean |
json_unquote(json_extract(...)) = 'true' |
json_extract(...) = 1 |
desc nulls last |
x desc nulls last |
x desc (NULLs already last) |
x desc nulls last |
asc nulls last |
x asc nulls last |
(x) is null, x asc |
x asc nulls last |
| JSON increment | jsonb_set(<col if object, else '{}'>, '{k}', to_jsonb(... + n), true) |
json_set(<col if object, else json_object()>, '$."k"', ... + n) |
json_set(<col if object, else '{}'>, '$."k"', ... + n) |
Dashboards: conditional aggregates and subtotals
| PostgreSQL | CockroachDB, SQLite | MySQL, MariaDB, MatrixOne | |
|---|---|---|---|
selectCountWhere() / selectSumWhere() / selectAggregateWhere() |
count(case when … then 1 end), sum(case when … then col end) |
same | same |
rollup() |
group by rollup (…) |
union all of one query per grouping level |
group by … with rollup |
On CockroachDB and SQLite, rollup() cannot be combined with having(), limit() or offset(), and the groupBy() columns must be selected by name.
Stale and historical reads
Named by intent; the drivers compile them:
| CockroachDB | MatrixOne | PostgreSQL, MySQL, MariaDB | SQLite | |
|---|---|---|---|---|
readStale() |
follower read (AS OF SYSTEM TIME follower_read_timestamp(), about 4.8 s old) |
no change: reads do not contend with writes | no change: Laravel already reads from the read connection when one is configured |
no change |
asOfTime($time) |
AS OF SYSTEM TIME |
{as of timestamp '...'} (in the connection's time zone) |
skipped with a warning (throws with db-portable.strict) |
same |
readCurrent() |
removes it | removes it | no change | no change |
They need the drivers' historical reads: vuthaihoc/cockroachdb-laravel 2.2.2+ and vuthaihoc/laravel-matrixone. CockroachDB does not accept them in subqueries or inside a transaction (the driver then reads current data). The time read must be after the table was created, and within the database's history retention (MatrixOne: PITR or garbage-collection window).
For raw query parts, Portable returns the same expressions:
Search boxes and full-text relevance
suggest() returns values starting with the search and, from 3 characters, values containing it (or similar to it, with trigrams): prefix matches first, then the most similar, then the shortest.
| CockroachDB | PostgreSQL | MatrixOne | MySQL, MariaDB | SQLite | |
|---|---|---|---|---|---|
whereStartsWith(), whereContains() |
ilike, trigram index |
ilike, trigram index |
ilike |
like (the _ci collation) |
like (ASCII case only) |
unaccent: true |
unaccent(lower(col)) |
unaccent() (extension) |
no effect: accents count | the collation decides | no effect |
whereSimilar(), orderBySimilarity() |
% and similarity() (driver) |
% and similarity() (pg_trgm) |
contains; score 1 prefix / 0.5 contains (warning) | same as MatrixOne | same as MatrixOne |
searchFullText(), *FullTextRelevance() |
ts_rank (driver) |
ts_rank |
match ... against (driver) |
match ... against |
no whereFullText(); relevance 0 (warning) |
The drivers (cockroachdb-laravel 2.3+, laravel-matrixone 1.1+) implement these methods themselves; the macros cover the other databases. MatrixOne has no typo-tolerant search: its ngram parser splits only CJK text into n-grams. The trigram threshold of % is the session's pg_trgm.similarity_threshold (0.3): set it with the connection's variables option on CockroachDB.
Migrations
Blueprint macros for schema features that differ between databases. When a database has no equivalent, the macro skips the feature and logs a warning. Set config(['db-portable.strict' => true]) to throw instead.
| Macro | PostgreSQL / CockroachDB | MySQL | MariaDB | MatrixOne | SQLite |
|---|---|---|---|---|---|
jsonWithDefault() |
default '[]' |
default ('[]') |
default ('[]') |
skipped: the column is nullable, set the default in the model's $attributes |
default '[]' |
jsonIndex() |
using gin (use jsonb() on PostgreSQL) |
skipped | skipped | skipped | skipped |
descIndex() |
(col desc) |
(col desc) |
(col desc) |
accepted, built ascending | (col desc) |
jsonKeyIndex() |
((col->>'key')) |
functional index ((cast(... as char(255)) collate utf8mb4_bin)) (8.0.13+) |
skipped | skipped (no expression indexes) | ((json_extract(...))) |
coveringIndex() |
(cols) include (extra) (CockroachDB's STORING) |
plain index on cols |
plain index on cols |
plain index on cols |
plain index on cols |
trigramIndex() |
using gin (col gin_trgm_ops); unaccent: true: (unaccent(lower(col)) gin_trgm_ops) on CockroachDB, the column on PostgreSQL (warning) |
fulltext with parser ngram |
fulltext | fulltext with parser ngram (CJK n-grams, whole words otherwise) |
skipped |
partialIndex() |
(cols) where ... |
plain index, condition dropped | plain index, condition dropped | plain index, condition dropped | (cols) where ... |
Skipped features and dropped conditions log a warning (or throw with db-portable.strict). forDriver() runs the callback of the connection's driver or family and emits nothing by itself.
The where condition of partialIndex() is raw SQL: keep it portable (deleted_at is null, status = 'active').
Search indexes for Scout models
db-portable:search-indexes reads the Scout attributes of your models and checks that their tables have the indexes the database engines need (SCOUT_DRIVER=database, crdb or matrixone), or writes a migration creating them:
Declared on toSearchableArray() |
CockroachDB / PostgreSQL | MatrixOne | MySQL | SQLite |
|---|---|---|---|---|
#[SearchUsingFullText(cols, ['language' => ...])] |
fullText(cols)->language(...), matching whereFullText() |
fullText(cols): required, MATCH fails without it |
fullText(cols) |
none |
#[SearchUsingFuzzy(cols, unaccent: ...)] (cockroachdb-laravel 2.4) |
trigramIndex(col, unaccent: ...) |
none (no trigram similarity) | none | none |
#[SearchUsingPrefix(cols)] |
trigramIndex(col) (serves ilike 'x%') |
index(col) |
index(col) |
index(col) |
other columns, with --like |
trigramIndex(col) |
none (LIKE '%x%' cannot use an index) |
none | none |
toSearchableEmbedding() |
vectorIndex(embedding) |
vectorIndex(embedding) |
none | none |
- Without
--migrationthe command lists every index asok,missing,outdatedorskipped(with the reason) and fails when one is missing: usable as a CI check. - Existing indexes are recognized by their definition, whatever their name (e.g. a hand-written
using gin (word gin_trgm_ops)). outdated: a CockroachDB full-text index made before cockroachdb-laravel 2.3 (nocoalesce()) or with another language; the migration drops it first.- MatrixOne: a table with its own foreign keys or a column already in another FULLTEXT index is reported instead of migrated (4.2.4 crashes on inserts into a table with both a FULLTEXT index and a foreign key; one FULLTEXT index per column).
- The migration is a regular file in
database/migrations(or--path), withdown(): review and commit it.
Switching databases
A suggested workflow, e.g. from CockroachDB (crdb) to MatrixOne (matrixone):
- Scan the code for SQL the new database will reject, and rewrite it with the methods above.
- Migrate the new database:
php artisan migrate --database=matrixone. - Audit the data against the new schema, and widen the columns it reports.
- Copy the data.
- Point
DB_CONNECTIONat the new database and run your test suite.
Scan
The scanner reads the string literals of your PHP files (not comments or code) and reports each construct with the families it breaks on and a replacement:
Targets: mysql (MySQL, MariaDB), matrixone, pgsql (PostgreSQL, CockroachDB), sqlite. Code that already branches per driver is still reported, because the scanner cannot tell which branch runs.
Audit
It reports tables and columns missing from the target, integers outside the target column's range, and strings longer than the target varchar(n). It runs one min/max query per table on the source.
Copy
- Copies the columns present on both sides and skips
migrations(change it with--except). - Reads in primary-key order (keyset pagination,
--chunk=500).--resumestarts after the highest key already in the target. - Converts values for the target: timestamps with a time zone offset become UTC for MySQL-family
datetimecolumns, booleans match the target type, and arrays are encoded as JSON. - Disables foreign key checks on MySQL-family and SQLite targets while copying. Rows are inserted with
insertOrIgnore(), so a rerun does not duplicate them.
Testing
The Unit suite needs no server. The Conformance suite runs the same assertions on SQLite (in memory), MatrixOne and CockroachDB, and skips a server that is not reachable:
Override the servers with MATRIXONE_HOST, MATRIXONE_PORT, CRDB_HOST, CRDB_PORT (see phpunit.xml.dist). To test against local checkouts of the drivers, add path repositories to a local copy of composer.json ("repositories": [{"type": "path", "url": "../laravel-matrixone"}]) and require them as @dev.
License
MIT. See LICENSE.
All versions of laravel-db-portable with dependencies
illuminate/console Version ^12.0 || ^13.0
illuminate/database Version ^12.0 || ^13.0
illuminate/support Version ^12.0 || ^13.0