Skip to content

refactor(audit): migrate the ClickHouse adapter to utopia-php/query 0.6 - #91

Merged
lohanidamodar merged 8 commits into
mainfrom
feat/audit-utopia-query-0.3.x
Aug 25, 2026
Merged

refactor(audit): migrate the ClickHouse adapter to utopia-php/query 0.6#91
lohanidamodar merged 8 commits into
mainfrom
feat/audit-utopia-query-0.3.x

Conversation

@lohanidamodar

@lohanidamodar lohanidamodar commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Supersedes utopia-php/audit#120.

utopia-php/audit is now a read-only subtree-split mirror of this monorepo, so the work has to land here. Per docs/distribution.md: "The distribution repositories become read-only mirrors: archive their open PRs, enable branch protection, and point contributors to the monorepo."

The mirror's origin/main was byte-identical to packages/audit/, so this is a direct transplant of that PR's net diff into packages/audit/, reconciled with monorepo layout and tooling.

What this does

Migrates the ClickHouse adapter from hand-assembled SQL to the utopia-php/query 0.3 schema/builder API.

  • setup() emits its DDL through Utopia\Query\Schema\ClickHouse: column types, LowCardinality(...) / Nullable(...) wrapping, bloom-filter indexes, engine, ORDER BY, PARTITION BY and SETTINGS all come from the schema builder. The retention MODIFY TTL / REMOVE TTL statements are unchanged.
  • find(), count(), getById(), createBatch() and cleanup() build their SQL through Utopia\Query\Builder\ClickHouse. Positional bindings become typed {paramN:Type} ClickHouse placeholders, derived from a column-to-type map built from getAttributes().
  • createBatch() uses bulkInsert(Format::JSONEachRow, ...) for the INSERT ... FORMAT JSONEachRow envelope and body instead of assembling the payload by hand.
  • cleanup() uses a lightweight DELETE FROM.
  • Query::getMethod() returns the Utopia\Query\Method enum upstream in 0.3, so Database::count() compares against enum cases. Utopia\Audit\Query keeps exposing the legacy TYPE_* string constants, which map to the same values.
  • Adds tests/Audit/Adapter/ClickHouseSqlSnapshotTest.php — a server-free SQL snapshot suite that pins the emitted DDL/INSERT/DELETE/SELECT shapes so a future query-lib bump cannot quietly change adapter SQL. It joins the unit testsuite and is excluded from e2e.

Filter semantics are unchanged: contains / notContains remain substring matches, now compiled to position(col, ?) > 0 / = 0 rather than LIKE '%needle%', which also removes the need for wildcard escaping.

Dependency bump

packages/audit/composer.json: utopia-php/query 0.1.* -> 0.3.*, locked at the tagged 0.3.3. No dev-branch pins. packages/audit/composer.lock regenerated; the result is byte-identical to the lock the mirror PR carried.

packages/audit is the only package in the monorepo that requires utopia-php/query, so the bump cannot conflict with a sibling.

Reconciliations with monorepo layout

  • Dropped the mirror PR's .gitignore change (adding /.phpunit.cache/) — the monorepo root .gitignore already ignores it.
  • Applied the monorepo's Rector rules to the ported code (bin/monorepo check audit --fix): ChangeOrIfContinueToMultiContinueRector in Database::count(), NewMethodCallWithoutParenthesesRector, and declare(strict_types=1) / final / assertSame on the new snapshot test.
  • No pint.json, phpstan.neon or CI workflow copies from the standalone repo were reintroduced; root pint.json / phpstan.neon stay the authority and packages/audit/phpstan.neon keeps its ../../phpstan.neon include. packages/audit/.github/workflows/mirror.yml is untouched.
  • docker-compose.yml unchanged; phpunit.xml only gains the snapshot-test entries in the existing unit / e2e testsuites.

Test plan

