feat(clickhouse): add UUID, Decimal, Array/Tuple, UInt8/Int8, raw ORDER BY, rawColumn passthrough#10
feat(clickhouse): add UUID, Decimal, Array/Tuple, UInt8/Int8, raw ORDER BY, rawColumn passthrough#10lohanidamodar wants to merge 2 commits intomainfrom
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 extends the schema builder with six new features:
Confidence Score: 3/5The Array nullable path silently generates DDL that ClickHouse will reject at runtime; the remaining new features are correct across all dialects. The src/Query/Schema/ClickHouse.php — the Array nullable branch and the nested element type compiler; tests/Query/Schema/ClickHouseTest.php — the Important Files Changed
|
| if ($column instanceof Column\ClickHouse && $column->arrayElementType !== null) { | ||
| if ($column->isLowCardinality) { | ||
| throw new UnsupportedException('LowCardinality is not supported inside Array(...). Wrap the element type instead.'); | ||
| } | ||
|
|
||
| $inner = $this->compileNestedElementType($column->arrayElementType, $column); | ||
| $type = 'Array(' . $inner . ')'; | ||
|
|
||
| if ($column->isNullable) { | ||
| $type = 'Nullable(' . $type . ')'; | ||
| } | ||
|
|
||
| return $type; | ||
| } |
There was a problem hiding this comment.
ClickHouse explicitly forbids wrapping
Array with Nullable — the DDL Nullable(Array(T)) is rejected at the server level. The pattern mirrors the existing LowCardinality guard two lines above; the fix is to throw UnsupportedException there instead of silently emitting invalid DDL. The test testCreateTableArrayNullable passes today only because it checks the generated string, not whether ClickHouse accepts it.
| if ($column instanceof Column\ClickHouse && $column->arrayElementType !== null) { | |
| if ($column->isLowCardinality) { | |
| throw new UnsupportedException('LowCardinality is not supported inside Array(...). Wrap the element type instead.'); | |
| } | |
| $inner = $this->compileNestedElementType($column->arrayElementType, $column); | |
| $type = 'Array(' . $inner . ')'; | |
| if ($column->isNullable) { | |
| $type = 'Nullable(' . $type . ')'; | |
| } | |
| return $type; | |
| } | |
| if ($column instanceof Column\ClickHouse && $column->arrayElementType !== null) { | |
| if ($column->isLowCardinality) { | |
| throw new UnsupportedException('LowCardinality is not supported inside Array(...). Wrap the element type instead.'); | |
| } | |
| if ($column->isNullable) { | |
| throw new UnsupportedException('Nullable(Array(...)) is not supported in ClickHouse. Use an empty array [] to represent a missing value instead.'); | |
| } | |
| $inner = $this->compileNestedElementType($column->arrayElementType, $column); | |
| $type = 'Array(' . $inner . ')'; | |
| return $type; | |
| } |
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).