diff --git a/README.md b/README.md index 72e2640..7d31d02 100644 --- a/README.md +++ b/README.md @@ -1776,7 +1776,7 @@ $result = $schema->table('users') Available column types: `id`, `string`, `text`, `mediumText`, `longText`, `integer`, `bigInteger`, `serial`, `bigSerial`, `smallSerial`, `float`, `boolean`, `datetime`, `timestamp`, `json`, `binary`, `enum`, `point`, `linestring`, `polygon`, `vector` (PostgreSQL only), `timestamps`. -Column modifiers: `nullable()`, `default($value)`, `unsigned()`, `unique()`, `primary()`, `autoIncrement()`, `after($column)`, `comment($text)`, `collation($collation)`, `check($expression)`, `generatedAs($expression)` + `stored()` / `virtual()`, `ttl($expression)` (ClickHouse), `userType($name)` (PostgreSQL). +Column modifiers: `nullable()`, `default($value)`, `unsigned()`, `unique()`, `primary()`, `autoIncrement()`, `after($column)`, `comment($text)`, `collation($collation)`, `check($expression)`, `generatedAs($expression)` + `stored()` / `virtual()`, `ttl($expression)` (ClickHouse), `lowCardinality()` (ClickHouse), `codec($codec)` (ClickHouse), `userType($name)` (PostgreSQL). **SERIAL types** — auto-incrementing integers. PostgreSQL emits native `SERIAL` / `BIGSERIAL` / `SMALLSERIAL`; MySQL/MariaDB compile to `INT AUTO_INCREMENT` / `BIGINT AUTO_INCREMENT` / `SMALLINT AUTO_INCREMENT`; SQLite maps to `INTEGER`. ClickHouse and MongoDB throw `UnsupportedException`: @@ -2130,6 +2130,71 @@ $schema->table('events') Setting names must match `[A-Za-z_][A-Za-z0-9_]*`; string values are restricted to `[A-Za-z0-9_.\-+/]*`. Use ints / floats / booleans for everything else. Other dialects ignore the call. +**LowCardinality** — wrap a column type in `LowCardinality(...)` for compact dictionary-encoded storage on string columns with a small number of distinct values (status enums, type discriminators, country codes, category labels): + +```php +$schema->table('events') + ->bigInteger('id')->primary() + ->string('status')->lowCardinality() + ->string('country')->lowCardinality()->nullable() + ->create(); + +// CREATE TABLE `events` (`id` Int64, `status` LowCardinality(String), +// `country` Nullable(LowCardinality(String))) ENGINE = MergeTree() ORDER BY (`id`) +``` + +`Nullable` is applied outside `LowCardinality` to match ClickHouse's required wrapping order. Other dialects throw `UnsupportedException` at compile time. + +**FixedString(N)** — fixed-length string column. Use for ISO codes, hash digests, and other values whose byte length is known and constant: + +```php +$schema->table('locations') + ->bigInteger('id')->primary() + ->fixedString('country_code', 2) // ISO 3166-1 alpha-2 + ->fixedString('currency_code', 3) // ISO 4217 + ->fixedString('digest', 32) // raw MD5 + ->create(); + +// CREATE TABLE `locations` (`id` Int64, `country_code` FixedString(2), +// `currency_code` FixedString(3), `digest` FixedString(32)) +// ENGINE = MergeTree() ORDER BY (`id`) +``` + +Length must be at least 1. Other dialects throw `UnsupportedException` at compile time. + +**Column-level CODEC** — append one or more compression codecs to a column. Multiple `codec()` calls accumulate and emit `CODEC(c1, c2, ...)`: + +```php +$schema->table('metrics') + ->bigInteger('id')->primary() + ->datetime('ts', 3)->codec('Delta(4)')->codec('LZ4') // monotonic timestamps + ->bigInteger('value')->codec('T64')->codec('LZ4') // integer column + ->string('payload')->codec('ZSTD(3)') // text column + ->create(); + +// CREATE TABLE `metrics` (`id` Int64, +// `ts` DateTime64(3) CODEC(Delta(4), LZ4), +// `value` Int64 CODEC(T64, LZ4), +// `payload` String CODEC(ZSTD(3))) ENGINE = MergeTree() ORDER BY (`id`) +``` + +Each codec string is emitted verbatim; supply codec arguments inline (`'Delta(4)'`, `'ZSTD(3)'`). Codec strings must not be empty or contain a semicolon. Other dialects throw `UnsupportedException` at compile time. + +**SAMPLE BY** — declare a sampling expression for approximate-query support (`SELECT ... SAMPLE k`). Emitted after `ORDER BY` and before `TTL` / `SETTINGS`: + +```php +$schema->table('events') + ->bigInteger('id')->primary() + ->bigInteger('user_id')->unsigned() + ->sampleBy('user_id') + ->create(); + +// CREATE TABLE `events` (`id` Int64, `user_id` UInt64) ENGINE = MergeTree() +// ORDER BY (`id`) SAMPLE BY user_id +``` + +The expression is emitted verbatim and must not be empty or contain a semicolon. SAMPLE BY only applies to engines that take an `ORDER BY` clause (the MergeTree family); using it with `Memory`, `Log`, `TinyLog`, or `StripeLog` throws `UnsupportedException`. Other dialects throw `UnsupportedException` at compile time. + ### SQLite Schema ```php diff --git a/src/Query/Schema.php b/src/Query/Schema.php index 2616492..6613919 100644 --- a/src/Query/Schema.php +++ b/src/Query/Schema.php @@ -47,6 +47,10 @@ public function compileCreate(Table $table, bool $ifNotExists = false): Statemen throw new UnsupportedException('TTL is only supported in ClickHouse.'); } + if ($table->sampleBy !== null) { + throw new UnsupportedException('SAMPLE BY is only supported in ClickHouse.'); + } + $columnDefs = []; $primaryKeys = []; $uniqueColumns = []; @@ -296,6 +300,14 @@ protected function compileColumnDefinition(Column $column): string throw new UnsupportedException('TTL is only supported in ClickHouse.'); } + if ($column->isLowCardinality) { + throw new UnsupportedException('LowCardinality is only supported in ClickHouse.'); + } + + if ($column->codecs !== []) { + throw new UnsupportedException('Column-level CODEC is only supported in ClickHouse.'); + } + $parts = [ $this->quote($column->name), $this->compileColumnType($column), diff --git a/src/Query/Schema/ClickHouse.php b/src/Query/Schema/ClickHouse.php index 22d35b0..c792b0b 100644 --- a/src/Query/Schema/ClickHouse.php +++ b/src/Query/Schema/ClickHouse.php @@ -25,6 +25,7 @@ protected function compileColumnType(Column $column): string $type = match ($column->type) { ColumnType::String, ColumnType::Varchar, ColumnType::Relationship => 'String', + ColumnType::FixedString => 'FixedString(' . ($column->length ?? throw new ValidationException('FixedString requires a length.')) . ')', ColumnType::Text => 'String', ColumnType::MediumText, ColumnType::LongText => 'String', ColumnType::Integer => $column->isUnsigned ? 'UInt32' : 'Int32', @@ -44,6 +45,10 @@ protected function compileColumnType(Column $column): string ColumnType::Serial, ColumnType::BigSerial, ColumnType::SmallSerial => throw new UnsupportedException('SERIAL types are not supported in ClickHouse.'), }; + if ($column->isLowCardinality) { + $type = 'LowCardinality(' . $type . ')'; + } + if ($column->isNullable) { $type = 'Nullable(' . $type . ')'; } @@ -80,6 +85,10 @@ protected function compileColumnDefinition(Column $column): string $parts[] = 'DEFAULT ' . $this->compileDefaultValue($column->default); } + if ($column->codecs !== []) { + $parts[] = 'CODEC(' . \implode(', ', $column->codecs) . ')'; + } + if ($column->ttl !== null) { $parts[] = 'TTL ' . $column->ttl; } @@ -217,6 +226,15 @@ public function compileCreate(Table $table, bool $ifNotExists = false): Statemen : ' ORDER BY tuple()'; } + if ($table->sampleBy !== null) { + if (! $engine->requiresOrderBy()) { + throw new UnsupportedException( + 'SAMPLE BY is only supported on engines that take an ORDER BY clause.' + ); + } + $sql .= ' SAMPLE BY ' . $table->sampleBy; + } + if ($table->ttl !== null) { $sql .= ' TTL ' . $table->ttl; } diff --git a/src/Query/Schema/Column.php b/src/Query/Schema/Column.php index 09d4075..e937416 100644 --- a/src/Query/Schema/Column.php +++ b/src/Query/Schema/Column.php @@ -52,6 +52,11 @@ class Column public private(set) ?string $userTypeName = null; + public private(set) bool $isLowCardinality = false; + + /** @var list ClickHouse column-level CODEC clauses, e.g. ['Delta(4)', 'LZ4'] */ + public private(set) array $codecs = []; + public function __construct( public Table $table, public string $name, @@ -247,6 +252,48 @@ public function ttl(string $expression): static return $this; } + /** + * Wrap the column type in `LowCardinality(...)` (ClickHouse only). + * + * Suitable for string columns with a small number of distinct values + * (status enums, type discriminators, country codes). Other dialects + * throw `UnsupportedException` at compile time. + */ + public function lowCardinality(): static + { + $this->isLowCardinality = true; + + return $this; + } + + /** + * Append a column-level CODEC clause (ClickHouse only). + * + * Multiple calls accumulate and emit `CODEC(c1, c2, ...)`. Pass either + * a bare codec name (`->codec('LZ4')`) or one with arguments + * (`->codec('Delta(4)')`, `->codec('ZSTD(3)')`). The codec string is + * emitted verbatim and must come from a trusted source. + * + * @throws ValidationException if the codec string is empty or contains + * a semicolon. + */ + public function codec(string $codec): static + { + $trimmed = \trim($codec); + + if ($trimmed === '') { + throw new ValidationException('CODEC expression must not be empty.'); + } + + if (\str_contains($trimmed, ';')) { + throw new ValidationException('CODEC expression must not contain ";".'); + } + + $this->codecs[] = $trimmed; + + return $this; + } + /** * Reference a user-defined type (e.g. a PostgreSQL enum type created via CREATE TYPE). * @@ -273,6 +320,11 @@ public function string(string $name, int $length = 255): Column return $this->table->string($name, $length); } + public function fixedString(string $name, int $length): Column + { + return $this->table->fixedString($name, $length); + } + public function text(string $name): Column { return $this->table->text($name); @@ -540,6 +592,11 @@ public function orderBy(array $columns): Table return $this->table->orderBy($columns); } + public function sampleBy(string $expression): Table + { + return $this->table->sampleBy($expression); + } + public function create(bool $ifNotExists = false): Statement { return $this->table->create($ifNotExists); diff --git a/src/Query/Schema/ColumnType.php b/src/Query/Schema/ColumnType.php index a7ff7a1..173bb51 100644 --- a/src/Query/Schema/ColumnType.php +++ b/src/Query/Schema/ColumnType.php @@ -5,6 +5,7 @@ enum ColumnType: string { case String = 'string'; + case FixedString = 'fixedstring'; case Varchar = 'varchar'; case Text = 'text'; case MediumText = 'mediumtext'; diff --git a/src/Query/Schema/MongoDB.php b/src/Query/Schema/MongoDB.php index 87267cd..60c2fc2 100644 --- a/src/Query/Schema/MongoDB.php +++ b/src/Query/Schema/MongoDB.php @@ -36,6 +36,7 @@ protected function compileColumnType(Column $column): string ColumnType::Linestring, ColumnType::Polygon => 'object', ColumnType::Uuid7 => 'string', ColumnType::Vector => 'array', + ColumnType::FixedString => throw new UnsupportedException('FixedString is only supported in ClickHouse.'), }; } diff --git a/src/Query/Schema/MySQL.php b/src/Query/Schema/MySQL.php index 707df77..d60f921 100644 --- a/src/Query/Schema/MySQL.php +++ b/src/Query/Schema/MySQL.php @@ -36,6 +36,7 @@ protected function compileColumnType(Column $column): string ColumnType::Polygon => 'POLYGON' . ($column->srid !== null ? ' SRID ' . $column->srid : ''), ColumnType::Uuid7 => 'VARCHAR(36)', ColumnType::Vector => throw new UnsupportedException('Vector type is not supported in MySQL.'), + ColumnType::FixedString => throw new UnsupportedException('FixedString is only supported in ClickHouse.'), }; } diff --git a/src/Query/Schema/PostgreSQL.php b/src/Query/Schema/PostgreSQL.php index 5a8bc2e..14f2ea9 100644 --- a/src/Query/Schema/PostgreSQL.php +++ b/src/Query/Schema/PostgreSQL.php @@ -42,6 +42,7 @@ protected function compileColumnType(Column $column): string ColumnType::Serial => 'SERIAL', ColumnType::BigSerial => 'BIGSERIAL', ColumnType::SmallSerial => 'SMALLSERIAL', + ColumnType::FixedString => throw new UnsupportedException('FixedString is only supported in ClickHouse.'), }; } @@ -57,6 +58,14 @@ protected function compileUnsigned(): string protected function compileColumnDefinition(Column $column): string { + if ($column->isLowCardinality) { + throw new UnsupportedException('LowCardinality is only supported in ClickHouse.'); + } + + if ($column->codecs !== []) { + throw new UnsupportedException('Column-level CODEC is only supported in ClickHouse.'); + } + $parts = [ $this->quote($column->name), $this->compileColumnType($column), diff --git a/src/Query/Schema/SQLite.php b/src/Query/Schema/SQLite.php index 69e5f70..134a370 100644 --- a/src/Query/Schema/SQLite.php +++ b/src/Query/Schema/SQLite.php @@ -27,6 +27,7 @@ protected function compileColumnType(Column $column): string ColumnType::Point, ColumnType::Linestring, ColumnType::Polygon => 'TEXT', ColumnType::Uuid7 => 'VARCHAR(36)', ColumnType::Vector => throw new UnsupportedException('Vector type is not supported in SQLite.'), + ColumnType::FixedString => throw new UnsupportedException('FixedString is only supported in ClickHouse.'), }; } @@ -48,6 +49,14 @@ protected function compileColumnDefinition(Column $column): string return parent::compileColumnDefinition($column); } + if ($column->isLowCardinality) { + throw new UnsupportedException('LowCardinality is only supported in ClickHouse.'); + } + + if ($column->codecs !== []) { + throw new UnsupportedException('Column-level CODEC is only supported in ClickHouse.'); + } + $parts = [ $this->quote($column->name), $this->compileColumnType($column), diff --git a/src/Query/Schema/Table.php b/src/Query/Schema/Table.php index b46121a..f4856ca 100644 --- a/src/Query/Schema/Table.php +++ b/src/Query/Schema/Table.php @@ -61,6 +61,9 @@ class Table /** @var array Table-level engine SETTINGS (ClickHouse only) */ public private(set) array $settings = []; + /** ClickHouse SAMPLE BY expression. Emitted after ORDER BY when set. */ + public private(set) ?string $sampleBy = null; + public function __construct( private readonly ?Schema $schema = null, public readonly string $name = '', @@ -166,6 +169,24 @@ public function string(string $name, int $length = 255): Column return $col; } + /** + * Add a fixed-length string column. Compiled to ClickHouse `FixedString(N)`; + * other dialects throw {@see UnsupportedException} at compile time. + * + * @throws ValidationException if $length is less than 1. + */ + public function fixedString(string $name, int $length): Column + { + if ($length < 1) { + throw new ValidationException('FixedString length must be at least 1.'); + } + + $col = new Column($this, $name, ColumnType::FixedString, $length); + $this->columns[] = $col; + + return $col; + } + public function text(string $name): Column { $col = new Column($this, $name, ColumnType::Text); @@ -655,6 +676,31 @@ public function orderBy(array $columns): static return $this; } + /** + * Set the ClickHouse SAMPLE BY expression. Emitted after ORDER BY at table + * creation time. Required to model tables that need approximate-query + * support via `SELECT ... SAMPLE k`. Other dialects throw + * {@see UnsupportedException} at compile time. + * + * @throws ValidationException if the expression is empty or contains a semicolon. + */ + public function sampleBy(string $expression): static + { + $trimmed = \trim($expression); + + if ($trimmed === '') { + throw new ValidationException('SAMPLE BY expression must not be empty.'); + } + + if (\str_contains($trimmed, ';')) { + throw new ValidationException('SAMPLE BY expression must not contain ";".'); + } + + $this->sampleBy = $trimmed; + + return $this; + } + /** * Attach a table-level TTL expression (ClickHouse only). * diff --git a/tests/Query/Schema/ClickHouseTest.php b/tests/Query/Schema/ClickHouseTest.php index 852ad04..36308ac 100644 --- a/tests/Query/Schema/ClickHouseTest.php +++ b/tests/Query/Schema/ClickHouseTest.php @@ -977,4 +977,230 @@ public function testAlterRejectsSettings(): void ->settings(['index_granularity' => 4096]) ->alter(); } + + // FixedString + + public function testCreateTableFixedStringColumn(): void + { + $schema = new Schema(); + $result = $schema->table('locations') + ->bigInteger('id')->primary() + ->fixedString('country_code', 2) + ->fixedString('currency_code', 3) + ->create(); + $this->assertBindingCount($result); + + $this->assertSame( + 'CREATE TABLE `locations` (`id` Int64, `country_code` FixedString(2), `currency_code` FixedString(3)) ENGINE = MergeTree() ORDER BY (`id`)', + $result->query, + ); + } + + public function testCreateTableFixedStringNullable(): void + { + $schema = new Schema(); + $result = $schema->table('t') + ->fixedString('hash', 32)->nullable() + ->create(); + $this->assertBindingCount($result); + + $this->assertSame( + 'CREATE TABLE `t` (`hash` Nullable(FixedString(32))) ENGINE = MergeTree() ORDER BY tuple()', + $result->query, + ); + } + + public function testFixedStringRejectsZeroLength(): void + { + $this->expectException(ValidationException::class); + + $schema = new Schema(); + $schema->table('t')->fixedString('bad', 0); + } + + // LowCardinality + + public function testCreateTableLowCardinalityColumn(): void + { + $schema = new Schema(); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->string('status')->lowCardinality() + ->create(); + $this->assertBindingCount($result); + + $this->assertSame( + 'CREATE TABLE `events` (`id` Int64, `status` LowCardinality(String)) ENGINE = MergeTree() ORDER BY (`id`)', + $result->query, + ); + } + + public function testCreateTableLowCardinalityNullableWrapsInBothOrder(): void + { + $schema = new Schema(); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->string('status')->lowCardinality()->nullable() + ->create(); + $this->assertBindingCount($result); + + $this->assertSame( + 'CREATE TABLE `events` (`id` Int64, `status` Nullable(LowCardinality(String))) ENGINE = MergeTree() ORDER BY (`id`)', + $result->query, + ); + } + + public function testAlterAddLowCardinalityColumn(): void + { + $schema = new Schema(); + $result = $schema->table('events') + ->addColumn('country', ColumnType::String)->lowCardinality() + ->alter(); + $this->assertBindingCount($result); + + $this->assertSame( + 'ALTER TABLE `events` ADD COLUMN `country` LowCardinality(String)', + $result->query, + ); + } + + // Column-level CODEC + + public function testCreateTableColumnWithSingleCodec(): void + { + $schema = new Schema(); + $result = $schema->table('metrics') + ->bigInteger('id')->primary() + ->datetime('ts', 3)->codec('LZ4') + ->create(); + $this->assertBindingCount($result); + + $this->assertSame( + 'CREATE TABLE `metrics` (`id` Int64, `ts` DateTime64(3) CODEC(LZ4)) ENGINE = MergeTree() ORDER BY (`id`)', + $result->query, + ); + } + + public function testCreateTableColumnWithMultipleCodecs(): void + { + $schema = new Schema(); + $result = $schema->table('metrics') + ->bigInteger('id')->primary() + ->datetime('ts', 3)->codec('Delta(4)')->codec('LZ4') + ->create(); + $this->assertBindingCount($result); + + $this->assertSame( + 'CREATE TABLE `metrics` (`id` Int64, `ts` DateTime64(3) CODEC(Delta(4), LZ4)) ENGINE = MergeTree() ORDER BY (`id`)', + $result->query, + ); + } + + public function testCodecOrderingRelativeToTtlAndComment(): void + { + $schema = new Schema(); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->string('payload') + ->codec('ZSTD(3)') + ->ttl('ts + INTERVAL 30 DAY') + ->comment('Compressed payload') + ->datetime('ts') + ->create(); + $this->assertBindingCount($result); + + $this->assertSame( + 'CREATE TABLE `events` (`id` Int64,' + . ' `payload` String CODEC(ZSTD(3)) TTL ts + INTERVAL 30 DAY COMMENT \'Compressed payload\',' + . ' `ts` DateTime) ENGINE = MergeTree() ORDER BY (`id`)', + $result->query, + ); + } + + public function testCodecRejectsEmpty(): void + { + $this->expectException(ValidationException::class); + + $schema = new Schema(); + $schema->table('t') + ->integer('id')->codec(''); + } + + public function testCodecRejectsSemicolon(): void + { + $this->expectException(ValidationException::class); + + $schema = new Schema(); + $schema->table('t') + ->integer('id')->codec('LZ4;'); + } + + // SAMPLE BY + + public function testCreateTableWithSampleBy(): void + { + $schema = new Schema(); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->bigInteger('user_id')->unsigned() + ->sampleBy('user_id') + ->create(); + $this->assertBindingCount($result); + + $this->assertSame( + 'CREATE TABLE `events` (`id` Int64, `user_id` UInt64) ENGINE = MergeTree() ORDER BY (`id`) SAMPLE BY user_id', + $result->query, + ); + } + + public function testCreateTableSampleByOrderingWithTtlAndSettings(): void + { + $schema = new Schema(); + $table = $schema->table('events'); + $table->bigInteger('id')->primary(); + $table->datetime('created_at'); + $result = $table + ->sampleBy('id') + ->ttl('`created_at` + INTERVAL 30 DAY') + ->settings(['index_granularity' => 4096]) + ->create(); + $this->assertBindingCount($result); + + $this->assertSame( + 'CREATE TABLE `events` (`id` Int64, `created_at` DateTime) ENGINE = MergeTree() ORDER BY (`id`)' + . ' SAMPLE BY id' + . ' TTL `created_at` + INTERVAL 30 DAY' + . ' SETTINGS index_granularity = 4096', + $result->query, + ); + } + + public function testSampleByRejectsEmpty(): void + { + $this->expectException(ValidationException::class); + + $schema = new Schema(); + $schema->table('events')->sampleBy(''); + } + + public function testSampleByRejectsSemicolon(): void + { + $this->expectException(ValidationException::class); + + $schema = new Schema(); + $schema->table('events')->sampleBy('id;'); + } + + public function testSampleByRejectedOnEnginesWithoutOrderBy(): void + { + $this->expectException(UnsupportedException::class); + $this->expectExceptionMessage('SAMPLE BY'); + + $schema = new Schema(); + $schema->table('cache') + ->integer('id')->primary() + ->engine(Engine::Memory) + ->sampleBy('id') + ->create(); + } } diff --git a/tests/Query/Schema/FluentBuilderTest.php b/tests/Query/Schema/FluentBuilderTest.php index fc05ecd..5ee1acf 100644 --- a/tests/Query/Schema/FluentBuilderTest.php +++ b/tests/Query/Schema/FluentBuilderTest.php @@ -670,6 +670,55 @@ public function testColumnTtlOnNonClickHouseSchemaThrowsAtCompile(): void ->create(); } + public function testFixedStringOnNonClickHouseSchemaThrowsAtCompile(): void + { + $schema = new SQLite(); + + $this->expectException(UnsupportedException::class); + $this->expectExceptionMessage('FixedString is only supported in ClickHouse'); + + $schema->table('locations') + ->fixedString('country_code', 2) + ->create(); + } + + public function testLowCardinalityOnNonClickHouseSchemaThrowsAtCompile(): void + { + $schema = new MySQL(); + + $this->expectException(UnsupportedException::class); + $this->expectExceptionMessage('LowCardinality is only supported in ClickHouse'); + + $schema->table('events') + ->string('status')->lowCardinality() + ->create(); + } + + public function testColumnCodecOnNonClickHouseSchemaThrowsAtCompile(): void + { + $schema = new PostgreSQL(); + + $this->expectException(UnsupportedException::class); + $this->expectExceptionMessage('Column-level CODEC is only supported in ClickHouse'); + + $schema->table('events') + ->datetime('ts')->codec('LZ4') + ->create(); + } + + public function testTableSampleByOnNonClickHouseSchemaThrowsAtCompile(): void + { + $schema = new MySQL(); + + $this->expectException(UnsupportedException::class); + $this->expectExceptionMessage('SAMPLE BY is only supported in ClickHouse'); + + $schema->table('events') + ->integer('id')->primary() + ->sampleBy('id') + ->create(); + } + public function testColumnAfterIsHonouredOnAlter(): void { $schema = new MySQL();