Run on PHP 8.5.8 with the monorepo toolchain.

  • bin/monorepo check audit — pint passed, PHPStan [OK] No errors, Rector [OK] Rector is done!, all checks passed.
  • bin/monorepo validateall packages valid.
  • packages/audit unit suite (composer test, i.e. phpunit --testsuite unit) — Tests: 22, Assertions: 106, Deprecations: 3, no failures. The three deprecations are Utopia\Query\Query::contains() in 0.3 pointing at containsString() / containsAny(); behaviour is unchanged and switching call sites is out of scope here.

The e2e tier could not run locally — no Docker daemon was available, so composer test:e2e and its compose services (ClickHouse, MariaDB) could not be started. Utopia\Tests\Audit\Adapter\ClickHouseTest and Utopia\Tests\Audit\Adapter\DatabaseTest were therefore unverified on my machine.

CI has since covered that gap: the test (audit) job brought up clickhouse/clickhouse-server:25.11-alpine and mariadb:10.11 and ran the e2e suite green — Tests: 91, Assertions: 979, Deprecations: 8, no failures.

Note on the mirror

utopia-php/audit's feat/utopia-query-0.3.x branch is deliberately left in place at c1aefabappwrite-labs/cloud#3965 currently pins utopia-php/audit: dev-feat/utopia-query-0.3.x and its lock references that commit. Only the PR is closed.

Moves the audit package from utopia-php/query 0.1.* to the 0.3 line
(locked at 0.3.3) and refreshes packages/audit/composer.lock.
setup() emits its DDL through Utopia\Query\Schema\ClickHouse instead of
hand-assembled SQL: column types, LowCardinality/Nullable wrapping,
bloom-filter indexes, engine, ORDER BY, PARTITION BY and SETTINGS all come
from the schema builder. The retention MODIFY TTL / REMOVE TTL statements
are unchanged.

find(), count(), getById(), createBatch() and cleanup() build their SQL
through Utopia\Query\Builder\ClickHouse. Positional bindings become typed
{paramN:Type} placeholders derived from getAttributes(). createBatch() uses
bulkInsert(Format::JSONEachRow, ...) for the INSERT envelope and body, and
cleanup() uses a lightweight DELETE FROM.

Query::getMethod() now returns the Utopia\Query\Method enum upstream, so
Database::count() compares against enum cases; Utopia\Audit\Query keeps
exposing the legacy TYPE_* string constants.

Filter semantics are unchanged: contains/notContains stay substring matches,
now compiled to position(col, ?) > 0 / = 0 instead of LIKE '%needle%', which
also drops the wildcard escaping.

Adds tests/Audit/Adapter/ClickHouseSqlSnapshotTest.php, a server-free SQL
snapshot suite pinning the emitted DDL/INSERT/DELETE/SELECT shapes; it runs
in the unit testsuite and is excluded from e2e.
@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR migrates the audit package’s ClickHouse adapter to utopia-php/query 0.3.

  • Replaces manually assembled DDL and data queries with the ClickHouse schema and query builders.
  • Introduces typed named bindings and builder-based bulk inserts, selects, counts, deletes, and cursor ordering.
  • Updates the query dependency and enum handling.
  • Adds server-free SQL-shape snapshots and updates the audit package test suites.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/audit/src/Audit/Adapter/ClickHouse.php Migrates schema creation and operational SQL generation to the query library’s ClickHouse builders.
packages/audit/src/Audit/Adapter/Database.php Updates query-method comparisons for the new Method enum contract.
packages/audit/src/Audit/Query.php Preserves legacy audit query constants while adopting the upstream enum-returning API.
packages/audit/tests/Audit/Adapter/ClickHouseSqlSnapshotTest.php Adds builder-level snapshots for representative ClickHouse SQL shapes.
packages/audit/composer.json Raises the utopia-php/query dependency from 0.1.x to 0.3.x.
packages/audit/composer.lock Locks query 0.3.3 and refreshes resolved transitive dependencies.

Reviews (3): Last reviewed commit: "Merge origin/main into feat/audit-utopia..." | Re-trigger Greptile

Comment thread packages/audit/tests/Audit/Adapter/ClickHouseSqlSnapshotTest.php
…ry-0.3.x

