Skip to content

Database Compatibility

Bagisto runs on MySQL 8.0, MariaDB 10.11 and PostgreSQL 16, and its continuous integration runs the whole Pest suite and the Playwright suites against all three. A package or customization that only ever ran on MySQL will usually break on PostgreSQL in one of a handful of predictable ways. This page lists the abstractions Bagisto provides so you do not have to write dialect-specific SQL, and the pitfalls those abstractions exist for.

Availability

PostgreSQL support, the db_grammar() helper and everything else on this page belong to Bagisto 2.5. Bagisto 2.4 supports MySQL and MariaDB only and has no db_grammar(); on 2.4 the MySQL syntax shown in the right-hand columns is what you write directly.

Which database is running

Webkul\Core\Enums\SupportedDatabaseEnum names the supported drivers and answers the question at runtime:

php
use Webkul\Core\Enums\SupportedDatabaseEnum;

SupportedDatabaseEnum::MYSQL;    // 'mysql'
SupportedDatabaseEnum::MARIADB;  // 'mariadb'
SupportedDatabaseEnum::PGSQL;    // 'pgsql'

SupportedDatabaseEnum::isPostgres();   // bool
SupportedDatabaseEnum::isMysql();      // true for MySQL and MariaDB
SupportedDatabaseEnum::currentDriver(); // app('db')->getDriverName()

The installer offers the three connections and fills in the default port (3306 or 5432); the .env keys are Laravel's usual DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD and DB_PREFIX.

pdo_pgsql

composer.json requires ext-pdo_mysql but not ext-pdo_pgsql, and the installer's requirement check does not verify a database driver extension. On a PostgreSQL host make sure pdo_pgsql is loaded before installing; CI installs it explicitly.

The db_grammar() helper

Every place Bagisto needs a piece of SQL that differs between dialects goes through db_grammar(), which returns an implementation of Webkul\Core\Contracts\DatabaseGrammar chosen by the connection driver: Webkul\Core\Helpers\Database\Grammar\MySqlGrammar for MySQL and MariaDB, PgSqlGrammar for PostgreSQL. It is bound as a singleton in CoreServiceProvider.

Every method returns a raw SQL string. Wrap it in DB::raw(), orderByRaw() or whereRaw() yourself, prefix table names with DB::getTablePrefix(), and never pass user input into these methods; they interpolate their arguments verbatim. Use bindings for values.

MethodMySQL / MariaDBPostgreSQL
concat(...$parts)CONCAT(a, b)(COALESCE(a,'') || COALESCE(b,''))
concatWs($separator, ...$parts)CONCAT_WS(sep, ...)CONCAT_WS(sep, ...)
groupConcat($column, $separator = ',', $distinct = false, $orderBy = null, $direction = 'ASC')GROUP_CONCAT([DISTINCT] col [ORDER BY …] SEPARATOR sep)STRING_AGG([DISTINCT] NULLIF(col::text,''), sep [ORDER BY …])
findInSet($needle, $column) (deprecated, prefer a pivot table)FIND_IN_SET(n, col)n = ANY(STRING_TO_ARRAY(col, ','))
orderByField($column, array $values)FIELD(col, 1, 2, 3)COALESCE(ARRAY_POSITION(ARRAY[1,2,3], col), n+1)
caseInsensitiveLike()LIKEILIKE
caseSensitiveLike()LIKE BINARYLIKE
castToString($expression, $length = 255)CAST(expr AS CHAR(len))CAST(expr AS VARCHAR(len))
now()NOW()NOW()
dateFormat($column, $format) (MySQL placeholders)DATE_FORMAT(col, fmt)TO_CHAR(col, fmt) with %Y, %m, %d, %H, %i, %s translated
dateDiff($date1, $date2)DATEDIFF(d1, d2)(d1::date - d2::date)
extractDatePart($part, $column)MONTH(col), YEAR(col), …EXTRACT(part FROM col)::integer
monthDay($column)DATE_FORMAT(col, '%m-%d')TO_CHAR(col, 'MM-DD')
fromUnixtime($column)FROM_UNIXTIME(col)TO_TIMESTAMP(col)
jsonExtractText($column, $path)json_unquote(json_extract(col, '$.key'))(col::jsonb->>'key')
jsonExtractNumeric($column, $path)COALESCE(CAST(… AS SIGNED), 0)COALESCE(NULLIF(col::jsonb->>'key','')::bigint, 0)

JSON paths are written in MySQL form ($.key or $."key") and translated for PostgreSQL.

Examples from core:

File: packages/Webkul/Admin/src/DataGrids/Customers/CustomerDataGrid.php

php
$queryBuilder = DB::table('customers')
    ->addSelect(DB::raw(db_grammar()->concat($tablePrefix.'customers.first_name', "' '", $tablePrefix.'customers.last_name').' as full_name'));

$this->addFilter('full_name', DB::raw(db_grammar()->concat($tablePrefix.'customers.first_name', "' '", $tablePrefix.'customers.last_name')));

File: packages/Webkul/Product/src/Jobs/Search/IndexProducts.php

php
->orderByRaw(db_grammar()->orderByField('id', $this->productIds))

File: packages/Webkul/DataTransfer/src/Helpers/Import.php

php
DB::raw('SUM('.db_grammar()->jsonExtractNumeric('summary', '$."created"').') AS created')

What breaks on PostgreSQL, and what to do instead

PitfallWhyDo this
where('name', 'like', …) for a search boxLIKE is case-sensitive on PostgreSQLwhere('name', db_grammar()->caseInsensitiveLike(), …). DataGrid text search already does this through Column::likeOperator()
CAST(x AS CHAR)On PostgreSQL CHAR with no length is one character, so ids become "1"db_grammar()->castToString('x')
A CASE whose branches return different typesPostgreSQL requires one type; MySQL coercesCast both branches to the same type
SELECT a, b … GROUP BY aPostgreSQL requires every non-aggregated selected column in GROUP BYGroup by all of them, as CustomerDataGrid does, or aggregate the rest
DISTINCT combined with ORDER BY on an unselected columnRejected by PostgreSQLCore's price indexer moved from distinct() to groupBy('products.id')
An empty string saved into a date, integer or boolean columnMySQL coerces ''; PostgreSQL raises a type errorAdd a set mutator on the model that turns '' into null or the default. Do not sanitize in the controller; the value has to be right whichever path writes it
Reading a boolean column without a castMySQL returns 1/0, PostgreSQL true/false, so === 1 checks failAdd 'boolean' to the model's $casts
Inserting rows with explicit ids, as seeders doPostgreSQL sequences do not advance past an explicit id, so the next insert collidesUse the Webkul\Core\Concerns\SyncsPostgresSequences trait and call $this->syncPostgresSequences(['categories']) after the insert; it is a no-op on MySQL
Thousands of rows in one insert()PostgreSQL caps a statement at 65 535 bound parametersChunk; Omnibus inserts 500 rows per statement
json() columns queried with ->>Works, but jsonb is what core migrations now use and what the grammar's JSON methods assumePrefer $table->jsonb() in new migrations
Raw MySQL functions in migrations or seeders (NOW(), IF(), CONCAT())Not portableUse the schema builder, or db_grammar() for expressions

Testing against every database

The parallel test databases are created once and not re-migrated, and CI runs the suite on each engine. Locally, point DB_CONNECTION at a PostgreSQL database and run the suite you touched:

bash
DB_CONNECTION=pgsql DB_PORT=5432 vendor/bin/pest --testsuite="Admin Feature Test"

The Testing Workflow page has the full setup; the DataGrid and Repositories pages show where the grammar fits in a package.

Released under the MIT License.