feat(clickhouse): add UUID, Decimal, Array/Tuple, UInt8/Int8, raw ORDER BY, rawColumn passthrough#10
Conversation
`rawColumn()` is the documented escape hatch for emitting dialect-specific column types the typed builder does not yet model. The base `Schema::compileCreate()` already iterates `$table->rawColumnDefs`, but the ClickHouse override loop did not — so raw fragments registered through the same fluent builder silently disappeared from the generated DDL on ClickHouse only. Mirror the loop in `Schema\ClickHouse::compileCreate()`.
…w(), plus ClickHouse Array/Tuple and raw ORDER BY Adds the remaining production-OLAP-shaped schema features that callers had to drop to `rawColumn()` for after the 0.3.x bump: - `Table::uuid()` — UUID column type, native on ClickHouse (`UUID`) and PostgreSQL (`UUID`); `CHAR(36)` on MySQL; `TEXT` on SQLite; `string` BSON type on MongoDB. Server-generated UUIDs are common as primary identifiers and need a dialect-specific default expression rather than an application-supplied value. - `Column::defaultRaw(string)` — raw default expression emitted verbatim after `DEFAULT`. Lets callers attach `generateUUIDv4()`, `gen_random_uuid()`, `UUID()`, `now()`, `CURRENT_TIMESTAMP`, etc. without the quoting `default()` applies to scalar values. Takes precedence over `default()` when both are set; rejects empty strings and semicolons. - `Table::tinyInteger()` and `Table::smallInteger()` — small integer column types. On ClickHouse they map to `UInt8`/`Int8` and `UInt16`/`Int16` (75% smaller than the default `UInt32` produced by `integer()->unsigned()`), to native `TINYINT`/`SMALLINT` on MySQL, to `SMALLINT` on PostgreSQL (which has no `TINYINT`), and to `INTEGER` on SQLite. Useful for bounded enumerations, percentage values, and other fields that fit well under 32 bits. - `Table::decimal(name, precision, scale)` — fixed-point numeric column for monetary and precision-sensitive values where binary-floating-point error is unacceptable. ClickHouse emits `Decimal(P, S)`; MySQL/PostgreSQL emit `DECIMAL(P, S)`; SQLite emits `NUMERIC(P, S)`; MongoDB maps to the `decimal` BSON type. Rejects negative scale and scale greater than precision. - `Table\ClickHouse::array(name, ColumnType $element)` and `Table\ClickHouse::tuple(name, list<ColumnType>)` — `Array(T)` and `Tuple(...)` nested column types. Core ClickHouse types for multi-valued attributes (tags, labels, parallel-array nested records) and fixed-arity composites (geo points, key/value pairs). Element types run back through the standard column-type compiler so `unsigned()` and `precision`/`scale` flags carry into the inner type. `Nullable(...)` wraps the whole `Array`/`Tuple`; `LowCardinality(...)` is rejected on these columns to match ClickHouse's documented constraints. - `Table\ClickHouse::orderByRaw(string)` — raw `ORDER BY` expression emitted verbatim. MergeTree `ORDER BY` clauses routinely include scalar function calls (`toDate(ts)`, `cityHash64(...)`, `intHash32(user_id)`) to control sparse-index cardinality; the existing identifier-only `orderBy(array)` blocks this common shape. Mirrors the `partitionBy(string)` convention. Takes precedence over `orderBy()` when both are set; rejects empty strings and semicolons. README updated under "Creating Tables" (new types and modifiers) and "ClickHouse Schema" (per-feature subsections with generated DDL). `Column::$scale` is added alongside the existing `$precision`/`$length` constructor args, and dialect `Table::newColumn()` overrides forward it through.
📊 Coverage
Full per-file breakdown in the job summary. |
Greptile SummaryThis PR adds a set of ClickHouse-oriented schema features —
Confidence Score: 5/5Safe to merge; the new nullable and LowCardinality guards are in place, the rawColumn bug is fixed, and dialect mappings are exhaustive and verified by tests. All core compilation paths are covered by the new test suite. The only gap is a missing test for the Tuple nullable rejection guard, which was added in direct response to prior review feedback but not paired with a test the way the Array guard was. src/Query/Schema/ClickHouse.php — the Tuple nullable guard fires after inner-type compilation (unlike the parallel Array guard), and the corresponding test is absent. Important Files Changed
Reviews (4): Last reviewed commit: "fix(tests): expect UnsupportedException ..." | Re-trigger Greptile |
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
@copilot Fix the unit test failure introduced by the last commit |
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
ClickHouse does not support Nullable(Array(...)) — the schema compiler already throws an UnsupportedException with a message directing callers to use an empty array as the missing-value sentinel. The previous test asserted compiled SQL that the compiler refuses to emit. Mirror the sibling testArrayRejectsLowCardinalityWrap pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Follow-up to #8 — adds the remaining ClickHouse schema features commonly needed in production OLAP workloads, plus a small compiler fix. Base-level features (
uuid(),decimal(),tinyInteger(),smallInteger(),defaultRaw()) also map cleanly across MySQL, PostgreSQL, SQLite, and MongoDB.What's new
UInt8/Int8viatinyInteger()andUInt16/Int16viasmallInteger()Small integer columns are a natural fit for bounded enumerations, percentage values, and other fields whose value range fits well below 32 bits. Storing them as
UInt8saves 75% of the disk and memory footprint compared to the defaultUInt32produced byinteger()->unsigned(). ClickHouse emitsUInt8/Int8andUInt16/Int16; MySQL maps toTINYINT/SMALLINT; PostgreSQL toSMALLINT(noTINYINT); SQLite toINTEGER.Array(T)andTuple(...)column typesArray(T)is the canonical ClickHouse type for multi-valued attributes — tags, labels, key/value pairs flattened into parallel arrays — and is the standard way to model nested records in the MergeTree family.Tuple(...)covers fixed-arity composites like geo points and key/value pairs.Element types run back through the standard column-type compiler so the parent column's
unsigned()andprecisionflags carry through to the inner type.Nullable(...)wraps the wholeArray/Tuple;LowCardinality(...)is rejected on these columns because ClickHouse only permits it on scalar types. ClickHouse-only — calling->array()or->tuple()on a different dialect's builder fails at the type level.decimal(precision, scale)Fixed-point numeric column type for monetary or precision-sensitive values where binary-floating-point error is unacceptable. ClickHouse emits
Decimal(P, S); MySQL/PostgreSQL emitDECIMAL(P, S); SQLite emitsNUMERIC(P, S); MongoDB maps to thedecimalBSON type. Combines withnullable()exactly as scalar columns do.UUIDcolumn type withdefaultRaw()UUIDs are first-class fixed-width identifier types in ClickHouse and PostgreSQL and a 36-character string elsewhere; production schemas commonly use them as primary identifiers with server-generated defaults.
Column::defaultRaw(string)emits the expression verbatim afterDEFAULT— distinct fromdefault(), which quotes string literals — so callers can attachgenerateUUIDv4(),gen_random_uuid(),UUID(),now(),CURRENT_TIMESTAMP, and similar dialect-specific server-generated defaults.uuid()compiles toUUIDon ClickHouse and PostgreSQL,CHAR(36)on MySQL,TEXTon SQLite, and thestringBSON type on MongoDB.defaultRaw()is on the baseColumn, so it works on every dialect; it takes precedence overdefault()when both are set, and rejects empty strings and semicolons.Raw expressions in
ORDER BYMergeTree
ORDER BYclauses routinely include scalar function calls —toDate(ts),cityHash64(...),intHash32(user_id)— to control sparse-index cardinality.orderBy(array)restricts each entry to a plain identifier;orderByRaw(string)accepts the full parenthesised tuple verbatim, mirroring the existingpartitionBy(string)convention.Takes precedence over
orderBy()when both are set; rejects empty strings and semicolons. ClickHouse-only.rawColumn()passthrough fix on ClickHouseTable::rawColumn(string $definition)is the documented escape hatch for column types the typed builder does not yet model. The baseSchema::compileCreate()already iterates$table->rawColumnDefs, but theSchema\ClickHouse::compileCreate()override loop did not — so raw fragments registered through the same fluent builder silently disappeared from the generated DDL on ClickHouse only. The fix mirrors the loop in the ClickHouse override (one for-loop).Out of scope (planned follow-up)
Builder\ClickHouse(FORMAT JSONEachRow,RowBinary,TabSeparated,Parquet) — broader surface that touches the builder rather than the schema compiler; deserves its own PR.Tests
38 new assertions across:
ClickHouseTest—uuid()with and withoutdefaultRaw(), nullable wrapping,defaultRaw()precedence and validation,tinyInteger()/smallInteger()(signed and unsigned),decimal()withnullable(),array(T)withString/UInt64/nullable wrapping,LowCardinalityrejection onArray,tuple()with empty-list validation,orderByRaw()with mixed function calls,orderByRaw()precedence and validation,rawColumn()passthrough throughcompileCreate().MySQLTest,PostgreSQLTest,SQLiteTest—tinyInteger/smallInteger/decimal/uuidcross-dialect mappings;defaultRaw()rendered correctly alongsideNOT NULL/PRIMARY KEY;decimal()precision/scale validation.MongoDBTest—decimal/tinyInteger/uuidBSON type mappings.All gates green:
composer test,composer lint,composer check(PHPStan level max).