# Conflicts:
#	packages/audit/composer.lock
Conflict was packages/audit/composer.json: main bumped utopia-php/validators to
^0.5 while this branch holds utopia-php/query at 0.3.*. Kept both.

The lock needed more than the usual one-line regeneration. Main's own lock is
stale against its composer.json - it declares validators ^0.5 but locks 0.3.1,
which is unsatisfiable because utopia-php/database 7.0.0 requires validators
0.3.*. Resolving that pulls database to 7.2.2 and 15 packages move against
main's lock. Only utopia-php/query 0.1.1 -> 0.3.3 is this branch's doing; the
rest is main's declared requirement catching up with its lock.

bin/monorepo validate: all packages valid. bin/monorepo check audit (what CI
runs): pint, phpstan [OK] No errors, rector [OK].
Review flagged that these snapshots reconstruct the schema and builder calls
instead of invoking the adapter, so an adapter-level mistake would not turn them
red. That is accurate, and the class docblock claimed more than the tests
deliver by describing them as pinning 'the SQL emitted by the adapter paths'.

The coverage itself is not missing: ClickHouseTest drives the real adapter -
setup, log, find, count, cleanup - against a live ClickHouse in CI, 76 tests
across it and the AuditBase trait. The docblock now states the split explicitly
so the next reader does not mistake a green snapshot run for adapter coverage.
The branch was pinned at 0.3.* while query had released 0.4, 0.5 and 0.6. The
adapter needed no changes: 0.4's Parser -> Classifier rename and its
UnsupportedException removal are symbols this package never used, 0.5's nested
join conditions are unused, and 0.6's Schema\Order left the index() call
compatible.

The one thing 0.6 did surface was Query::contains() being deprecated in favour
of containsString() for substring matching and containsAny() for array
attributes. Only tests called the deprecated factory - the adapter switches on
Method::Contains, which is unchanged - and containsString() returns that same
Method, so the swap is behaviour-preserving. Verified rather than assumed: the
SQL snapshots hold at the same 37 assertions and the deprecation notices are
gone.

bin/monorepo check audit: pint, phpstan [OK] No errors, rector [OK]. Snapshot
and query suites green. The ClickHouse integration suite needs a live server and
runs in CI.
@lohanidamodar lohanidamodar changed the title refactor(audit): migrate the ClickHouse adapter to utopia-php/query 0.3 refactor(audit): migrate the ClickHouse adapter to utopia-php/query 0.6 Aug 25, 2026
@lohanidamodar

Copy link
Copy Markdown
Contributor Author

Now on utopia-php/query 0.6, and ready for review

Both this PR and its sibling were pinned at 0.3.* while query had released 0.4, 0.5 and 0.6. All three consumers — audit, usage and cloud — now move together, because composer resolves one utopia-php/query for the whole tree: leaving one behind either fails to resolve or silently picks a version none of them were tested against.

No adapter changes were needed. The breaking changes in between are all symbols these packages don't use:

release change exposure here
0.4.0 ParserClassifier rename none
0.4.0 UnsupportedException replaced by typed capability none
0.4.0 stops silently dropping input a dialect can't honour none
0.5.0 nested join ON conditions none
0.6.0 Schema\Order for index directions index() call still compatible

The one thing 0.6 surfaced was Query::contains() being deprecated in favour of containsString() (substring) and containsAny() (array attributes). Only tests called the deprecated factory — the adapters switch on Method::Contains, which is unchanged — and containsString() returns that same Method, so the swap emits identical SQL. Verified rather than assumed: the SQL snapshots hold at the same assertion count with the deprecation notices gone.

CI is green on 0.6 including the ClickHouse integration suites, which is what actually exercises the emitted SQL against a server.

Release order this unblocks: merge and tag here, then cloud swaps its dev-… branch aliases for real version constraints. Cloud is currently pinned to the dev branches of both packages, so it cannot merge until these release.

