Skip to content

Migration from v1

Muhammet Şafak edited this page May 24, 2026 · 1 revision

Migration from v1

The 2.0.0 line is a deliberate breaking release: bug fixes (including one with SQL-injection implications), a wide architectural refactor, and a hardened security surface. This page walks you through the upgrade.

TL;DR

If your code only calls the public clause-builder methods (select, from, where, andWhere, set, generate*Query, getParameter, …), the upgrade is mostly transparent. The behavioral fixes change a handful of edge cases that v1 silently produced wrong SQL for — most are bugs you wanted fixed.

If you subclass BaseDriver / MySQL / PgSQL / SQLite, or you directly reference the driver classes by name, you'll need to rename. See § Driver renames.

If you parse the structure produced by exportQB(), the shape is unchanged — the structure array still has the same keys and value shapes.

Bump PHP

- "php": ">=8.0"
+ "php": "^8.1"

v2.0.0 requires PHP 8.1+. PHP 8.4 is supported (no more implicit- nullable deprecations).

Composer

- "require": {
-     "initorm/query-builder": "^1.0"
- }
+ "require": {
+     "initorm/query-builder": "^2.0"
+ }

Driver renames

This is the biggest surface for breakage. All four driver classes were renamed and the abstract base too:

v1 (gone) v2
InitORM\QueryBuilder\Drivers\BaseDriver InitORM\QueryBuilder\Drivers\AbstractDriver (now abstract)
InitORM\QueryBuilder\Drivers\MySQL InitORM\QueryBuilder\Drivers\MySqlDriver
InitORM\QueryBuilder\Drivers\PgSQL InitORM\QueryBuilder\Drivers\PostgreSqlDriver
InitORM\QueryBuilder\Drivers\SQLite InitORM\QueryBuilder\Drivers\SqliteDriver
(implicit default in BaseDriver) InitORM\QueryBuilder\Drivers\GenericDriver (explicit no-op driver)

If you only ever passed 'mysql' / 'pgsql' / 'sqlite' as a string to new QueryBuilder(…), you don't need to do anything — the strings still work.

DriverInterface API

- public function escapeIdentify(string &$string): string;
- public function getDriver(): ?string;
+ public function escapeIdentifier(string $identifier): string;
+ public function getName(): ?string;
  • escapeIdentify(&$s) was a by-reference mutator that also returned the value. v2's escapeIdentifier($s) is pure — returns the escaped identifier, never mutates the input.
  • getDriver() was renamed to getName() to clarify what it returns.

If you implemented your own driver, you must:

  1. Rename both methods.
  2. Drop the by-reference parameter on escapeIdentifier.

Identifier hardening

escapeIdentifier() now raises QueryBuilderInvalidArgumentException when the supplied identifier contains ; or --. If you were silently constructing identifiers from user input that contained such characters (you shouldn't have been — that's an injection vector), the call now fails loudly. See Security §V3.

QueryBuilderInterface signature changes

Subtle but breaking for callers that implement the interface directly:

- public function naturalJoin(string|RawQuery $table, string|RawQuery|Closure $onStmt): self;
+ public function naturalJoin(string|RawQuery $table): static;

naturalJoin() no longer takes an $onStmt argument — NATURAL JOIN does not carry an ON clause.

- public function where(RawQuery|string $column, string $operator = '=', ...): self;
- public function having(RawQuery|string $column, string $operator = '=', ...): self;
- public function on(RawQuery|string $column, string $operator = '=', ...): self;
+ public function where(RawQuery|string $column, mixed $operator = '=', ...): static;
+ public function having(RawQuery|string $column, mixed $operator = '=', ...): static;
+ public function on(RawQuery|string $column, mixed $operator = '=', ...): static;

The $operator slot is mixed (not string) — to preserve the value-shortcut where('id', 5) where the second argument is treated as the value when the operator slot is not a SQL operator.

- public function group(Closure $closure): self;
+ public function group(Closure $closure, string $logical = 'AND'): static;

group() now declares the $logical parameter that the implementation always accepted (was implicit before).

All return types went from self to static so subclasses inherit their own static return from chained calls.

Behavioral fixes (LIKELY the SQL you wanted)

Most of these are bugs you might have been working around. Search your codebase for the patterns and decide whether to remove the workaround.

andWhereNotIn — was emitting IN

- // v1: $qb->andWhereNotIn('id', [1, 2])  →  WHERE id IN (1, 2)
+ // v2:                                    →  WHERE id NOT IN (1, 2)

orLike / andLike — connectors were swapped

