Skip to content

Commit fb0b086

Browse files
authored
Merge pull request #11 from utopia-php/feat/clickhouse-insert-delete-settings-mv
feat(clickhouse): INSERT FORMAT, DELETE forms+settings, MV, groupByTimeBucket, typed bindings
2 parents 5c8bba8 + 0af2688 commit fb0b086

32 files changed

Lines changed: 1616 additions & 100 deletions

README.md

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1472,7 +1472,55 @@ Query::containsString('tags', ['php']); // position(`tags`, ?) > 0
14721472

14731473
**Regex** — uses `match()` function instead of `REGEXP`.
14741474

1475-
**UPDATE/DELETE** — compiles to `ALTER TABLE ... UPDATE/DELETE` with mandatory WHERE:
1475+
**Time bucketing** — groups rows into fixed-width windows on a timestamp column. Allowed intervals: `1m`, `5m`, `15m`, `1h`, `1d`, `1w`, `1M`. Compiles to `toStartOfMinute / toStartOfFiveMinutes / toStartOfFifteenMinutes / toStartOfHour / toStartOfDay / toStartOfWeek / toStartOfMonth`:
1476+
1477+
```php
1478+
$result = (new Builder())
1479+
->from('events')
1480+
->selectRaw('toStartOfHour(`time`) AS `bucket`')
1481+
->count('*', 'cnt')
1482+
->groupByTimeBucket('time', '1h')
1483+
->orderByRaw('`bucket` ASC')
1484+
->build();
1485+
1486+
// SELECT COUNT(*) AS `cnt`, toStartOfHour(`time`) AS `bucket`
1487+
// FROM `events`
1488+
// GROUP BY toStartOfHour(`time`)
1489+
// ORDER BY `bucket` ASC
1490+
```
1491+
1492+
Other dialects throw `UnsupportedException` from `compileGroupByTimeBucket`. Re-emit the bucket function via `selectRaw` / `orderByRaw` when you need to reference it in the SELECT list or ORDER BY (same pattern as `groupByRaw`).
1493+
1494+
**Named-typed bindings** — opt into ClickHouse `{name:Type}` placeholders for safe parameterization over the HTTP interface. Off by default; positional `?` placeholders remain the default and behave identically to every other dialect:
1495+
1496+
```php
1497+
$result = (new Builder())
1498+
->useNamedBindings()
1499+
->withParamTypes([
1500+
'time' => 'DateTime64(3)',
1501+
'tenant' => 'String',
1502+
'value' => 'Int64',
1503+
])
1504+
->from('events')
1505+
->filter([
1506+
Query::greaterThan('time', '2024-01-01 00:00:00'),
1507+
Query::equal('tenant', ['acme']),
1508+
Query::lessThanEqual('value', 100),
1509+
])
1510+
->build();
1511+
1512+
// SELECT * FROM `events`
1513+
// WHERE `time` > {param0:DateTime64(3)}
1514+
// AND `tenant` IN ({param1:String})
1515+
// AND `value` <= {param2:Int64}
1516+
1517+
$result->namedBindings;
1518+
// ['param0' => '2024-01-01 00:00:00', 'param1' => 'acme', 'param2' => 100]
1519+
```
1520+
1521+
Unregistered columns fall through to value-based inference: `int → Int64`, `float → Float64`, `bool → UInt8`, `null → Nullable(String)`, `DateTimeInterface → DateTime64(3)`, everything else → `String`. Register types via `withParamType($column, $type)` or `withParamTypes($map)` whenever the inference rule doesn't match the column's ClickHouse declaration. The positional `$bindings` array is still exposed on the resulting `Statement` for callers that prefer it.
1522+
1523+
**UPDATE** — compiles to `ALTER TABLE ... UPDATE` with mandatory WHERE:
14761524

14771525
```php
14781526
$result = (new Builder())
@@ -1484,6 +1532,31 @@ $result = (new Builder())
14841532
// ALTER TABLE `events` UPDATE `status` = ? WHERE `created_at` < ?
14851533
```
14861534

1535+
**DELETE** — two forms. `delete()` defaults to the lightweight `DELETE FROM …` form, which marks rows deleted via a mask and is async by default. Opt into the heavier mutation form (`ALTER TABLE … DELETE`) when you need parts rewritten on disk; the two are not interchangeable, so the builder never auto-translates between them.
1536+
1537+
```php
1538+
// Lightweight (default) — pair with `lightweight_deletes_sync = 0` for async
1539+
$result = (new Builder())
1540+
->from('audit_log')
1541+
->settings(['lightweight_deletes_sync' => '0'])
1542+
->filter([Query::lessThan('time', '2024-01-01 00:00:00')])
1543+
->delete();
1544+
1545+
// DELETE FROM `audit_log` WHERE `time` < ? SETTINGS lightweight_deletes_sync=0
1546+
1547+
// Mutation — opt in. Pair with `mutations_sync = 0` for async
1548+
$result = (new Builder())
1549+
->from('audit_log')
1550+
->deleteMode(Builder::DELETE_MODE_MUTATION)
1551+
->settings(['mutations_sync' => '0'])
1552+
->filter([Query::lessThan('time', '2024-01-01 00:00:00')])
1553+
->delete();
1554+
1555+
// ALTER TABLE `audit_log` DELETE WHERE `time` < ? SETTINGS mutations_sync=0
1556+
```
1557+
1558+
The trailing `SETTINGS` clause is whatever the caller registers via `settings()` — the builder does not auto-pair a sync setting to a chosen delete mode.
1559+
14871560
> **Note:** Full-text search (`Query::search()`) is not supported in ClickHouse and throws `UnsupportedException`. The ClickHouse builder also forces all join filter hook conditions to WHERE placement, since ClickHouse does not support subqueries in JOIN ON.
14881561

14891562
### MongoDB
@@ -1726,6 +1799,8 @@ Unsupported features are not on the class — consumers type-hint the interface
17261799
| ARRAY JOIN | | | | | | | x | |
17271800
| ASOF JOIN (typed operator) | | | | | | | x | |
17281801
| WITH FILL | | | | | | | x | |
1802+
| `groupByTimeBucket` | | | | | | | x | |
1803+
| Named-typed `{name:Type}` bindings | | | | | | | x | |
17291804
| Approximate Aggregates (incl. `quantiles`) | | | | | | | x | |
17301805
| Upsert (Mongo-style) | | | | | | | | x |
17311806
| Full-Text Search (Mongo) | | | | | | | | x |

0 commit comments

Comments
 (0)