Comment thread packages/audit/src/Audit/Adapter/ClickHouse.php Outdated
@@ -1450,208 +1520,33 @@ private function parseQueries(array $queries): array
// otherwise turn `Query::contains('attr', [])` into a full-table
// match instead of an empty result.
if (\in_array($method, self::VALUE_REQUIRED_METHODS, true) && $values === []) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$values === [] is this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed — this is the one of the five I'd push back on.

VALUE_REQUIRED_METHODS covers the filters that are meaningless without a value (equal, contains, between, …). Without the guard, Query::contains('event', []) compiles to a WHERE fragment with nothing in it, which ClickHouse reads as match everything — so a filter the caller asked for silently doesn't apply and they get the full table back instead of an empty result. A wrong answer that looks like a working query, rather than an error.

isNull / isNotNull are deliberately not in that list, since taking no values is correct for them.

This isn't hypothetical: the sibling usage PR shipped exactly this shape, and review found Method::ContainsAny falling out of the Database adapter's switch with no arm and no default — the predicate contributed no SQL and the query returned every row. Both adapters there now refuse instead. The guard here is the same defence one level down, and it mirrors Validator/Query/Filter.php in utopia-php/database, so the two libraries reject the same input.

Comment thread packages/audit/CHANGELOG.md Outdated
*/
private function normalizeFilterValues(string $attribute, array $values): array
{
if ($this->getParamType($attribute) !== 'DateTime64(3)') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we use getColumnTypeMap here? whats the difference between getParamType and getColumnTypeMap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — they were two type tables that could disagree, and getParamType() now derives from getColumnTypeMap() in f772c93c.

The difference was:

  • getColumnTypeMap() builds from getAttributes(), so every attribute with type === VAR_DATETIME gets DateTime64(3), plus id, tenant (when shared) and the limit/offset/max binding slots. It is what the builder is handed via withParamTypes().
  • getParamType() was a hardcoded match recognising only 'time' and 'tenant'.

Today they agree because time is the only datetime attribute. Add a second one and the same column would be bound as String by getParamType() and declared DateTime64(3) in the builder's map — the kind of mismatch that shows up as a ClickHouse parse error at the top of a range rather than in a test. One map now, getColumnTypeMap()[$attribute] ?? 'String'.

Comment thread packages/audit/src/Audit/Adapter/ClickHouse.php Outdated
…he type maps, bind the tenant filter

Four of Chirag's five comments were actionable.

mapAttributeType() had one call site and held a one-line ternary. AGENTS.md is
explicit that a helper whose only job is to stop a short expression appearing
twice should be inlined, so it is.

getParamType() and getColumnTypeMap() were two type tables that could disagree.
getColumnTypeMap() derives from getAttributes(), typing every VAR_DATETIME
attribute as DateTime64(3); getParamType() hardcoded a match that recognised
only 'time'. A second datetime column would have been bound as String by one and
declared DateTime64(3) by the other. getParamType() now reads the same map.

getTenantFilter() returned a raw ' AND `tenant` = 42' fragment that four call
sites re-attached with $builder->whereRaw(ltrim($fragment, ' AND')). Two
problems: the tenant was interpolated rather than bound, alone among this
adapter's predicates, and ltrim() with ' AND' trims a character set - any
leading space, A, N or D - not a prefix, so it only worked because of what
followed it. Replaced with applyTenantFilter($builder), which pushes
Query::equal('tenant', [...]) through the builder like every other filter.

CHANGELOG said 'query 0.3 builder' and 'to 0.3.* (locked at 0.3.3)' after the
bump to 0.6 - the file a consumer reads to decide whether an upgrade breaks
them.

The fifth comment asked whether the empty-values guard is needed. It is, and it
is answered on the thread.

bin/monorepo check audit: pint, phpstan [OK] No errors, rector [OK]. Snapshot
and query suites 22 tests / 106 assertions. The tenant path is covered by the
live-server shared-tables test in CI.
@lohanidamodar
lohanidamodar merged commit 813884a into main Aug 25, 2026
5 checks passed
@lohanidamodar
lohanidamodar deleted the feat/audit-utopia-query-0.3.x branch August 25, 2026 06:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants