WHERE DSL Reference

This reference is for Pionia Shop developers who prefer Medoo-style WHERE arrays over fluent where() when building ProductService filters. Every operator below applies to products, projects, and customers queries on the shop’s port 8000 stack.

What you will learn

  • Write comparison, LIKE, IN, and NULL operators in WHERE arrays
  • Nest AND / OR groups for complex board filters
  • Embed raw SQL fragments safely with Piql::raw()
Before you start

How it works

WHERE array  →  Piql parser  →  parameterized SQL + binds
table('products')->filter([...])  |  ->where([...])  |  Agg / Where builders

Porm passes WHERE arrays to Piql, which implements a Medoo-compatible DSL. Use them in where(), filter(), all($where), get($where), aggregates, and Agg / Where builders.

Prefer the fluent where() syntax for readability: where('status', 'open'), where('title', 'starts_with', 'Desk'), etc.

Equality (default)

['status' => 'open', 'project_id' => 1]
// status = 'open' AND project_id = 1

Comparison operators

Prefix the column with the operator:

SyntaxSQL
'priority[>]' => 2priority > 2
'priority[>=]' => 2priority >= 2
'sort_order[<]' => 10sort_order < 10
'sort_order[<=]' => 10sort_order <= 10
'status[!]' => 'done'status != 'done' or IS NOT NULL
'priority[<>]' => [1, 3]BETWEEN 1 AND 3
'priority[><]' => [1, 3]NOT BETWEEN
table('products')->filter(['priority[>]' => 2, 'sort_order[<=]' => 100])->all();

LIKE

SyntaxMeaning
'title[~]' => 'desk'LIKE '%desk%'
'title[!~]' => 'draft'NOT LIKE
'title[~]' => ['desk', 'flow']OR of LIKEs

IN / NOT IN

['id' => [1, 2, 3]]           // IN (1,2,3)
['status[!]' => ['archived']]  // NOT IN

NULL

['completed_at' => null]   // IS NULL
['assignee_id[!]' => null]    // IS NOT NULL

AND / OR groups

[
    'AND' => [
        'status' => 'open',
        'OR' => [
            'priority' => 1,
            'priority' => 2,
        ],
    ],
]

Top-level keys in a flat array are AND-ed. Use explicit AND / OR keys for nesting.

ORDER, LIMIT, GROUP, HAVING

On a Builder chain, prefer fluent methods. In raw WHERE arrays:

KeyExample
ORDER['ORDER' => ['created_at' => 'DESC']]
LIMIT['LIMIT' => 10] or ['LIMIT' => [20, 10]] (offset, limit)
GROUP['GROUP' => 'project_id']
HAVING['HAVING' => ['count[>]' => 5]]

startAt($offset) on the Builder requires a prior limit() call — it maps to LIMIT [$offset, $limit].

FULLTEXT — match()

On the Builder:

table('products')
    ->filter()
    ->match('title,description', 'pionia-shop sprint', 'natural')
    ->all();

Adds a MATCH … AGAINST style clause (driver-dependent).

Column aliases & casts

Select with alias: 'column(alias)' in columns().

Cast on read: suffix [Int], [Bool], [Number], [String], [JSON], [Object]:

table('products')->columns(['metadata[JSON]'])->get(1);

[Object] uses PHP unserialize() — only use with trusted database content.

Programmatic builders

Where builder

use Pionia\Porm\Database\Builders\Where;

$clause = Where::builder()
    ->and(['priority[>]' => 2])
    ->or(['status' => 'blocked'])
    ->build();

table('products')->filter($clause)->all();

Agg builder

For HAVING-style expressions, comparisons in WHERE, and computed columns — see Aggregation.

Raw fragments

use Pionia\Porm\Core\Piql;

table('products')->filter([
    'created_at[>]' => Piql::raw('DATE_SUB(NOW(), INTERVAL 7 DAY)'),
])->all();

See also Transactions & raw SQL.

Common mistakes

  • Passing user search text into Piql::raw() — use bound where() or [~] LIKE operators for Pionia Shop search boxes.
  • Duplicate keys inside OR arrays — PHP keeps only the last value; nest groups or use whereIn().
  • Using [Object] casts on untrusted columns — never on client-supplied JSON in Pionia Shop staging data.
  • Embedding ORDER/LIMIT in WHERE when a Builder exists — prefer orderBy() / limit() on filter() chains in services.

What’s next

Filtering

Fluent where() on the Builder.

Aggregation

Agg builder for HAVING.

Transactions

Raw SQL and Piql::raw().