- // v1: $qb->where(...)->orLike(...)   →   ... AND ...
- //     $qb->where(...)->andLike(...)  →   ... OR ...
+ // v2: $qb->where(...)->orLike(...)   →   ... OR ...
+ //     $qb->where(...)->andLike(...)  →   ... AND ...

orBetween — corrupted its arguments

- // v1: $qb->orBetween('id', 10, 20)  →  malformed array value, AND connector lost
+ // v2:                                →  OR id BETWEEN 10 AND 20

startLike / endLike — wildcard placement was inverted

- // v1: $qb->startLike('name', 'A')  →  LIKE '%A'   (matches "ends with A")
+ // v2:                              →  LIKE 'A%'  (matches "starts with A")

- // v1: $qb->endLike('name', 'A')    →  LIKE 'A%'
+ // v2:                              →  LIKE '%A'

RawQuery Closure form — was dead code

- // v1: new RawQuery(fn ($qb) => $qb->select(...)->from(...))
- //     → TypeError "Closure cannot be converted to string"
+ // v2:                                   → produces the inner SELECT

FIND_IN_SET — was inlining user strings as literal SQL

- // v1: $qb->findInSet('roles', 'admin')   →   FIND_IN_SET(admin, roles)
- //     ↑ "admin" is inlined verbatim — SQL injection vector for user input
+ // v2:                                    →   FIND_IN_SET(:roles, roles)
+ //     ↑ parameterized

where() with boolean values — was collapsing to bare column

- // v1: $qb->where('active', true)   →   WHERE active   (operator was lost)
+ // v2:                              →   WHERE active = :active

where().orWhere() — was silently joining with AND

- // v1: $qb->where('a', 1)->orWhere('b', 2)   →   WHERE a = 1 AND b = 2
+ // v2:                                       →   WHERE a = 1 OR b = 2

If you depended on the buggy AND-collapse, wrap the OR side in group(..., 'OR') for the explicit parenthesized form.

New defense: LIKE wildcard auto-escape

- // v1: $qb->like('name', '50%')   →   param :name = '%50%%'  ≡ LIKE '%50%' (matches anything starting with "50")
+ // v2:                            →   param :name = '%50\%%' (matches only strings containing the literal "50%")

User input containing %, _, or \ is auto-escaped. Pass a RawQuery to opt out:

// v2: keep raw wildcards
$qb->like('name', $qb->raw("'custom%pattern'"));
// LIKE 'custom%pattern'

See Security §V4.

License header simplification

Every source file's license header went from a ~10-line block to a two-line @package / @license pair. The canonical legal text now lives in LICENSE only. This is informational only — it doesn't affect you unless you forked the source.

Internal renames (only matters if you reached inside)

If your code referenced the private __generate* helpers (it shouldn't have — they were private by name and intent):

v1 v2
QueryBuilder::__generateWhereQuery gone — see Compiler\AbstractCompiler::compileWhere
QueryBuilder::__generateHavingQuery gone — see compileHaving
QueryBuilder::__generateLimitQuery gone — see compileLimit
QueryBuilder::__generateSchemaName gone — see compileSchemaName
QueryBuilder::__generateStructure gone — see Helper\BucketCompiler::compile

The compile mechanics now live in dedicated Compiler\* classes. The public QueryBuilder::generate*Query() methods are unchanged.

Step-by-step upgrade

  1. Composer: bump initorm/query-builder to ^2.0 and PHP constraint to ^8.1. Run composer update.
  2. Driver class names: if you reference BaseDriver, MySQL, PgSQL or SQLite directly, search-and-replace per the rename table.
  3. Custom drivers: rename escapeIdentifyescapeIdentifier (drop the by-reference parameter) and getDrivergetName. If you relied on BaseDriver not being abstract, switch to extends AbstractDriver.
  4. Identifier inputs: anywhere you forwarded user input as a table or column name (from($_GET['table']) etc.), wrap it in a whitelist or accept that QueryBuilderInvalidArgumentException may now fire on bad inputs.
  5. LIKE values: if you used like('name', '%foo%') expecting raw wildcards, switch to like('name', 'foo') (the builder adds the wildcards based on $type) or like('name', $qb->raw("'%foo%'")) when you really want raw control.
  6. OR semantics: search for orWhere, orLike, orBetween etc. chains. Pre-v2 they silently behaved as AND; post-v2 they actually OR. Verify the SQL matches your intent.
  7. Run your tests. Particularly anything that asserted the exact emitted SQL — many edge cases will now differ.
  8. Read the CHANGELOG for the complete list.

Help

If something breaks in a way that isn't covered here, please open an issue with a minimal reproduction — we'll either expand this page or fix the regression.


Back to: Home

Clone this wiki locally