diff --git a/README.md b/README.md index 79cc33c..ac760c5 100644 --- a/README.md +++ b/README.md @@ -1750,26 +1750,28 @@ use Utopia\Query\Schema\Table; ```php $schema = new Schema(); -$result = $schema->create('users', function (Table $table) { - $table->id(); - $table->string('name', 255); - $table->string('email', 255)->unique(); - $table->integer('age')->nullable(); - $table->boolean('active')->default(true); - $table->json('metadata'); - $table->timestamps(); -}); +$result = $schema->table('users') + ->id() + ->string('name', 255) + ->string('email', 255)->unique() + ->integer('age')->nullable() + ->boolean('active')->default(true) + ->json('metadata') + ->timestamps() + ->create(); $result->query; // CREATE TABLE `users` (...) ``` +`Schema::table($name)` returns a fluent builder. Column-adding methods (`id`, `string`, `integer`, …) return a `Column` you can chain modifiers on; the column also exposes the table-level builder so you can keep chaining sibling columns or terminal calls without breaking the chain. Terminal methods (`create`, `createIfNotExists`, `alter`, `drop`, `dropIfExists`, `truncate`, `rename`) compile and return a `Statement`. + Use `createIfNotExists()` to add `IF NOT EXISTS`: ```php -$result = $schema->createIfNotExists('users', function (Table $table) { - $table->id(); - $table->string('name', 255); -}); +$result = $schema->table('users') + ->id() + ->string('name', 255) + ->createIfNotExists(); ``` 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`. @@ -1779,35 +1781,38 @@ Column modifiers: `nullable()`, `default($value)`, `unsigned()`, `unique()`, `pr **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`: ```php -$result = $schema->create('orders', function (Table $table) { - $table->serial('id')->primary(); - $table->bigSerial('external_id'); -}); +$result = $schema->table('orders') + ->serial('id')->primary() + ->bigSerial('external_id') + ->create(); ``` ### Altering Tables ```php -$result = $schema->alter('users', function (Table $table) { - $table->string('phone', 20)->nullable(); - $table->modifyColumn('name', 'string', 500); - $table->renameColumn('email', 'email_address'); - $table->dropColumn('legacy_field'); -}); +use Utopia\Query\Schema\ColumnType; + +$result = $schema->table('users') + ->string('phone', 20)->nullable() + ->modifyColumn('name', ColumnType::String, 500) + ->renameColumn('email', 'email_address') + ->dropColumn('legacy_field') + ->alter(); ``` +`addColumn(string $name, ColumnType $type, ?int $lengthOrPrecision = null)` and `modifyColumn(...)` take the `ColumnType` enum directly. The `addIndex(...)` overload takes the `IndexType` enum. + ### CHECK Constraints Typed `CHECK` constraints are supported at both the table and column level on MySQL 8.0.16+, MariaDB, PostgreSQL, and SQLite. ClickHouse throws `UnsupportedException`. ```php -$result = $schema->create('people', function (Table $table) { - $table->id(); - $table->integer('age')->check('>= 0'); // column-level - $table->string('email', 255); - - $table->check('age_range', '`age` >= 0 AND `age` < 150'); // table-level -}); +$result = $schema->table('people') + ->id() + ->integer('age')->check('>= 0') // column-level + ->string('email', 255) + ->check('age_range', '`age` >= 0 AND `age` < 150') // table-level + ->create(); ``` Constraint names are validated as standard SQL identifiers; expressions are emitted verbatim and must come from trusted sources — never from untrusted input. @@ -1817,18 +1822,17 @@ Constraint names are validated as standard SQL identifiers; expressions are emit Generated columns compute their value from an expression. Both `STORED` and `VIRTUAL` are supported on MySQL, MariaDB, and SQLite. PostgreSQL supports only `STORED` (calling `virtual()` and compiling for PostgreSQL throws `UnsupportedException`). ClickHouse throws `UnsupportedException` for generated columns. ```php -$result = $schema->create('boxes', function (Table $table) { - $table->id(); - $table->integer('width'); - $table->integer('height'); - $table->integer('area') +$result = $schema->table('boxes') + ->id() + ->integer('width') + ->integer('height') + ->integer('area') ->generatedAs('`width` * `height`') - ->stored(); - - $table->integer('half_area') + ->stored() + ->integer('half_area') ->generatedAs('(`width` * `height`) / 2') - ->virtual(); -}); + ->virtual() + ->create(); ``` ### Composite Primary Keys @@ -1836,13 +1840,12 @@ $result = $schema->create('boxes', function (Table $table) { Declare a primary key across two or more columns with `Table::primary([...])`. Mixing a column-level `->primary()` with `Table::primary([...])` throws `ValidationException`. MongoDB throws `UnsupportedException`. ```php -$result = $schema->create('order_items', function (Table $table) { - $table->integer('order_id'); - $table->integer('product_id'); - $table->integer('quantity'); - - $table->primary(['order_id', 'product_id']); -}); +$result = $schema->table('order_items') + ->integer('order_id') + ->integer('product_id') + ->integer('quantity') + ->primary(['order_id', 'product_id']) + ->create(); ``` ### Indexes @@ -1893,11 +1896,11 @@ Available on MySQL, PostgreSQL, and ClickHouse: ```php // Define partition strategy in table creation -$result = $schema->create('events', function (Table $table) { - $table->id(); - $table->datetime('created_at'); - $table->partitionByRange('created_at'); -}); +$result = $schema->table('events') + ->id() + ->datetime('created_at') + ->partitionByRange('created_at') + ->create(); // Create a child partition (MySQL, PostgreSQL) $result = $schema->createPartition('events', 'events_2024', "VALUES LESS THAN ('2025-01-01')"); @@ -1909,11 +1912,11 @@ $result = $schema->dropPartition('events', 'events_2024'); Partition strategies: `partitionByRange($expression)`, `partitionByList($expression)`, `partitionByHash($expression, ?int $partitions = null)`. The optional partition count on `partitionByHash()` emits `PARTITIONS ` (MySQL/MariaDB HASH/KEY semantics) and must be `>= 1`: ```php -$result = $schema->create('users', function (Table $table) { - $table->id(); - $table->integer('user_id'); - $table->partitionByHash('`user_id`', 4); -}); +$result = $schema->table('users') + ->id() + ->integer('user_id') + ->partitionByHash('`user_id`', 4) + ->create(); // ... PARTITION BY HASH(`user_id`) PARTITIONS 4 ``` @@ -1981,10 +1984,10 @@ END; // Custom types — reference from a column via Column::userType() $result = $schema->createType('mood_type', ['happy', 'sad', 'angry']); -$result = $schema->create('users', function (Table $table) { - $table->id(); - $table->string('mood')->userType('mood_type'); -}); +$result = $schema->table('users') + ->id() + ->string('mood')->userType('mood_type') + ->create(); $result = $schema->dropType('mood_type'); @@ -2018,12 +2021,12 @@ use Utopia\Query\Schema\ClickHouse\Engine; $schema = new Schema(); -$result = $schema->create('events', function (Table $table) { - $table->string('event_id', 36)->primary(); - $table->string('event_type', 50); - $table->integer('count'); - $table->datetime('created_at'); -}); +$result = $schema->table('events') + ->string('event_id', 36)->primary() + ->string('event_type', 50) + ->integer('count') + ->datetime('created_at') + ->create(); // CREATE TABLE `events` (...) ENGINE = MergeTree() ORDER BY (...) ``` @@ -2036,32 +2039,32 @@ Supports the `TableComments`, `ColumnComments`, and `DropPartition` interfaces. ```php // Standard MergeTree family -$schema->create('dedup', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->integer('version'); - $table->engine(Engine::ReplacingMergeTree, 'version'); -}); +$schema->table('dedup') + ->bigInteger('id')->primary() + ->integer('version') + ->engine(Engine::ReplacingMergeTree, 'version') + ->create(); // ... ENGINE = ReplacingMergeTree(`version`) ORDER BY (`id`) -$schema->create('metrics', function (Table $table) { - $table->integer('key')->primary(); - $table->bigInteger('total')->unsigned(); - $table->engine(Engine::SummingMergeTree, 'total'); -}); +$schema->table('metrics') + ->integer('key')->primary() + ->bigInteger('total')->unsigned() + ->engine(Engine::SummingMergeTree, 'total') + ->create(); // CollapsingMergeTree requires a sign column (throws ValidationException otherwise) // ReplicatedMergeTree requires zookeeper_path + replica_name -$schema->create('replicated', function (Table $table) { - $table->integer('id')->primary(); - $table->engine(Engine::ReplicatedMergeTree, '/clickhouse/tables/events', 'replica_1'); -}); +$schema->table('replicated') + ->integer('id')->primary() + ->engine(Engine::ReplicatedMergeTree, '/clickhouse/tables/events', 'replica_1') + ->create(); // Non-MergeTree engines skip the ORDER BY tuple() fallback entirely -$schema->create('cache', function (Table $table) { - $table->integer('id')->primary(); - $table->string('value'); - $table->engine(Engine::Memory); -}); +$schema->table('cache') + ->integer('id')->primary() + ->string('value') + ->engine(Engine::Memory) + ->create(); // CREATE TABLE `cache` (...) ENGINE = Memory ``` @@ -2070,13 +2073,15 @@ The 10 variants: `MergeTree`, `ReplacingMergeTree`, `SummingMergeTree`, `Aggrega **TTL** — table-level and column-level time-to-live expressions: ```php -$schema->create('events', function (Table $table) { - $table->integer('id')->primary(); - $table->datetime('ts'); - $table->datetime('expires_at')->ttl('now() + INTERVAL 1 HOUR'); // column-level +$schema->table('events') + ->integer('id')->primary() + ->datetime('ts') + ->datetime('expires_at')->ttl('now() + INTERVAL 1 HOUR') // column-level + ->ttl('ts + INTERVAL 1 DAY') // table-level + ->create(); - $table->ttl('ts + INTERVAL 1 DAY'); // table-level -}); +// Set the ORDER BY clause explicitly with `->orderBy([...])` if it should +// differ from the primary key. ``` TTL expressions are emitted verbatim; they must not be empty or contain semicolons. Dialects other than ClickHouse throw `UnsupportedException`. @@ -2102,13 +2107,13 @@ The MongoDB schema generates JSON commands for collection management with BSON t ```php $schema = new Schema(); -$result = $schema->create('users', function (Table $table) { - $table->string('name', 255); - $table->string('email', 255)->unique(); - $table->integer('age')->nullable(); - $table->boolean('active')->default(true); - $table->json('metadata'); -}); +$result = $schema->table('users') + ->string('name', 255) + ->string('email', 255)->unique() + ->integer('age')->nullable() + ->boolean('active')->default(true) + ->json('metadata') + ->create(); // Generates a create command with bsonType validators ``` @@ -2116,9 +2121,9 @@ $result = $schema->create('users', function (Table $table) { **Altering collections:** ```php -$result = $schema->alter('users', function (Table $table) { - $table->string('phone', 20)->nullable(); -}); +$result = $schema->table('users') + ->string('phone', 20)->nullable() + ->alter(); // Generates a collMod command to update the validator ``` @@ -2133,9 +2138,9 @@ $result = $schema->dropIndex('users', 'idx_email'); **Collection operations:** ```php -$result = $schema->drop('users'); -$result = $schema->rename('old_name', 'new_name'); -$result = $schema->truncate('users'); +$result = $schema->table('users')->drop(); +$result = $schema->table('old_name')->rename('new_name'); +$result = $schema->table('users')->truncate(); $result = $schema->analyzeTable('users'); ``` diff --git a/src/Query/Schema.php b/src/Query/Schema.php index 3105a65..2616492 100644 --- a/src/Query/Schema.php +++ b/src/Query/Schema.php @@ -32,22 +32,18 @@ abstract protected function compileColumnType(Column $column): string; abstract protected function compileAutoIncrement(): string; /** - * @param callable(Table): void $definition + * Begin a fluent table builder. Terminal methods on the returned {@see Table} + * (`create()`, `alter()`, `drop()`, `dropIfExists()`, `truncate()`, `rename()`) + * compile and return the final {@see Statement}. */ - public function createIfNotExists(string $table, callable $definition): Statement + public function table(string $name): Table { - return $this->create($table, $definition, true); + return new Table($this, $name); } - /** - * @param callable(Table): void $definition - */ - public function create(string $table, callable $definition, bool $ifNotExists = false): Statement + public function compileCreate(Table $table, bool $ifNotExists = false): Statement { - $blueprint = new Table(); - $definition($blueprint); - - if ($blueprint->ttl !== null) { + if ($table->ttl !== null) { throw new UnsupportedException('TTL is only supported in ClickHouse.'); } @@ -55,7 +51,7 @@ public function create(string $table, callable $definition, bool $ifNotExists = $primaryKeys = []; $uniqueColumns = []; - foreach ($blueprint->columns as $column) { + foreach ($table->columns as $column) { $def = $this->compileColumnDefinition($column); $columnDefs[] = $def; @@ -67,21 +63,21 @@ public function create(string $table, callable $definition, bool $ifNotExists = } } - if (! empty($blueprint->compositePrimaryKey) && ! empty($primaryKeys)) { + if (! empty($table->compositePrimaryKey) && ! empty($primaryKeys)) { throw new ValidationException('Cannot combine column-level primary() with Table::primary() composite key.'); } // Raw column definitions (bypass typed Column objects) - foreach ($blueprint->rawColumnDefs as $rawDef) { + foreach ($table->rawColumnDefs as $rawDef) { $columnDefs[] = $rawDef; } // Inline PRIMARY KEY constraint if (! empty($primaryKeys)) { $columnDefs[] = 'PRIMARY KEY (' . \implode(', ', $primaryKeys) . ')'; - } elseif (! empty($blueprint->compositePrimaryKey)) { + } elseif (! empty($table->compositePrimaryKey)) { $columnDefs[] = 'PRIMARY KEY (' - . \implode(', ', \array_map(fn (string $c): string => $this->quote($c), $blueprint->compositePrimaryKey)) + . \implode(', ', \array_map(fn (string $c): string => $this->quote($c), $table->compositePrimaryKey)) . ')'; } @@ -91,12 +87,12 @@ public function create(string $table, callable $definition, bool $ifNotExists = } // Table-level CHECK constraints - foreach ($blueprint->checks as $check) { + foreach ($table->checks as $check) { $columnDefs[] = 'CONSTRAINT ' . $this->quote($check->name) . ' CHECK (' . $check->expression . ')'; } // Indexes - foreach ($blueprint->indexes as $index) { + foreach ($table->indexes as $index) { $keyword = match ($index->type) { IndexType::Unique => 'UNIQUE INDEX', IndexType::Fulltext => 'FULLTEXT INDEX', @@ -108,12 +104,12 @@ public function create(string $table, callable $definition, bool $ifNotExists = } // Raw index definitions (bypass typed Index objects) - foreach ($blueprint->rawIndexDefs as $rawIdx) { + foreach ($table->rawIndexDefs as $rawIdx) { $columnDefs[] = $rawIdx; } // Foreign keys - foreach ($blueprint->foreignKeys as $fk) { + foreach ($table->foreignKeys as $fk) { $def = 'FOREIGN KEY (' . $this->quote($fk->column) . ')' . ' REFERENCES ' . $this->quote($fk->refTable) . ' (' . $this->quote($fk->refColumn) . ')'; @@ -126,30 +122,24 @@ public function create(string $table, callable $definition, bool $ifNotExists = $columnDefs[] = $def; } - $sql = 'CREATE TABLE ' . ($ifNotExists ? 'IF NOT EXISTS ' : '') . $this->quote($table) + $sql = 'CREATE TABLE ' . ($ifNotExists ? 'IF NOT EXISTS ' : '') . $this->quote($table->name) . ' (' . \implode(', ', $columnDefs) . ')'; - if ($blueprint->partitionType !== null) { - $sql .= ' PARTITION BY ' . $blueprint->partitionType->value . '(' . $blueprint->partitionExpression . ')'; - if ($blueprint->partitionCount !== null) { - $sql .= ' PARTITIONS ' . $blueprint->partitionCount; + if ($table->partitionType !== null) { + $sql .= ' PARTITION BY ' . $table->partitionType->value . '(' . $table->partitionExpression . ')'; + if ($table->partitionCount !== null) { + $sql .= ' PARTITIONS ' . $table->partitionCount; } } return new Statement($sql, [], executor: $this->executor); } - /** - * @param callable(Table): void $definition - */ - public function alter(string $table, callable $definition): Statement + public function compileAlter(Table $table): Statement { - $blueprint = new Table(); - $definition($blueprint); - $alterations = []; - foreach ($blueprint->columns as $column) { + foreach ($table->columns as $column) { $keyword = $column->isModify ? 'MODIFY COLUMN' : 'ADD COLUMN'; $def = $keyword . ' ' . $this->compileColumnDefinition($column); if ($column->after !== null) { @@ -158,16 +148,16 @@ public function alter(string $table, callable $definition): Statement $alterations[] = $def; } - foreach ($blueprint->renameColumns as $rename) { + foreach ($table->renameColumns as $rename) { $alterations[] = 'RENAME COLUMN ' . $this->quote($rename->from) . ' TO ' . $this->quote($rename->to); } - foreach ($blueprint->dropColumns as $col) { + foreach ($table->dropColumns as $col) { $alterations[] = 'DROP COLUMN ' . $this->quote($col); } - foreach ($blueprint->indexes as $index) { + foreach ($table->indexes as $index) { $keyword = match ($index->type) { IndexType::Unique => 'ADD UNIQUE INDEX', IndexType::Fulltext => 'ADD FULLTEXT INDEX', @@ -178,11 +168,11 @@ public function alter(string $table, callable $definition): Statement . ' (' . $this->compileIndexColumns($index) . ')'; } - foreach ($blueprint->dropIndexes as $name) { + foreach ($table->dropIndexes as $name) { $alterations[] = 'DROP INDEX ' . $this->quote($name); } - foreach ($blueprint->foreignKeys as $fk) { + foreach ($table->foreignKeys as $fk) { $def = 'ADD FOREIGN KEY (' . $this->quote($fk->column) . ')' . ' REFERENCES ' . $this->quote($fk->refTable) . ' (' . $this->quote($fk->refColumn) . ')'; @@ -195,27 +185,26 @@ public function alter(string $table, callable $definition): Statement $alterations[] = $def; } - foreach ($blueprint->dropForeignKeys as $name) { + foreach ($table->dropForeignKeys as $name) { $alterations[] = 'DROP FOREIGN KEY ' . $this->quote($name); } - $sql = 'ALTER TABLE ' . $this->quote($table) + $sql = 'ALTER TABLE ' . $this->quote($table->name) . ' ' . \implode(', ', $alterations); return new Statement($sql, [], executor: $this->executor); } - public function drop(string $table): Statement + public function compileDrop(string $name, bool $ifExists): Statement { - return new Statement('DROP TABLE ' . $this->quote($table), [], executor: $this->executor); - } - - public function dropIfExists(string $table): Statement - { - return new Statement('DROP TABLE IF EXISTS ' . $this->quote($table), [], executor: $this->executor); + return new Statement( + 'DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $this->quote($name), + [], + executor: $this->executor, + ); } - public function rename(string $from, string $to): Statement + public function compileRename(string $from, string $to): Statement { return new Statement( 'RENAME TABLE ' . $this->quote($from) . ' TO ' . $this->quote($to), @@ -224,9 +213,9 @@ public function rename(string $from, string $to): Statement ); } - public function truncate(string $table): Statement + public function compileTruncate(string $name): Statement { - return new Statement('TRUNCATE TABLE ' . $this->quote($table), [], executor: $this->executor); + return new Statement('TRUNCATE TABLE ' . $this->quote($name), [], executor: $this->executor); } /** diff --git a/src/Query/Schema/ClickHouse.php b/src/Query/Schema/ClickHouse.php index a16717d..e76df4a 100644 --- a/src/Query/Schema/ClickHouse.php +++ b/src/Query/Schema/ClickHouse.php @@ -101,39 +101,34 @@ public function dropIndex(string $table, string $name): Statement ); } - /** - * @param callable(Table): void $definition - */ - public function alter(string $table, callable $definition): Statement + #[\Override] + public function compileAlter(Table $table): Statement { - $blueprint = new Table(); - $definition($blueprint); - $alterations = []; - foreach ($blueprint->columns as $column) { + foreach ($table->columns as $column) { $keyword = $column->isModify ? 'MODIFY COLUMN' : 'ADD COLUMN'; $alterations[] = $keyword . ' ' . $this->compileColumnDefinition($column); } - foreach ($blueprint->renameColumns as $rename) { + foreach ($table->renameColumns as $rename) { $alterations[] = 'RENAME COLUMN ' . $this->quote($rename->from) . ' TO ' . $this->quote($rename->to); } - foreach ($blueprint->dropColumns as $col) { + foreach ($table->dropColumns as $col) { $alterations[] = 'DROP COLUMN ' . $this->quote($col); } - foreach ($blueprint->dropIndexes as $name) { + foreach ($table->dropIndexes as $name) { $alterations[] = 'DROP INDEX ' . $this->quote($name); } - if (! empty($blueprint->foreignKeys)) { + if (! empty($table->foreignKeys)) { throw new UnsupportedException('Foreign keys are not supported in ClickHouse.'); } - if (! empty($blueprint->dropForeignKeys)) { + if (! empty($table->dropForeignKeys)) { throw new UnsupportedException('Foreign keys are not supported in ClickHouse.'); } @@ -141,24 +136,19 @@ public function alter(string $table, callable $definition): Statement throw new ValidationException('ALTER TABLE requires at least one alteration.'); } - $sql = 'ALTER TABLE ' . $this->quote($table) + $sql = 'ALTER TABLE ' . $this->quote($table->name) . ' ' . \implode(', ', $alterations); return new Statement($sql, [], executor: $this->executor); } - /** - * @param callable(Table): void $definition - */ - public function create(string $table, callable $definition, bool $ifNotExists = false): Statement + #[\Override] + public function compileCreate(Table $table, bool $ifNotExists = false): Statement { - $blueprint = new Table(); - $definition($blueprint); - $columnDefs = []; $primaryKeys = []; - foreach ($blueprint->columns as $column) { + foreach ($table->columns as $column) { $def = $this->compileColumnDefinition($column); $columnDefs[] = $def; @@ -167,48 +157,52 @@ public function create(string $table, callable $definition, bool $ifNotExists = } } - if (! empty($blueprint->compositePrimaryKey) && ! empty($primaryKeys)) { + if (! empty($table->compositePrimaryKey) && ! empty($primaryKeys)) { throw new ValidationException('Cannot combine column-level primary() with Table::primary() composite key.'); } - if (empty($primaryKeys) && ! empty($blueprint->compositePrimaryKey)) { - $primaryKeys = \array_map(fn (string $c): string => $this->quote($c), $blueprint->compositePrimaryKey); + if (empty($primaryKeys) && ! empty($table->compositePrimaryKey)) { + $primaryKeys = \array_map(fn (string $c): string => $this->quote($c), $table->compositePrimaryKey); } // Indexes (ClickHouse uses INDEX ... TYPE ... GRANULARITY ...) - foreach ($blueprint->indexes as $index) { + foreach ($table->indexes as $index) { $cols = \array_map(fn (string $c): string => $this->quote($c), $index->columns); $expr = \count($cols) === 1 ? $cols[0] : '(' . \implode(', ', $cols) . ')'; $columnDefs[] = 'INDEX ' . $this->quote($index->name) . ' ' . $expr . ' TYPE minmax GRANULARITY 3'; } - if (! empty($blueprint->foreignKeys)) { + if (! empty($table->foreignKeys)) { throw new UnsupportedException('Foreign keys are not supported in ClickHouse.'); } - if (! empty($blueprint->checks)) { + if (! empty($table->checks)) { throw new UnsupportedException('CHECK constraints are not supported in ClickHouse.'); } - $engine = $blueprint->engine ?? Engine::MergeTree; + $engine = $table->engine ?? Engine::MergeTree; - $sql = 'CREATE TABLE ' . ($ifNotExists ? 'IF NOT EXISTS ' : '') . $this->quote($table) + $sql = 'CREATE TABLE ' . ($ifNotExists ? 'IF NOT EXISTS ' : '') . $this->quote($table->name) . ' (' . \implode(', ', $columnDefs) . ')' - . ' ENGINE = ' . $this->compileEngine($engine, $blueprint->engineArgs); + . ' ENGINE = ' . $this->compileEngine($engine, $table->engineArgs); - if ($blueprint->partitionType !== null) { - $sql .= ' PARTITION BY ' . $blueprint->partitionExpression; + if ($table->partitionType !== null) { + $sql .= ' PARTITION BY ' . $table->partitionExpression; } if ($engine->requiresOrderBy()) { - $sql .= ! empty($primaryKeys) - ? ' ORDER BY (' . \implode(', ', $primaryKeys) . ')' + $orderBy = ! empty($table->orderBy) + ? \array_map(fn (string $c): string => $this->quote($c), $table->orderBy) + : $primaryKeys; + + $sql .= ! empty($orderBy) + ? ' ORDER BY (' . \implode(', ', $orderBy) . ')' : ' ORDER BY tuple()'; } - if ($blueprint->ttl !== null) { - $sql .= ' TTL ' . $blueprint->ttl; + if ($table->ttl !== null) { + $sql .= ' TTL ' . $table->ttl; } return new Statement($sql, [], executor: $this->executor); diff --git a/src/Query/Schema/Column.php b/src/Query/Schema/Column.php index 2d960db..5a81cd1 100644 --- a/src/Query/Schema/Column.php +++ b/src/Query/Schema/Column.php @@ -2,7 +2,9 @@ namespace Utopia\Query\Schema; +use Utopia\Query\Builder\Statement; use Utopia\Query\Exception\ValidationException; +use Utopia\Query\Schema\ClickHouse\Engine; class Column { @@ -50,6 +52,7 @@ class Column public private(set) ?string $userTypeName = null; public function __construct( + public Table $table, public string $name, public ColumnType $type, public ?int $length = null, @@ -86,11 +89,23 @@ public function unique(): static return $this; } - public function primary(): static + /** + * Mark this column as a primary key (no args), or declare a composite + * primary key on the parent table (when an array is passed). + * + * @param list $columns + * + * @phpstan-return ($columns is array{} ? static : Table) + */ + public function primary(array $columns = []): static|Table { - $this->isPrimary = true; + if ($columns === []) { + $this->isPrimary = true; - return $this; + return $this; + } + + return $this->table->primary($columns); } public function after(string $column): static @@ -122,13 +137,28 @@ public function collation(string $collation): static } /** - * @param string[] $values + * Set the allowed values on this enum column (when called with one array + * argument), or add a new enum column to the parent table (when called + * with a name and a list of values). + * + * @param string|string[] $nameOrValues + * @param string[]|null $values + * + * @throws ValidationException if the value list is empty. */ - public function enum(array $values): static + public function enum(string|array $nameOrValues, ?array $values = null): static|Column { - $this->enumValues = $values; + if (\is_array($nameOrValues)) { + if ($nameOrValues === []) { + throw new ValidationException('enum() requires at least one allowed value.'); + } - return $this; + $this->enumValues = $nameOrValues; + + return $this; + } + + return $this->table->enum($nameOrValues, $values ?? []); } public function srid(int $srid): static @@ -153,24 +183,25 @@ public function modify(): static } /** - * Attach a column-level CHECK constraint. + * Attach a CHECK constraint. Called with one argument it sets a column- + * level CHECK on this column; called with two arguments it adds a named + * table-level CHECK constraint via the parent table. * - * The expression is emitted verbatim inside `CHECK (...)` and must come from - * trusted (developer-controlled) source — never from untrusted input. + * @phpstan-return ($expression is null ? static : Table) */ - public function check(string $expression): static + public function check(string $expressionOrName, ?string $expression = null): static|Table { - $this->checkExpression = $expression; + if ($expression === null) { + $this->checkExpression = $expressionOrName; - return $this; + return $this; + } + + return $this->table->check($expressionOrName, $expression); } /** * Mark the column as a generated column computed from the given expression. - * - * The expression is emitted verbatim inside `GENERATED ALWAYS AS (...)` and - * must come from trusted (developer-controlled) source — never from untrusted - * input. */ public function generatedAs(string $expression): static { @@ -179,9 +210,6 @@ public function generatedAs(string $expression): static return $this; } - /** - * Mark a generated column as STORED. Mutually exclusive with {@see virtual()}. - */ public function stored(): static { $this->generatedStored = true; @@ -189,9 +217,6 @@ public function stored(): static return $this; } - /** - * Mark a generated column as VIRTUAL. Mutually exclusive with {@see stored()}. - */ public function virtual(): static { $this->generatedStored = false; @@ -202,10 +227,6 @@ public function virtual(): static /** * Attach a column-level TTL expression (ClickHouse only). * - * Emitted verbatim as `TTL ` inline with the column - * definition. Other dialects throw UnsupportedException when compiling - * the column. - * * @throws ValidationException if the expression is empty or contains a semicolon. */ public function ttl(string $expression): static @@ -228,11 +249,6 @@ public function ttl(string $expression): static /** * Reference a user-defined type (e.g. a PostgreSQL enum type created via CREATE TYPE). * - * The column's emitted type will be the quoted identifier, overriding the mapping - * implied by its ColumnType. Only supported by dialects that implement user-defined - * types (currently PostgreSQL); other dialects throw UnsupportedException when - * compiling the column. - * * @throws ValidationException if $name is not a valid identifier. */ public function userType(string $name): static @@ -245,4 +261,293 @@ public function userType(string $name): static return $this; } + + public function id(string $name = 'id'): Column + { + return $this->table->id($name); + } + + public function string(string $name, int $length = 255): Column + { + return $this->table->string($name, $length); + } + + public function text(string $name): Column + { + return $this->table->text($name); + } + + public function mediumText(string $name): Column + { + return $this->table->mediumText($name); + } + + public function longText(string $name): Column + { + return $this->table->longText($name); + } + + public function integer(string $name): Column + { + return $this->table->integer($name); + } + + public function bigInteger(string $name): Column + { + return $this->table->bigInteger($name); + } + + public function serial(string $name): Column + { + return $this->table->serial($name); + } + + public function bigSerial(string $name): Column + { + return $this->table->bigSerial($name); + } + + public function smallSerial(string $name): Column + { + return $this->table->smallSerial($name); + } + + public function float(string $name): Column + { + return $this->table->float($name); + } + + public function boolean(string $name): Column + { + return $this->table->boolean($name); + } + + public function datetime(string $name, int $precision = 0): Column + { + return $this->table->datetime($name, $precision); + } + + public function timestamp(string $name, int $precision = 0): Column + { + return $this->table->timestamp($name, $precision); + } + + public function json(string $name): Column + { + return $this->table->json($name); + } + + public function binary(string $name): Column + { + return $this->table->binary($name); + } + + public function point(string $name, int $srid = 4326): Column + { + return $this->table->point($name, $srid); + } + + public function linestring(string $name, int $srid = 4326): Column + { + return $this->table->linestring($name, $srid); + } + + public function polygon(string $name, int $srid = 4326): Column + { + return $this->table->polygon($name, $srid); + } + + public function vector(string $name, int $dimensions): Column + { + return $this->table->vector($name, $dimensions); + } + + public function timestamps(int $precision = 3): Table + { + return $this->table->timestamps($precision); + } + + public function addColumn(string $name, ColumnType $type, ?int $lengthOrPrecision = null): Column + { + return $this->table->addColumn($name, $type, $lengthOrPrecision); + } + + public function modifyColumn(string $name, ColumnType $type, ?int $lengthOrPrecision = null): Column + { + return $this->table->modifyColumn($name, $type, $lengthOrPrecision); + } + + public function renameColumn(string $from, string $to): Table + { + return $this->table->renameColumn($from, $to); + } + + public function dropColumn(string $name): Table + { + return $this->table->dropColumn($name); + } + + /** + * @param string[] $columns + * @param array $lengths + * @param array $orders + * @param array $collations + */ + public function index( + array $columns, + string $name = '', + string $method = '', + string $operatorClass = '', + array $lengths = [], + array $orders = [], + array $collations = [], + ): Table { + return $this->table->index($columns, $name, $method, $operatorClass, $lengths, $orders, $collations); + } + + /** + * @param string[] $columns + * @param array $lengths + * @param array $orders + * @param array $collations + */ + public function uniqueIndex( + array $columns, + string $name = '', + array $lengths = [], + array $orders = [], + array $collations = [], + ): Table { + return $this->table->uniqueIndex($columns, $name, $lengths, $orders, $collations); + } + + /** + * @param string[] $columns + */ + public function fulltextIndex(array $columns, string $name = ''): Table + { + return $this->table->fulltextIndex($columns, $name); + } + + /** + * @param string[] $columns + */ + public function spatialIndex(array $columns, string $name = ''): Table + { + return $this->table->spatialIndex($columns, $name); + } + + /** + * @param string[] $columns + * @param array $lengths + * @param array $orders + * @param array $collations + * @param list $rawColumns + */ + public function addIndex( + string $name, + array $columns, + IndexType $type = IndexType::Index, + array $lengths = [], + array $orders = [], + string $method = '', + string $operatorClass = '', + array $collations = [], + array $rawColumns = [], + ): Table { + return $this->table->addIndex($name, $columns, $type, $lengths, $orders, $method, $operatorClass, $collations, $rawColumns); + } + + public function dropIndex(string $name): Table + { + return $this->table->dropIndex($name); + } + + public function foreignKey(string $column): ForeignKey + { + return $this->table->foreignKey($column); + } + + public function addForeignKey(string $column): ForeignKey + { + return $this->table->addForeignKey($column); + } + + public function dropForeignKey(string $name): Table + { + return $this->table->dropForeignKey($name); + } + + public function rawColumn(string $definition): Table + { + return $this->table->rawColumn($definition); + } + + public function rawIndex(string $definition): Table + { + return $this->table->rawIndex($definition); + } + + public function partitionByRange(string $expression): Table + { + return $this->table->partitionByRange($expression); + } + + public function partitionByList(string $expression): Table + { + return $this->table->partitionByList($expression); + } + + public function partitionByHash(string $expression, ?int $partitions = null): Table + { + return $this->table->partitionByHash($expression, $partitions); + } + + public function engine(Engine $engine, string ...$args): Table + { + return $this->table->engine($engine, ...$args); + } + + /** + * @param list $columns + */ + public function orderBy(array $columns): Table + { + return $this->table->orderBy($columns); + } + + public function create(bool $ifNotExists = false): Statement + { + return $this->table->create($ifNotExists); + } + + public function createIfNotExists(): Statement + { + return $this->table->createIfNotExists(); + } + + public function alter(): Statement + { + return $this->table->alter(); + } + + public function drop(): Statement + { + return $this->table->drop(); + } + + public function dropIfExists(): Statement + { + return $this->table->dropIfExists(); + } + + public function truncate(): Statement + { + return $this->table->truncate(); + } + + public function rename(string $to): Statement + { + return $this->table->rename($to); + } } diff --git a/src/Query/Schema/ForeignKey.php b/src/Query/Schema/ForeignKey.php index 83d3d3f..53c621a 100644 --- a/src/Query/Schema/ForeignKey.php +++ b/src/Query/Schema/ForeignKey.php @@ -2,6 +2,9 @@ namespace Utopia\Query\Schema; +use Utopia\Query\Builder\Statement; +use Utopia\Query\Schema\ClickHouse\Engine; + class ForeignKey { public private(set) string $refTable = ''; @@ -13,6 +16,7 @@ class ForeignKey public private(set) ?ForeignKeyAction $onUpdate = null; public function __construct( + public readonly Table $table, public readonly string $column, ) { } @@ -44,4 +48,319 @@ public function onUpdate(ForeignKeyAction $action): static return $this; } + + public function id(string $name = 'id'): Column + { + return $this->table->id($name); + } + + public function string(string $name, int $length = 255): Column + { + return $this->table->string($name, $length); + } + + public function text(string $name): Column + { + return $this->table->text($name); + } + + public function mediumText(string $name): Column + { + return $this->table->mediumText($name); + } + + public function longText(string $name): Column + { + return $this->table->longText($name); + } + + public function integer(string $name): Column + { + return $this->table->integer($name); + } + + public function bigInteger(string $name): Column + { + return $this->table->bigInteger($name); + } + + public function serial(string $name): Column + { + return $this->table->serial($name); + } + + public function bigSerial(string $name): Column + { + return $this->table->bigSerial($name); + } + + public function smallSerial(string $name): Column + { + return $this->table->smallSerial($name); + } + + public function float(string $name): Column + { + return $this->table->float($name); + } + + public function boolean(string $name): Column + { + return $this->table->boolean($name); + } + + public function datetime(string $name, int $precision = 0): Column + { + return $this->table->datetime($name, $precision); + } + + public function timestamp(string $name, int $precision = 0): Column + { + return $this->table->timestamp($name, $precision); + } + + public function json(string $name): Column + { + return $this->table->json($name); + } + + public function binary(string $name): Column + { + return $this->table->binary($name); + } + + /** + * @param string[] $values + */ + public function enum(string $name, array $values): Column + { + return $this->table->enum($name, $values); + } + + public function point(string $name, int $srid = 4326): Column + { + return $this->table->point($name, $srid); + } + + public function linestring(string $name, int $srid = 4326): Column + { + return $this->table->linestring($name, $srid); + } + + public function polygon(string $name, int $srid = 4326): Column + { + return $this->table->polygon($name, $srid); + } + + public function vector(string $name, int $dimensions): Column + { + return $this->table->vector($name, $dimensions); + } + + public function timestamps(int $precision = 3): Table + { + return $this->table->timestamps($precision); + } + + public function addColumn(string $name, ColumnType $type, ?int $lengthOrPrecision = null): Column + { + return $this->table->addColumn($name, $type, $lengthOrPrecision); + } + + public function modifyColumn(string $name, ColumnType $type, ?int $lengthOrPrecision = null): Column + { + return $this->table->modifyColumn($name, $type, $lengthOrPrecision); + } + + public function renameColumn(string $from, string $to): Table + { + return $this->table->renameColumn($from, $to); + } + + public function dropColumn(string $name): Table + { + return $this->table->dropColumn($name); + } + + /** + * @param string[] $columns + * @param array $lengths + * @param array $orders + * @param array $collations + */ + public function index( + array $columns, + string $name = '', + string $method = '', + string $operatorClass = '', + array $lengths = [], + array $orders = [], + array $collations = [], + ): Table { + return $this->table->index($columns, $name, $method, $operatorClass, $lengths, $orders, $collations); + } + + /** + * @param string[] $columns + * @param array $lengths + * @param array $orders + * @param array $collations + */ + public function uniqueIndex( + array $columns, + string $name = '', + array $lengths = [], + array $orders = [], + array $collations = [], + ): Table { + return $this->table->uniqueIndex($columns, $name, $lengths, $orders, $collations); + } + + /** + * @param string[] $columns + */ + public function fulltextIndex(array $columns, string $name = ''): Table + { + return $this->table->fulltextIndex($columns, $name); + } + + /** + * @param string[] $columns + */ + public function spatialIndex(array $columns, string $name = ''): Table + { + return $this->table->spatialIndex($columns, $name); + } + + /** + * @param string[] $columns + * @param array $lengths + * @param array $orders + * @param array $collations + * @param list $rawColumns + */ + public function addIndex( + string $name, + array $columns, + IndexType $type = IndexType::Index, + array $lengths = [], + array $orders = [], + string $method = '', + string $operatorClass = '', + array $collations = [], + array $rawColumns = [], + ): Table { + return $this->table->addIndex($name, $columns, $type, $lengths, $orders, $method, $operatorClass, $collations, $rawColumns); + } + + public function dropIndex(string $name): Table + { + return $this->table->dropIndex($name); + } + + public function foreignKey(string $column): ForeignKey + { + return $this->table->foreignKey($column); + } + + public function addForeignKey(string $column): ForeignKey + { + return $this->table->addForeignKey($column); + } + + public function dropForeignKey(string $name): Table + { + return $this->table->dropForeignKey($name); + } + + /** + * @param list $columns + */ + public function primary(array $columns): Table + { + return $this->table->primary($columns); + } + + public function check(string $name, string $expression): Table + { + return $this->table->check($name, $expression); + } + + public function rawColumn(string $definition): Table + { + return $this->table->rawColumn($definition); + } + + public function rawIndex(string $definition): Table + { + return $this->table->rawIndex($definition); + } + + public function partitionByRange(string $expression): Table + { + return $this->table->partitionByRange($expression); + } + + public function partitionByList(string $expression): Table + { + return $this->table->partitionByList($expression); + } + + public function partitionByHash(string $expression, ?int $partitions = null): Table + { + return $this->table->partitionByHash($expression, $partitions); + } + + public function engine(Engine $engine, string ...$args): Table + { + return $this->table->engine($engine, ...$args); + } + + /** + * @param list $columns + */ + public function orderBy(array $columns): Table + { + return $this->table->orderBy($columns); + } + + public function ttl(string $expression): Table + { + return $this->table->ttl($expression); + } + + public function create(bool $ifNotExists = false): Statement + { + return $this->table->create($ifNotExists); + } + + public function createIfNotExists(): Statement + { + return $this->table->createIfNotExists(); + } + + public function alter(): Statement + { + return $this->table->alter(); + } + + public function drop(): Statement + { + return $this->table->drop(); + } + + public function dropIfExists(): Statement + { + return $this->table->dropIfExists(); + } + + public function truncate(): Statement + { + return $this->table->truncate(); + } + + public function rename(string $to): Statement + { + return $this->table->rename($to); + } } diff --git a/src/Query/Schema/MongoDB.php b/src/Query/Schema/MongoDB.php index 242c2fc..87267cd 100644 --- a/src/Query/Schema/MongoDB.php +++ b/src/Query/Schema/MongoDB.php @@ -44,22 +44,17 @@ protected function compileAutoIncrement(): string return ''; } - /** - * @param callable(Table): void $definition - */ - public function create(string $table, callable $definition, bool $ifNotExists = false): Statement + #[\Override] + public function compileCreate(Table $table, bool $ifNotExists = false): Statement { - $blueprint = new Table(); - $definition($blueprint); - - if (! empty($blueprint->compositePrimaryKey)) { + if (! empty($table->compositePrimaryKey)) { throw new UnsupportedException('Composite primary keys are not supported in MongoDB; documents use "_id" implicitly.'); } $properties = []; $required = []; - foreach ($blueprint->columns as $column) { + foreach ($table->columns as $column) { $bsonType = $this->compileColumnType($column); $prop = ['bsonType' => $bsonType]; @@ -93,7 +88,7 @@ public function create(string $table, callable $definition, bool $ifNotExists = $command = [ 'command' => 'createCollection', - 'collection' => $table, + 'collection' => $table->name, ]; if (! empty($validator)) { @@ -107,22 +102,17 @@ public function create(string $table, callable $definition, bool $ifNotExists = ); } - /** - * @param callable(Table): void $definition - */ - public function alter(string $table, callable $definition): Statement + #[\Override] + public function compileAlter(Table $table): Statement { - $blueprint = new Table(); - $definition($blueprint); - - if (! empty($blueprint->dropColumns) || ! empty($blueprint->renameColumns)) { + if (! empty($table->dropColumns) || ! empty($table->renameColumns)) { throw new UnsupportedException('MongoDB does not support dropping or renaming columns via schema. Use $unset/$rename update operators.'); } $properties = []; $required = []; - foreach ($blueprint->columns as $column) { + foreach ($table->columns as $column) { $bsonType = $this->compileColumnType($column); $prop = ['bsonType' => $bsonType]; @@ -151,7 +141,7 @@ public function alter(string $table, callable $definition): Statement $command = [ 'command' => 'collMod', - 'collection' => $table, + 'collection' => $table->name, ]; if (! empty($validator)) { @@ -165,21 +155,18 @@ public function alter(string $table, callable $definition): Statement ); } - public function drop(string $table): Statement + #[\Override] + public function compileDrop(string $name, bool $ifExists): Statement { return new Statement( - \json_encode(['command' => 'drop', 'collection' => $table], JSON_THROW_ON_ERROR), + \json_encode(['command' => 'drop', 'collection' => $name], JSON_THROW_ON_ERROR), [], executor: $this->executor, ); } - public function dropIfExists(string $table): Statement - { - return $this->drop($table); - } - - public function rename(string $from, string $to): Statement + #[\Override] + public function compileRename(string $from, string $to): Statement { return new Statement( \json_encode([ @@ -192,12 +179,13 @@ public function rename(string $from, string $to): Statement ); } - public function truncate(string $table): Statement + #[\Override] + public function compileTruncate(string $name): Statement { return new Statement( \json_encode([ 'command' => 'deleteMany', - 'collection' => $table, + 'collection' => $name, 'filter' => new stdClass(), ], JSON_THROW_ON_ERROR), [], diff --git a/src/Query/Schema/PostgreSQL.php b/src/Query/Schema/PostgreSQL.php index 73ce42e..5a8bc2e 100644 --- a/src/Query/Schema/PostgreSQL.php +++ b/src/Query/Schema/PostgreSQL.php @@ -278,17 +278,12 @@ private function assertSafeDollarQuotedBody(string $body): void } } - /** - * @param callable(Table): void $definition - */ - public function alter(string $table, callable $definition): Statement + #[\Override] + public function compileAlter(Table $table): Statement { - $blueprint = new Table(); - $definition($blueprint); - $alterations = []; - foreach ($blueprint->columns as $column) { + foreach ($table->columns as $column) { $keyword = $column->isModify ? 'ALTER COLUMN' : 'ADD COLUMN'; if ($column->isModify) { $def = $keyword . ' ' . $this->quote($column->name) @@ -299,16 +294,16 @@ public function alter(string $table, callable $definition): Statement $alterations[] = $def; } - foreach ($blueprint->renameColumns as $rename) { + foreach ($table->renameColumns as $rename) { $alterations[] = 'RENAME COLUMN ' . $this->quote($rename->from) . ' TO ' . $this->quote($rename->to); } - foreach ($blueprint->dropColumns as $col) { + foreach ($table->dropColumns as $col) { $alterations[] = 'DROP COLUMN ' . $this->quote($col); } - foreach ($blueprint->foreignKeys as $fk) { + foreach ($table->foreignKeys as $fk) { $def = 'ADD FOREIGN KEY (' . $this->quote($fk->column) . ')' . ' REFERENCES ' . $this->quote($fk->refTable) . ' (' . $this->quote($fk->refColumn) . ')'; @@ -321,23 +316,23 @@ public function alter(string $table, callable $definition): Statement $alterations[] = $def; } - foreach ($blueprint->dropForeignKeys as $name) { + foreach ($table->dropForeignKeys as $name) { $alterations[] = 'DROP CONSTRAINT ' . $this->quote($name); } $statements = []; if (! empty($alterations)) { - $statements[] = 'ALTER TABLE ' . $this->quote($table) + $statements[] = 'ALTER TABLE ' . $this->quote($table->name) . ' ' . \implode(', ', $alterations); } // PostgreSQL indexes are standalone statements, not ALTER TABLE clauses - foreach ($blueprint->indexes as $index) { + foreach ($table->indexes as $index) { $keyword = $index->type === IndexType::Unique ? 'CREATE UNIQUE INDEX' : 'CREATE INDEX'; $indexSql = $keyword . ' ' . $this->quote($index->name) - . ' ON ' . $this->quote($table); + . ' ON ' . $this->quote($table->name); if ($index->method !== '') { $indexSql .= ' USING ' . \strtoupper($index->method); @@ -347,14 +342,15 @@ public function alter(string $table, callable $definition): Statement $statements[] = $indexSql; } - foreach ($blueprint->dropIndexes as $name) { + foreach ($table->dropIndexes as $name) { $statements[] = 'DROP INDEX ' . $this->quote($name); } return new Statement(\implode('; ', $statements), [], executor: $this->executor); } - public function rename(string $from, string $to): Statement + #[\Override] + public function compileRename(string $from, string $to): Statement { return new Statement( 'ALTER TABLE ' . $this->quote($from) . ' RENAME TO ' . $this->quote($to), diff --git a/src/Query/Schema/SQLite.php b/src/Query/Schema/SQLite.php index 8ae69a6..69e5f70 100644 --- a/src/Query/Schema/SQLite.php +++ b/src/Query/Schema/SQLite.php @@ -35,6 +35,72 @@ protected function compileAutoIncrement(): string return 'AUTOINCREMENT'; } + /** + * SQLite requires `AUTOINCREMENT` to be paired with `INTEGER PRIMARY KEY` + * inline on the same column declaration. Emit those keywords together and + * skip the separate `PRIMARY KEY (col)` clause that the base class adds + * at the end of the column list. + */ + #[\Override] + protected function compileColumnDefinition(Column $column): string + { + if (! $column->isAutoIncrement || ! $column->isPrimary) { + return parent::compileColumnDefinition($column); + } + + $parts = [ + $this->quote($column->name), + $this->compileColumnType($column), + 'PRIMARY KEY', + $this->compileAutoIncrement(), + ]; + + if (! $column->isNullable) { + $parts[] = 'NOT NULL'; + } + + if ($column->hasDefault) { + $parts[] = 'DEFAULT ' . $this->compileDefaultValue($column->default); + } + + if ($column->checkExpression !== null) { + $parts[] = 'CHECK (' . $column->checkExpression . ')'; + } + + return \implode(' ', $parts); + } + + /** + * SQLite emits its primary key inline when paired with `AUTOINCREMENT`, so + * suppress the redundant trailing `PRIMARY KEY (col)` constraint that the + * base compiler would otherwise add. + */ + #[\Override] + public function compileCreate(\Utopia\Query\Schema\Table $table, bool $ifNotExists = false): Statement + { + $hasInlinePrimary = false; + foreach ($table->columns as $column) { + if ($column->isAutoIncrement && $column->isPrimary) { + $hasInlinePrimary = true; + break; + } + } + + if (! $hasInlinePrimary) { + return parent::compileCreate($table, $ifNotExists); + } + + $statement = parent::compileCreate($table, $ifNotExists); + $sql = \preg_replace( + '/, PRIMARY KEY \(`[^`]+`\)/', + '', + $statement->query, + 1, + ); + + return new Statement($sql ?? $statement->query, $statement->bindings, executor: $this->executor); + } + protected function compileUnsigned(): string { return ''; @@ -50,7 +116,8 @@ public function dropDatabase(string $name): Statement throw new UnsupportedException('SQLite does not support DROP DATABASE.'); } - public function rename(string $from, string $to): Statement + #[\Override] + public function compileRename(string $from, string $to): Statement { return new Statement( 'ALTER TABLE ' . $this->quote($from) . ' RENAME TO ' . $this->quote($to), @@ -59,9 +126,10 @@ public function rename(string $from, string $to): Statement ); } - public function truncate(string $table): Statement + #[\Override] + public function compileTruncate(string $name): Statement { - return new Statement('DELETE FROM ' . $this->quote($table), [], executor: $this->executor); + return new Statement('DELETE FROM ' . $this->quote($name), [], executor: $this->executor); } public function dropIndex(string $table, string $name): Statement diff --git a/src/Query/Schema/Table.php b/src/Query/Schema/Table.php index b43a2c3..9274a28 100644 --- a/src/Query/Schema/Table.php +++ b/src/Query/Schema/Table.php @@ -2,7 +2,10 @@ namespace Utopia\Query\Schema; +use Utopia\Query\Builder\Statement; +use Utopia\Query\Exception\UnsupportedException; use Utopia\Query\Exception\ValidationException; +use Utopia\Query\Schema; use Utopia\Query\Schema\ClickHouse\Engine; class Table @@ -40,6 +43,9 @@ class Table /** @var list */ public private(set) array $compositePrimaryKey = []; + /** @var list ClickHouse ORDER BY columns; falls back to primary key when empty */ + public private(set) array $orderBy = []; + public private(set) ?PartitionType $partitionType = null; public private(set) string $partitionExpression = ''; public private(set) ?int $partitionCount = null; @@ -51,13 +57,59 @@ class Table public private(set) ?string $ttl = null; + public function __construct( + private readonly ?Schema $schema = null, + public readonly string $name = '', + ) { + } + + public function create(bool $ifNotExists = false): Statement + { + return $this->requireSchema()->compileCreate($this, $ifNotExists); + } + + public function createIfNotExists(): Statement + { + return $this->create(true); + } + + public function alter(): Statement + { + return $this->requireSchema()->compileAlter($this); + } + + public function drop(): Statement + { + return $this->requireSchema()->compileDrop($this->name, false); + } + + public function dropIfExists(): Statement + { + return $this->requireSchema()->compileDrop($this->name, true); + } + + public function truncate(): Statement + { + return $this->requireSchema()->compileTruncate($this->name); + } + + public function rename(string $to): Statement + { + return $this->requireSchema()->compileRename($this->name, $to); + } + + private function requireSchema(): Schema + { + if ($this->schema === null) { + throw new UnsupportedException('Cannot compile a Table without a Schema. Use Schema::table($name) to obtain a builder.'); + } + + return $this->schema; + } + /** * Add a table-level CHECK constraint. * - * The expression is emitted verbatim inside `CHECK (...)` and must come from - * trusted (developer-controlled) source — never from untrusted input. The - * constraint name is validated as a standard SQL identifier. - * * @throws ValidationException if $name is not a valid identifier. */ public function check(string $name, string $expression): static @@ -95,10 +147,8 @@ public function primary(array $columns): static public function id(string $name = 'id'): Column { - $col = (new Column($name, ColumnType::BigInteger)) - ->unsigned() - ->autoIncrement() - ->primary(); + $col = new Column($this, $name, ColumnType::BigInteger); + $col->unsigned()->autoIncrement()->primary(); $this->columns[] = $col; return $col; @@ -106,7 +156,7 @@ public function id(string $name = 'id'): Column public function string(string $name, int $length = 255): Column { - $col = new Column($name, ColumnType::String, $length); + $col = new Column($this, $name, ColumnType::String, $length); $this->columns[] = $col; return $col; @@ -114,7 +164,7 @@ public function string(string $name, int $length = 255): Column public function text(string $name): Column { - $col = new Column($name, ColumnType::Text); + $col = new Column($this, $name, ColumnType::Text); $this->columns[] = $col; return $col; @@ -122,7 +172,7 @@ public function text(string $name): Column public function mediumText(string $name): Column { - $col = new Column($name, ColumnType::MediumText); + $col = new Column($this, $name, ColumnType::MediumText); $this->columns[] = $col; return $col; @@ -130,7 +180,7 @@ public function mediumText(string $name): Column public function longText(string $name): Column { - $col = new Column($name, ColumnType::LongText); + $col = new Column($this, $name, ColumnType::LongText); $this->columns[] = $col; return $col; @@ -138,7 +188,7 @@ public function longText(string $name): Column public function integer(string $name): Column { - $col = new Column($name, ColumnType::Integer); + $col = new Column($this, $name, ColumnType::Integer); $this->columns[] = $col; return $col; @@ -146,7 +196,7 @@ public function integer(string $name): Column public function bigInteger(string $name): Column { - $col = new Column($name, ColumnType::BigInteger); + $col = new Column($this, $name, ColumnType::BigInteger); $this->columns[] = $col; return $col; @@ -158,7 +208,7 @@ public function bigInteger(string $name): Column */ public function serial(string $name): Column { - $col = (new Column($name, ColumnType::Serial)) + $col = (new Column($this, $name, ColumnType::Serial)) ->autoIncrement(); $this->columns[] = $col; @@ -171,7 +221,7 @@ public function serial(string $name): Column */ public function bigSerial(string $name): Column { - $col = (new Column($name, ColumnType::BigSerial)) + $col = (new Column($this, $name, ColumnType::BigSerial)) ->autoIncrement(); $this->columns[] = $col; @@ -184,7 +234,7 @@ public function bigSerial(string $name): Column */ public function smallSerial(string $name): Column { - $col = (new Column($name, ColumnType::SmallSerial)) + $col = (new Column($this, $name, ColumnType::SmallSerial)) ->autoIncrement(); $this->columns[] = $col; @@ -193,7 +243,7 @@ public function smallSerial(string $name): Column public function float(string $name): Column { - $col = new Column($name, ColumnType::Float); + $col = new Column($this, $name, ColumnType::Float); $this->columns[] = $col; return $col; @@ -201,7 +251,7 @@ public function float(string $name): Column public function boolean(string $name): Column { - $col = new Column($name, ColumnType::Boolean); + $col = new Column($this, $name, ColumnType::Boolean); $this->columns[] = $col; return $col; @@ -209,7 +259,7 @@ public function boolean(string $name): Column public function datetime(string $name, int $precision = 0): Column { - $col = new Column($name, ColumnType::Datetime, precision: $precision); + $col = new Column($this, $name, ColumnType::Datetime, precision: $precision); $this->columns[] = $col; return $col; @@ -217,7 +267,7 @@ public function datetime(string $name, int $precision = 0): Column public function timestamp(string $name, int $precision = 0): Column { - $col = new Column($name, ColumnType::Timestamp, precision: $precision); + $col = new Column($this, $name, ColumnType::Timestamp, precision: $precision); $this->columns[] = $col; return $col; @@ -225,7 +275,7 @@ public function timestamp(string $name, int $precision = 0): Column public function json(string $name): Column { - $col = new Column($name, ColumnType::Json); + $col = new Column($this, $name, ColumnType::Json); $this->columns[] = $col; return $col; @@ -233,7 +283,7 @@ public function json(string $name): Column public function binary(string $name): Column { - $col = new Column($name, ColumnType::Binary); + $col = new Column($this, $name, ColumnType::Binary); $this->columns[] = $col; return $col; @@ -241,10 +291,16 @@ public function binary(string $name): Column /** * @param string[] $values + * + * @throws ValidationException if the value list is empty. */ public function enum(string $name, array $values): Column { - $col = (new Column($name, ColumnType::Enum)) + if ($values === []) { + throw new ValidationException('enum() requires at least one allowed value.'); + } + + $col = (new Column($this, $name, ColumnType::Enum)) ->enum($values); $this->columns[] = $col; @@ -253,7 +309,7 @@ public function enum(string $name, array $values): Column public function point(string $name, int $srid = 4326): Column { - $col = (new Column($name, ColumnType::Point)) + $col = (new Column($this, $name, ColumnType::Point)) ->srid($srid); $this->columns[] = $col; @@ -262,7 +318,7 @@ public function point(string $name, int $srid = 4326): Column public function linestring(string $name, int $srid = 4326): Column { - $col = (new Column($name, ColumnType::Linestring)) + $col = (new Column($this, $name, ColumnType::Linestring)) ->srid($srid); $this->columns[] = $col; @@ -271,7 +327,7 @@ public function linestring(string $name, int $srid = 4326): Column public function polygon(string $name, int $srid = 4326): Column { - $col = (new Column($name, ColumnType::Polygon)) + $col = (new Column($this, $name, ColumnType::Polygon)) ->srid($srid); $this->columns[] = $col; @@ -280,17 +336,19 @@ public function polygon(string $name, int $srid = 4326): Column public function vector(string $name, int $dimensions): Column { - $col = (new Column($name, ColumnType::Vector)) + $col = (new Column($this, $name, ColumnType::Vector)) ->dimensions($dimensions); $this->columns[] = $col; return $col; } - public function timestamps(int $precision = 3): void + public function timestamps(int $precision = 3): static { $this->datetime('created_at', $precision); $this->datetime('updated_at', $precision); + + return $this; } /** @@ -307,11 +365,13 @@ public function index( array $lengths = [], array $orders = [], array $collations = [], - ): void { + ): static { if ($name === '') { $name = 'idx_' . \implode('_', $columns); } $this->indexes[] = new Index($name, $columns, IndexType::Index, $lengths, $orders, $method, $operatorClass, $collations); + + return $this; } /** @@ -326,74 +386,96 @@ public function uniqueIndex( array $lengths = [], array $orders = [], array $collations = [], - ): void { + ): static { if ($name === '') { $name = 'uniq_' . \implode('_', $columns); } $this->indexes[] = new Index($name, $columns, IndexType::Unique, $lengths, $orders, collations: $collations); + + return $this; } /** * @param string[] $columns */ - public function fulltextIndex(array $columns, string $name = ''): void + public function fulltextIndex(array $columns, string $name = ''): static { if ($name === '') { $name = 'ft_' . \implode('_', $columns); } $this->indexes[] = new Index($name, $columns, IndexType::Fulltext); + + return $this; } /** * @param string[] $columns */ - public function spatialIndex(array $columns, string $name = ''): void + public function spatialIndex(array $columns, string $name = ''): static { if ($name === '') { $name = 'sp_' . \implode('_', $columns); } $this->indexes[] = new Index($name, $columns, IndexType::Spatial); + + return $this; } + /** + * Declare a foreign key. The behaviour is identical for create and alter + * contexts — the dialect compiler switches between `FOREIGN KEY (...)` (in + * a CREATE TABLE column list) and `ADD FOREIGN KEY (...)` (in an ALTER + * TABLE clause) when emitting the statement. {@see addForeignKey()} is + * an alias for use in alter chains; both register the same FK exactly once. + */ public function foreignKey(string $column): ForeignKey { - $fk = new ForeignKey($column); + $fk = new ForeignKey($this, $column); $this->foreignKeys[] = $fk; return $fk; } - public function addColumn(string $name, ColumnType|string $type, int|null $lengthOrPrecision = null): Column + public function addColumn(string $name, ColumnType $type, ?int $lengthOrPrecision = null): Column { - if (\is_string($type)) { - $type = ColumnType::from($type); - } - $col = new Column($name, $type, $type === ColumnType::String ? $lengthOrPrecision : null, $type !== ColumnType::String ? $lengthOrPrecision : null); + $col = new Column( + $this, + $name, + $type, + $type === ColumnType::String ? $lengthOrPrecision : null, + $type !== ColumnType::String ? $lengthOrPrecision : null, + ); $this->columns[] = $col; return $col; } - public function modifyColumn(string $name, ColumnType|string $type, int|null $lengthOrPrecision = null): Column + public function modifyColumn(string $name, ColumnType $type, ?int $lengthOrPrecision = null): Column { - if (\is_string($type)) { - $type = ColumnType::from($type); - } - $col = (new Column($name, $type, $type === ColumnType::String ? $lengthOrPrecision : null, $type !== ColumnType::String ? $lengthOrPrecision : null)) - ->modify(); + $col = (new Column( + $this, + $name, + $type, + $type === ColumnType::String ? $lengthOrPrecision : null, + $type !== ColumnType::String ? $lengthOrPrecision : null, + ))->modify(); $this->columns[] = $col; return $col; } - public function renameColumn(string $from, string $to): void + public function renameColumn(string $from, string $to): static { $this->renameColumns[] = new RenameColumn($from, $to); + + return $this; } - public function dropColumn(string $name): void + public function dropColumn(string $name): static { $this->dropColumns[] = $name; + + return $this; } /** @@ -406,36 +488,41 @@ public function dropColumn(string $name): void public function addIndex( string $name, array $columns, - IndexType|string $type = IndexType::Index, + IndexType $type = IndexType::Index, array $lengths = [], array $orders = [], string $method = '', string $operatorClass = '', array $collations = [], array $rawColumns = [], - ): void { - if (\is_string($type)) { - $type = IndexType::from($type); - } + ): static { $this->indexes[] = new Index($name, $columns, $type, $lengths, $orders, $method, $operatorClass, $collations, $rawColumns); + + return $this; } - public function dropIndex(string $name): void + public function dropIndex(string $name): static { $this->dropIndexes[] = $name; + + return $this; } + /** + * Alias of {@see foreignKey()}, for symmetry with the other `add*`/`drop*` + * alter helpers. Returns the same registered {@see ForeignKey}; calling + * both methods for the same column registers the FK twice. + */ public function addForeignKey(string $column): ForeignKey { - $fk = new ForeignKey($column); - $this->foreignKeys[] = $fk; - - return $fk; + return $this->foreignKey($column); } - public function dropForeignKey(string $name): void + public function dropForeignKey(string $name): static { $this->dropForeignKeys[] = $name; + + return $this; } /** @@ -443,9 +530,11 @@ public function dropForeignKey(string $name): void * * Example: $table->rawColumn('`my_col` VARCHAR(255) NOT NULL DEFAULT ""') */ - public function rawColumn(string $definition): void + public function rawColumn(string $definition): static { $this->rawColumnDefs[] = $definition; + + return $this; } /** @@ -453,23 +542,29 @@ public function rawColumn(string $definition): void * * Example: $table->rawIndex('INDEX `idx_name` (`col1`, `col2`)') */ - public function rawIndex(string $definition): void + public function rawIndex(string $definition): static { $this->rawIndexDefs[] = $definition; + + return $this; } - public function partitionByRange(string $expression): void + public function partitionByRange(string $expression): static { $this->partitionType = PartitionType::Range; $this->partitionExpression = $expression; $this->partitionCount = null; + + return $this; } - public function partitionByList(string $expression): void + public function partitionByList(string $expression): static { $this->partitionType = PartitionType::List; $this->partitionExpression = $expression; $this->partitionCount = null; + + return $this; } /** @@ -520,10 +615,28 @@ public function engine(Engine $engine, string ...$args): static } /** - * Attach a table-level TTL expression (ClickHouse only). + * Set the ClickHouse ORDER BY clause. When unset, ClickHouse falls back to the + * primary key columns. * - * Emitted verbatim as `TTL ` after ORDER BY/PARTITION BY. - * Other dialects throw UnsupportedException when compiling the blueprint. + * @param list $columns + * + * @throws ValidationException if any column name is not a valid identifier. + */ + public function orderBy(array $columns): static + { + foreach ($columns as $column) { + if (! \preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $column)) { + throw new ValidationException('Invalid column name in ORDER BY: ' . $column); + } + } + + $this->orderBy = $columns; + + return $this; + } + + /** + * Attach a table-level TTL expression (ClickHouse only). * * @throws ValidationException if the expression is empty or contains a semicolon. */ diff --git a/tests/Integration/MongoDBClient.php b/tests/Integration/MongoDBClient.php index ec0cc48..1e3c47e 100644 --- a/tests/Integration/MongoDBClient.php +++ b/tests/Integration/MongoDBClient.php @@ -74,10 +74,42 @@ public function command(string $commandJson): void 'createIndex' => $this->createIndex($op), 'dropIndex' => $this->database->selectCollection($collectionName)->dropIndex($indexName), 'deleteMany' => $this->database->selectCollection($collectionName)->deleteMany($filter), + 'collMod' => $this->collMod($collectionName, $op), + 'renameCollection' => $this->renameCollection($op), default => throw new \RuntimeException('Unknown MongoDB command: ' . $command), }; } + /** + * @param array $op + */ + private function collMod(string $collection, array $op): void + { + $command = ['collMod' => $collection]; + if (isset($op['validator'])) { + $command['validator'] = $op['validator']; + } + $this->database->command($command); + } + + /** + * @param array $op + */ + private function renameCollection(array $op): void + { + /** @var string $from */ + $from = $op['from'] ?? ''; + /** @var string $to */ + $to = $op['to'] ?? ''; + + $databaseName = $this->database->getDatabaseName(); + + $this->database->getManager()->executeCommand('admin', new \MongoDB\Driver\Command([ + 'renameCollection' => $databaseName . '.' . $from, + 'to' => $databaseName . '.' . $to, + ])); + } + public function dropCollection(string $name): void { $this->database->dropCollection($name); diff --git a/tests/Integration/Schema/ClickHouseIntegrationTest.php b/tests/Integration/Schema/ClickHouseIntegrationTest.php index c20b10b..2c6f394 100644 --- a/tests/Integration/Schema/ClickHouseIntegrationTest.php +++ b/tests/Integration/Schema/ClickHouseIntegrationTest.php @@ -6,7 +6,6 @@ use Utopia\Query\Schema\ClickHouse; use Utopia\Query\Schema\ClickHouse\Engine; use Utopia\Query\Schema\ColumnType; -use Utopia\Query\Schema\Table; class ClickHouseIntegrationTest extends IntegrationTestCase { @@ -23,11 +22,11 @@ public function testCreateTableWithMergeTreeEngine(): void $table = 'test_mergetree_' . uniqid(); $this->trackClickhouseTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('name', 100); - $bp->integer('value'); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->string('name', 100) + ->integer('value') + ->create(); $this->clickhouseStatement($result->query); @@ -52,11 +51,11 @@ public function testCreateTableWithNullableColumns(): void $table = 'test_nullable_' . uniqid(); $this->trackClickhouseTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('optional_name', 100)->nullable(); - $bp->integer('optional_count')->nullable(); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->string('optional_name', 100)->nullable() + ->integer('optional_count')->nullable() + ->create(); $this->clickhouseStatement($result->query); @@ -83,14 +82,14 @@ public function testAlterTableAddColumn(): void $table = 'test_alter_add_' . uniqid(); $this->trackClickhouseTable($table); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->create(); $this->clickhouseStatement($create->query); - $alter = $this->schema->alter($table, function (Table $bp) { - $bp->addColumn('description', ColumnType::String, 200); - }); + $alter = $this->schema->table($table) + ->addColumn('description', ColumnType::String, 200) + ->alter(); $this->clickhouseStatement($alter->query); $ch = $this->connectClickhouse(); @@ -106,12 +105,12 @@ public function testDropTable(): void { $table = 'test_drop_' . uniqid(); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->create(); $this->clickhouseStatement($create->query); - $drop = $this->schema->drop($table); + $drop = $this->schema->table($table)->drop(); $this->clickhouseStatement($drop->query); $ch = $this->connectClickhouse(); @@ -127,11 +126,11 @@ public function testCreateTableWithDateTimePrecision(): void $table = 'test_dt64_' . uniqid(); $this->trackClickhouseTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->datetime('created_at', 3); - $bp->datetime('updated_at', 6); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->datetime('created_at', 3) + ->datetime('updated_at', 6) + ->create(); $this->clickhouseStatement($result->query); @@ -157,12 +156,12 @@ public function testCreateReplacingMergeTree(): void $table = 'test_replacing_' . uniqid(); $this->trackClickhouseTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->unsigned()->primary(); - $bp->string('name'); - $bp->integer('version')->unsigned(); - $bp->engine(Engine::ReplacingMergeTree, 'version'); - }); + $result = $this->schema->table($table) + ->integer('id')->unsigned()->primary() + ->string('name') + ->integer('version')->unsigned() + ->engine(Engine::ReplacingMergeTree, 'version') + ->create(); $this->clickhouseStatement($result->query); @@ -190,11 +189,11 @@ public function testCreateSummingMergeTree(): void $table = 'test_summing_' . uniqid(); $this->trackClickhouseTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('key')->unsigned()->primary(); - $bp->bigInteger('total')->unsigned(); - $bp->engine(Engine::SummingMergeTree, 'total'); - }); + $result = $this->schema->table($table) + ->integer('key')->unsigned()->primary() + ->bigInteger('total')->unsigned() + ->engine(Engine::SummingMergeTree, 'total') + ->create(); $this->clickhouseStatement($result->query); @@ -295,11 +294,11 @@ public function testCreateTableWithPartitionBy(): void $table = 'test_partition_' . uniqid(); $this->trackClickhouseTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->datetime('ts'); - $bp->partitionByHash('toYYYYMM(`ts`)'); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->datetime('ts') + ->partitionByHash('toYYYYMM(`ts`)') + ->create(); $this->assertStringContainsString('PARTITION BY toYYYYMM(`ts`)', $result->query); diff --git a/tests/Integration/Schema/FluentBuilderIntegrationTest.php b/tests/Integration/Schema/FluentBuilderIntegrationTest.php new file mode 100644 index 0000000..6928a72 --- /dev/null +++ b/tests/Integration/Schema/FluentBuilderIntegrationTest.php @@ -0,0 +1,634 @@ +foreignKey()->...->string()->create()`) + * - Table terminals: createIfNotExists, dropIfExists, rename, truncate + * - Table::orderBy() (ClickHouse explicit ORDER BY) + * + * Each test runs a real CREATE TABLE / INSERT / SELECT round-trip against the + * docker-compose test stack to confirm the emitted SQL is accepted by the + * target engine and behaves as expected. + */ +class FluentBuilderIntegrationTest extends IntegrationTestCase +{ + public function testMysqlColumnPrimaryArrayDispatchCreatesCompositeKey(): void + { + $table = 'fluent_pk_' . uniqid(); + $this->trackMysqlTable($table); + $schema = new MySQL(); + + $stmt = $schema->table($table) + ->integer('order_id') + ->integer('product_id') + ->primary(['order_id', 'product_id']) + ->create(); + + $this->mysqlStatement($stmt->query); + + $row = $this->fetchOneMysql( + "SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY ORDINAL_POSITION) AS cols " + . "FROM information_schema.KEY_COLUMN_USAGE " + . "WHERE TABLE_SCHEMA = 'query_test' AND TABLE_NAME = '{$table}' " + . "AND CONSTRAINT_NAME = 'PRIMARY'" + ); + + $this->assertSame('order_id,product_id', (string) $row['cols']); // @phpstan-ignore cast.string + } + + public function testPostgresColumnPrimaryArrayDispatchCreatesCompositeKey(): void + { + $table = 'fluent_pk_' . uniqid(); + $this->trackPostgresTable($table); + $schema = new PostgreSQL(); + + $stmt = $schema->table($table) + ->integer('tenant_id') + ->integer('event_id') + ->primary(['tenant_id', 'event_id']) + ->create(); + + $this->postgresStatement($stmt->query); + + $rows = $this->fetchColumnPostgres( + "SELECT a.attname FROM pg_index i " + . "JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) " + . "WHERE i.indrelid = '\"{$table}\"'::regclass AND i.indisprimary " + . 'ORDER BY array_position(i.indkey, a.attnum)' + ); + + $this->assertSame(['tenant_id', 'event_id'], $rows); + } + + public function testSqliteColumnPrimaryArrayDispatchCreatesCompositeKey(): void + { + $table = 'fluent_pk_' . uniqid(); + $schema = new SQLite(); + + $stmt = $schema->table($table) + ->integer('a') + ->integer('b') + ->primary(['a', 'b']) + ->create(); + + $this->sqliteStatement($stmt->query); + + $rows = $this->fetchAllSqlite("PRAGMA table_info('{$table}')"); + + $pkCols = []; + foreach ($rows as $r) { + if ((int) $r['pk'] > 0) { // @phpstan-ignore cast.int + $pkCols[(int) $r['pk']] = $r['name']; // @phpstan-ignore cast.int + } + } + \ksort($pkCols); + + $this->assertSame(['a', 'b'], \array_values($pkCols)); + } + + public function testClickhouseColumnPrimaryArrayDispatchPopulatesOrderBy(): void + { + $table = 'fluent_pk_' . uniqid(); + $this->trackClickhouseTable($table); + $schema = new ClickHouse(); + + $stmt = $schema->table($table) + ->integer('a')->unsigned() + ->integer('b')->unsigned() + ->primary(['a', 'b']) + ->create(); + + $this->clickhouseStatement($stmt->query); + + $ch = $this->connectClickhouse(); + $rows = $ch->execute( + "SELECT sorting_key FROM system.tables WHERE database = 'query_test' AND name = '{$table}'" + ); + + $this->assertSame('a, b', $rows[0]['sorting_key']); + } + + public function testMysqlColumnCheckTwoArgDispatchCreatesTableLevelCheck(): void + { + $table = 'fluent_check_' . uniqid(); + $this->trackMysqlTable($table); + $schema = new MySQL(); + + $stmt = $schema->table($table) + ->id() + ->integer('age') + ->check('age_range', '`age` >= 0 AND `age` < 150') + ->create(); + + $this->mysqlStatement($stmt->query); + + $pdo = $this->connectMysql(); + $insertOk = $pdo->prepare("INSERT INTO `{$table}` (`age`) VALUES (42)"); + \assert($insertOk !== false); + $insertOk->execute(); + + $rejected = false; + try { + $stmt = $pdo->prepare("INSERT INTO `{$table}` (`age`) VALUES (200)"); + \assert($stmt !== false); + $stmt->execute(); + } catch (\PDOException) { + $rejected = true; + } + + $this->assertTrue($rejected, 'Two-arg check forwarder should produce table-level CHECK constraint'); + } + + public function testPostgresColumnCheckTwoArgDispatchCreatesTableLevelCheck(): void + { + $table = 'fluent_check_' . uniqid(); + $this->trackPostgresTable($table); + $schema = new PostgreSQL(); + + $stmt = $schema->table($table) + ->id() + ->integer('age') + ->check('age_min', '"age" >= 18') + ->create(); + + $this->postgresStatement($stmt->query); + + $pdo = $this->connectPostgres(); + $ok = $pdo->prepare("INSERT INTO \"{$table}\" (\"age\") VALUES (25)"); + \assert($ok !== false); + $ok->execute(); + + $rejected = false; + try { + $bad = $pdo->prepare("INSERT INTO \"{$table}\" (\"age\") VALUES (5)"); + \assert($bad !== false); + $bad->execute(); + } catch (\PDOException) { + $rejected = true; + } + + $this->assertTrue($rejected); + } + + public function testSqliteColumnCheckTwoArgDispatchCreatesTableLevelCheck(): void + { + $table = 'fluent_check_' . uniqid(); + $schema = new SQLite(); + + $stmt = $schema->table($table) + ->id() + ->integer('score') + ->check('score_range', '"score" BETWEEN 0 AND 100') + ->create(); + + $this->sqliteStatement($stmt->query); + + $pdo = $this->connectSqlite(); + $ok = $pdo->prepare("INSERT INTO `{$table}` (`score`) VALUES (50)"); + \assert($ok !== false); + $ok->execute(); + + $rejected = false; + try { + $bad = $pdo->prepare("INSERT INTO `{$table}` (`score`) VALUES (200)"); + \assert($bad !== false); + $bad->execute(); + } catch (\PDOException) { + $rejected = true; + } + + $this->assertTrue($rejected); + } + + public function testMysqlColumnEnumTwoArgDispatchAddsSiblingEnumColumn(): void + { + $table = 'fluent_enum_' . uniqid(); + $this->trackMysqlTable($table); + $schema = new MySQL(); + + $stmt = $schema->table($table) + ->id() + ->string('label', 50) + ->enum('status', ['draft', 'live']) + ->create(); + + $this->mysqlStatement($stmt->query); + + $row = $this->fetchOneMysql( + "SELECT COLUMN_TYPE AS ct FROM information_schema.COLUMNS " + . "WHERE TABLE_SCHEMA = 'query_test' AND TABLE_NAME = '{$table}' AND COLUMN_NAME = 'status'" + ); + + $this->assertSame("enum('draft','live')", (string) $row['ct']); // @phpstan-ignore cast.string + } + + public function testMysqlForeignKeyForwarderMidChainContinuesColumnsAfterFK(): void + { + $parent = 'fluent_fk_parent_' . uniqid(); + $child = 'fluent_fk_child_' . uniqid(); + $this->trackMysqlTable($child); + $this->trackMysqlTable($parent); + $schema = new MySQL(); + + $this->mysqlStatement($schema->table($parent)->id()->string('name', 50)->create()->query); + + $stmt = $schema->table($child) + ->id() + ->bigInteger('parent_id')->unsigned() + ->foreignKey('parent_id')->references('id')->on($parent)->onDelete(ForeignKeyAction::Cascade) + ->string('extra_col', 50) + ->boolean('flag') + ->create(); + + $this->mysqlStatement($stmt->query); + + $cols = $this->fetchColumnMysql( + "SELECT COLUMN_NAME FROM information_schema.COLUMNS " + . "WHERE TABLE_SCHEMA = 'query_test' AND TABLE_NAME = '{$child}' " + . 'ORDER BY ORDINAL_POSITION' + ); + $this->assertSame(['id', 'parent_id', 'extra_col', 'flag'], $cols); + + $fkRow = $this->fetchOneMysql( + "SELECT DELETE_RULE AS dr FROM information_schema.REFERENTIAL_CONSTRAINTS " + . "WHERE CONSTRAINT_SCHEMA = 'query_test' AND TABLE_NAME = '{$child}'" + ); + $this->assertSame('CASCADE', (string) $fkRow['dr']); // @phpstan-ignore cast.string + } + + public function testPostgresForeignKeyForwarderMidChainContinuesColumnsAfterFK(): void + { + $parent = 'fluent_fk_parent_' . uniqid(); + $child = 'fluent_fk_child_' . uniqid(); + $this->trackPostgresTable($child); + $this->trackPostgresTable($parent); + $schema = new PostgreSQL(); + + $this->postgresStatement($schema->table($parent)->id()->string('name', 50)->create()->query); + + $stmt = $schema->table($child) + ->id() + ->integer('parent_id') + ->foreignKey('parent_id')->references('id')->on($parent)->onDelete(ForeignKeyAction::Cascade) + ->string('extra_col', 50) + ->boolean('flag') + ->create(); + + $this->postgresStatement($stmt->query); + + $cols = $this->fetchColumnPostgres( + "SELECT column_name FROM information_schema.columns " + . "WHERE table_catalog = 'query_test' AND table_schema = 'public' AND table_name = '{$child}' " + . 'ORDER BY ordinal_position' + ); + $this->assertSame(['id', 'parent_id', 'extra_col', 'flag'], $cols); + } + + public function testMysqlCreateIfNotExistsIsIdempotent(): void + { + $table = 'fluent_ine_' . uniqid(); + $this->trackMysqlTable($table); + $schema = new MySQL(); + + $stmt = $schema->table($table)->id()->string('name')->createIfNotExists(); + $this->mysqlStatement($stmt->query); + $this->mysqlStatement($stmt->query); // idempotent — must not throw + + $row = $this->fetchOneMysql( + "SELECT COUNT(*) AS cnt FROM information_schema.TABLES " + . "WHERE TABLE_SCHEMA = 'query_test' AND TABLE_NAME = '{$table}'" + ); + $this->assertSame('1', (string) $row['cnt']); // @phpstan-ignore cast.string + } + + public function testMysqlDropIfExistsTolerantOfMissingTable(): void + { + $missing = 'fluent_missing_' . uniqid(); + $schema = new MySQL(); + + $stmt = $schema->table($missing)->dropIfExists(); + $this->mysqlStatement($stmt->query); // must not throw on missing table + + $this->addToAssertionCount(1); + } + + public function testMysqlRenameRoundTrip(): void + { + $from = 'fluent_rn_a_' . uniqid(); + $to = 'fluent_rn_b_' . uniqid(); + $this->trackMysqlTable($from); + $this->trackMysqlTable($to); + $schema = new MySQL(); + + $this->mysqlStatement($schema->table($from)->id()->create()->query); + $this->mysqlStatement($schema->table($from)->rename($to)->query); + + $row = $this->fetchOneMysql( + "SELECT COUNT(*) AS cnt FROM information_schema.TABLES " + . "WHERE TABLE_SCHEMA = 'query_test' AND TABLE_NAME = '{$to}'" + ); + $this->assertSame('1', (string) $row['cnt']); // @phpstan-ignore cast.string + } + + public function testPostgresRenameRoundTrip(): void + { + $from = 'fluent_rn_a_' . uniqid(); + $to = 'fluent_rn_b_' . uniqid(); + $this->trackPostgresTable($from); + $this->trackPostgresTable($to); + $schema = new PostgreSQL(); + + $this->postgresStatement($schema->table($from)->id()->create()->query); + $this->postgresStatement($schema->table($from)->rename($to)->query); + + $row = $this->fetchOnePostgres( + "SELECT COUNT(*) AS cnt FROM information_schema.tables " + . "WHERE table_catalog = 'query_test' AND table_schema = 'public' AND table_name = '{$to}'" + ); + $this->assertSame('1', (string) $row['cnt']); // @phpstan-ignore cast.string + } + + public function testSqliteRenameRoundTrip(): void + { + $from = 'fluent_rn_a_' . uniqid(); + $to = 'fluent_rn_b_' . uniqid(); + $schema = new SQLite(); + + $this->sqliteStatement($schema->table($from)->id()->create()->query); + $this->sqliteStatement($schema->table($from)->rename($to)->query); + + $row = $this->fetchOneSqlite( + "SELECT COUNT(*) AS cnt FROM sqlite_master WHERE type = 'table' AND name = '{$to}'" + ); + $this->assertSame(1, (int) $row['cnt']); // @phpstan-ignore cast.int + } + + public function testMysqlTruncateRemovesRowsKeepsTable(): void + { + $table = 'fluent_tr_' . uniqid(); + $this->trackMysqlTable($table); + $schema = new MySQL(); + + $this->mysqlStatement($schema->table($table)->id()->string('name')->create()->query); + $pdo = $this->connectMysql(); + $insert = $pdo->prepare("INSERT INTO `{$table}` (`name`) VALUES ('a'), ('b'), ('c')"); + \assert($insert !== false); + $insert->execute(); + + $this->mysqlStatement($schema->table($table)->truncate()->query); + + $row = $this->fetchOneMysql("SELECT COUNT(*) AS cnt FROM `{$table}`"); + $this->assertSame('0', (string) $row['cnt']); // @phpstan-ignore cast.string + + $existsRow = $this->fetchOneMysql( + "SELECT COUNT(*) AS cnt FROM information_schema.TABLES " + . "WHERE TABLE_SCHEMA = 'query_test' AND TABLE_NAME = '{$table}'" + ); + $this->assertSame('1', (string) $existsRow['cnt']); // @phpstan-ignore cast.string + } + + public function testSqliteTruncateRemovesRowsKeepsTable(): void + { + $table = 'fluent_tr_' . uniqid(); + $schema = new SQLite(); + + $this->sqliteStatement($schema->table($table)->id()->string('name')->create()->query); + $pdo = $this->connectSqlite(); + $insert = $pdo->prepare("INSERT INTO `{$table}` (`name`) VALUES ('a'), ('b')"); + \assert($insert !== false); + $insert->execute(); + + $this->sqliteStatement($schema->table($table)->truncate()->query); + + $row = $this->fetchOneSqlite("SELECT COUNT(*) AS cnt FROM `{$table}`"); + $this->assertSame(0, (int) $row['cnt']); // @phpstan-ignore cast.int + } + + public function testClickhouseOrderByExplicitlyControlsSortingKey(): void + { + $table = 'fluent_orderby_' . uniqid(); + $this->trackClickhouseTable($table); + $schema = new ClickHouse(); + + $stmt = $schema->table($table) + ->string('tenantId') + ->string('eventId') + ->datetime('createdAt') + ->primary(['tenantId', 'eventId']) + ->engine(Engine::MergeTree) + ->orderBy(['tenantId', 'createdAt']) + ->create(); + + $this->clickhouseStatement($stmt->query); + + $ch = $this->connectClickhouse(); + $row = $ch->execute( + "SELECT sorting_key FROM system.tables WHERE database = 'query_test' AND name = '{$table}'" + ); + + $this->assertSame('tenantId, createdAt', $row[0]['sorting_key']); + } + + public function testClickhouseOrderByFallsBackToPrimaryWhenUnset(): void + { + $table = 'fluent_orderby_pk_' . uniqid(); + $this->trackClickhouseTable($table); + $schema = new ClickHouse(); + + $stmt = $schema->table($table) + ->integer('id')->unsigned()->primary() + ->string('value') + ->engine(Engine::MergeTree) + ->create(); + + $this->clickhouseStatement($stmt->query); + + $ch = $this->connectClickhouse(); + $row = $ch->execute( + "SELECT sorting_key FROM system.tables WHERE database = 'query_test' AND name = '{$table}'" + ); + + $this->assertSame('id', $row[0]['sorting_key']); + } + + public function testClickhouseEndToEndExampleFromReadme(): void + { + $table = 'events_readme_' . uniqid(); + $this->trackClickhouseTable($table); + $schema = new ClickHouse(); + + $stmt = $schema->table($table) + ->string('tenantId') + ->string('eventId') + ->string('payload')->nullable() + ->datetime('createdAt') + ->primary(['tenantId', 'eventId']) + ->engine(Engine::MergeTree) + ->orderBy(['tenantId', 'createdAt']) + ->ttl('createdAt + INTERVAL 90 DAY') + ->create(); + + $this->clickhouseStatement($stmt->query); + + $ch = $this->connectClickhouse(); + + $engineRow = $ch->execute( + "SELECT engine, sorting_key, engine_full FROM system.tables " + . "WHERE database = 'query_test' AND name = '{$table}'" + ); + $this->assertSame('MergeTree', $engineRow[0]['engine']); + $this->assertSame('tenantId, createdAt', $engineRow[0]['sorting_key']); + $engineFull = $engineRow[0]['engine_full']; + \assert(\is_string($engineFull)); + $this->assertStringContainsString('TTL', $engineFull); + + // Round-trip with a far-future timestamp so the 90-day TTL on + // createdAt cannot collect rows during background merges before + // SELECT runs. + $future = (new \DateTimeImmutable('+1 year'))->format('Y-m-d H:i:s'); + $this->clickhouseStatement( + "INSERT INTO `{$table}` (tenantId, eventId, payload, createdAt) VALUES " + . "('t1', 'e1', NULL, '{$future}'), " + . "('t1', 'e2', 'p2', '{$future}')" + ); + $rows = $ch->execute("SELECT eventId, payload FROM `{$table}` ORDER BY eventId"); + $this->assertCount(2, $rows); + $this->assertNull($rows[0]['payload']); + $this->assertSame('p2', $rows[1]['payload']); + } + + public function testMongoFluentTerminalsAndForwardersRoundTrip(): void + { + $collection = 'fluent_mongo_' . uniqid(); + $this->trackMongoCollection($collection); + + $this->connectMongoDB(); + $mongo = $this->mongoClient; + \assert($mongo !== null); + + $schema = new MongoDB(); + + $createPlan = $schema->table($collection) + ->string('name', 100) + ->integer('age')->nullable() + ->boolean('active')->default(true) + ->createIfNotExists(); + $mongo->command($createPlan->query); + + $this->assertContains($collection, $mongo->listCollectionNames()); + + $alterPlan = $schema->table($collection) + ->string('phone', 20)->nullable()->comment('contact phone') + ->alter(); + $mongo->command($alterPlan->query); + + $mongo->insertOne($collection, ['name' => 'a', 'active' => true]); + $mongo->insertOne($collection, ['name' => 'b', 'active' => true]); + $mongo->command($schema->table($collection)->truncate()->query); + $this->assertContains($collection, $mongo->listCollectionNames()); + + $renamed = $collection . '_renamed'; + $this->trackMongoCollection($renamed); + $mongo->command($schema->table($collection)->rename($renamed)->query); + $this->assertContains($renamed, $mongo->listCollectionNames()); + $this->assertNotContains($collection, $mongo->listCollectionNames()); + + $mongo->command($schema->table($renamed)->drop()->query); + $this->assertNotContains($renamed, $mongo->listCollectionNames()); + } + + /** + * @return array + */ + private function fetchOneMysql(string $sql): array + { + $stmt = $this->connectMysql()->query($sql); + \assert($stmt !== false); + /** @var array|false $row */ + $row = $stmt->fetch(PDO::FETCH_ASSOC); + \assert(\is_array($row)); + + return $row; + } + + /** + * @return list + */ + private function fetchColumnMysql(string $sql): array + { + $stmt = $this->connectMysql()->query($sql); + \assert($stmt !== false); + + /** @var list */ + return $stmt->fetchAll(PDO::FETCH_COLUMN); + } + + /** + * @return array + */ + private function fetchOnePostgres(string $sql): array + { + $stmt = $this->connectPostgres()->query($sql); + \assert($stmt !== false); + /** @var array|false $row */ + $row = $stmt->fetch(PDO::FETCH_ASSOC); + \assert(\is_array($row)); + + return $row; + } + + /** + * @return list + */ + private function fetchColumnPostgres(string $sql): array + { + $stmt = $this->connectPostgres()->query($sql); + \assert($stmt !== false); + + /** @var list */ + return $stmt->fetchAll(PDO::FETCH_COLUMN); + } + + /** + * @return array + */ + private function fetchOneSqlite(string $sql): array + { + $stmt = $this->connectSqlite()->query($sql); + \assert($stmt !== false); + /** @var array|false $row */ + $row = $stmt->fetch(PDO::FETCH_ASSOC); + \assert(\is_array($row)); + + return $row; + } + + /** + * @return list> + */ + private function fetchAllSqlite(string $sql): array + { + $stmt = $this->connectSqlite()->query($sql); + \assert($stmt !== false); + + /** @var list> */ + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } +} diff --git a/tests/Integration/Schema/MongoDBIntegrationTest.php b/tests/Integration/Schema/MongoDBIntegrationTest.php index 4075287..3144937 100644 --- a/tests/Integration/Schema/MongoDBIntegrationTest.php +++ b/tests/Integration/Schema/MongoDBIntegrationTest.php @@ -5,7 +5,6 @@ use MongoDB\Driver\Exception\BulkWriteException; use Tests\Integration\IntegrationTestCase; use Utopia\Query\Schema\MongoDB; -use Utopia\Query\Schema\Table; class MongoDBIntegrationTest extends IntegrationTestCase { @@ -23,11 +22,11 @@ public function testCreateCollection(): void $collection = 'schema_create_' . \uniqid(); $this->trackMongoCollection($collection); - $plan = $this->schema->create($collection, function (Table $bp) { - $bp->integer('id'); - $bp->string('name', 100); - $bp->integer('age')->nullable(); - }); + $plan = $this->schema->table($collection) + ->integer('id') + ->string('name', 100) + ->integer('age')->nullable() + ->create(); $mongo = $this->mongoClient; $this->assertNotNull($mongo); @@ -59,10 +58,10 @@ public function testCreateIndexSingleField(): void $mongo = $this->mongoClient; $this->assertNotNull($mongo); - $mongo->command($this->schema->create($collection, function (Table $bp) { - $bp->integer('id'); - $bp->string('email', 255); - })->query); + $mongo->command($this->schema->table($collection) + ->integer('id') + ->string('email', 255) + ->create()->query); $mongo->command($this->schema->createIndex($collection, 'idx_email', ['email'])->query); @@ -78,11 +77,11 @@ public function testCreateIndexCompound(): void $mongo = $this->mongoClient; $this->assertNotNull($mongo); - $mongo->command($this->schema->create($collection, function (Table $bp) { - $bp->integer('id'); - $bp->string('country', 32); - $bp->string('city', 64); - })->query); + $mongo->command($this->schema->table($collection) + ->integer('id') + ->string('country', 32) + ->string('city', 64) + ->create()->query); $indexStatement = $this->schema->createIndex( $collection, @@ -106,10 +105,10 @@ public function testCreateIndexUnique(): void $mongo = $this->mongoClient; $this->assertNotNull($mongo); - $mongo->command($this->schema->create($collection, function (Table $bp) { - $bp->integer('id'); - $bp->string('email', 255); - })->query); + $mongo->command($this->schema->table($collection) + ->integer('id') + ->string('email', 255) + ->create()->query); $mongo->command($this->schema->createIndex($collection, 'idx_email_unique', ['email'], unique: true)->query); @@ -133,13 +132,13 @@ public function testDropCollection(): void $mongo = $this->mongoClient; $this->assertNotNull($mongo); - $mongo->command($this->schema->create($collection, function (Table $bp) { - $bp->integer('id'); - })->query); + $mongo->command($this->schema->table($collection) + ->integer('id') + ->create()->query); $this->assertContains($collection, $mongo->listCollectionNames()); - $mongo->command($this->schema->drop($collection)->query); + $mongo->command($this->schema->table($collection)->drop()->query); $this->assertNotContains($collection, $mongo->listCollectionNames()); } diff --git a/tests/Integration/Schema/MySQLIntegrationTest.php b/tests/Integration/Schema/MySQLIntegrationTest.php index b72c1c3..3b73703 100644 --- a/tests/Integration/Schema/MySQLIntegrationTest.php +++ b/tests/Integration/Schema/MySQLIntegrationTest.php @@ -6,7 +6,6 @@ use Utopia\Query\Schema\ColumnType; use Utopia\Query\Schema\ForeignKeyAction; use Utopia\Query\Schema\MySQL; -use Utopia\Query\Schema\Table; class MySQLIntegrationTest extends IntegrationTestCase { @@ -23,11 +22,11 @@ public function testCreateTableWithBasicColumns(): void $table = 'test_basic_' . uniqid(); $this->trackMysqlTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('age'); - $bp->string('name', 100); - $bp->boolean('active'); - }); + $result = $this->schema->table($table) + ->integer('age') + ->string('name', 100) + ->boolean('active') + ->create(); $this->mysqlStatement($result->query); @@ -48,10 +47,10 @@ public function testCreateTableWithPrimaryKeyAndUnique(): void $table = 'test_pk_uniq_' . uniqid(); $this->trackMysqlTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('email', 255)->unique(); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->string('email', 255)->unique() + ->create(); $this->mysqlStatement($result->query); @@ -77,10 +76,10 @@ public function testCreateTableWithAutoIncrement(): void $table = 'test_autoinc_' . uniqid(); $this->trackMysqlTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->id(); - $bp->string('label', 50); - }); + $result = $this->schema->table($table) + ->id() + ->string('label', 50) + ->create(); $this->mysqlStatement($result->query); @@ -102,14 +101,14 @@ public function testAlterTableAddColumn(): void $table = 'test_alter_add_' . uniqid(); $this->trackMysqlTable($table); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->create(); $this->mysqlStatement($create->query); - $alter = $this->schema->alter($table, function (Table $bp) { - $bp->addColumn('description', ColumnType::Text); - }); + $alter = $this->schema->table($table) + ->addColumn('description', ColumnType::Text) + ->alter(); $this->mysqlStatement($alter->query); $columns = $this->fetchMysqlColumns($table); @@ -123,15 +122,15 @@ public function testAlterTableDropColumn(): void $table = 'test_alter_drop_' . uniqid(); $this->trackMysqlTable($table); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('temp', 100); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->string('temp', 100) + ->create(); $this->mysqlStatement($create->query); - $alter = $this->schema->alter($table, function (Table $bp) { - $bp->dropColumn('temp'); - }); + $alter = $this->schema->table($table) + ->dropColumn('temp') + ->alter(); $this->mysqlStatement($alter->query); $columns = $this->fetchMysqlColumns($table); @@ -145,15 +144,15 @@ public function testAlterTableAddIndex(): void $table = 'test_alter_idx_' . uniqid(); $this->trackMysqlTable($table); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('email', 255); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->string('email', 255) + ->create(); $this->mysqlStatement($create->query); - $alter = $this->schema->alter($table, function (Table $bp) { - $bp->addIndex('idx_email', ['email']); - }); + $alter = $this->schema->table($table) + ->addIndex('idx_email', ['email']) + ->alter(); $this->mysqlStatement($alter->query); $pdo = $this->connectMysql(); @@ -174,12 +173,12 @@ public function testDropTable(): void { $table = 'test_drop_' . uniqid(); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->create(); $this->mysqlStatement($create->query); - $drop = $this->schema->drop($table); + $drop = $this->schema->table($table)->drop(); $this->mysqlStatement($drop->query); $pdo = $this->connectMysql(); @@ -202,19 +201,19 @@ public function testCreateTableWithForeignKey(): void $this->trackMysqlTable($childTable); $this->trackMysqlTable($parentTable); - $createParent = $this->schema->create($parentTable, function (Table $bp) { - $bp->id(); - }); + $createParent = $this->schema->table($parentTable) + ->id() + ->create(); $this->mysqlStatement($createParent->query); - $createChild = $this->schema->create($childTable, function (Table $bp) use ($parentTable) { - $bp->id(); - $bp->bigInteger('parent_id')->unsigned(); - $bp->foreignKey('parent_id') + $createChild = $this->schema->table($childTable) + ->id() + ->bigInteger('parent_id')->unsigned() + ->foreignKey('parent_id') ->references('id') ->on($parentTable) - ->onDelete(ForeignKeyAction::Cascade); - }); + ->onDelete(ForeignKeyAction::Cascade) + ->create(); $this->mysqlStatement($createChild->query); $pdo = $this->connectMysql(); @@ -236,11 +235,11 @@ public function testCreateTableWithNullableAndDefault(): void $table = 'test_null_def_' . uniqid(); $this->trackMysqlTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('nickname', 100)->nullable()->default('anonymous'); - $bp->integer('score')->default(0); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->string('nickname', 100)->nullable()->default('anonymous') + ->integer('score')->default(0) + ->create(); $this->mysqlStatement($result->query); @@ -259,11 +258,11 @@ public function testCreateTableWithCheckConstraint(): void $table = 'test_check_' . uniqid(); $this->trackMysqlTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->id(); - $bp->integer('age'); - $bp->check('age_range', '`age` >= 0 AND `age` < 150'); - }); + $result = $this->schema->table($table) + ->id() + ->integer('age') + ->check('age_range', '`age` >= 0 AND `age` < 150') + ->create(); $this->mysqlStatement($result->query); @@ -288,12 +287,12 @@ public function testCreateTableWithGeneratedColumn(): void $table = 'test_generated_' . uniqid(); $this->trackMysqlTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->id(); - $bp->integer('width'); - $bp->integer('height'); - $bp->integer('area')->generatedAs('`width` * `height`')->stored(); - }); + $result = $this->schema->table($table) + ->id() + ->integer('width') + ->integer('height') + ->integer('area')->generatedAs('`width` * `height`')->stored() + ->create(); $this->mysqlStatement($result->query); @@ -325,10 +324,10 @@ public function testCreateTableWithPartitioning(): void $table = 'test_partition_' . uniqid(); $this->trackMysqlTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->partitionByHash('`id`'); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->partitionByHash('`id`') + ->create(); $this->assertStringContainsString('PARTITION BY HASH(`id`)', $result->query); $this->mysqlStatement($result->query); @@ -351,12 +350,12 @@ public function testCreateTableWithCompositeIndex(): void $table = 'test_composite_idx_' . uniqid(); $this->trackMysqlTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->id(); - $bp->string('first_name', 100); - $bp->string('last_name', 100); - $bp->addIndex('idx_name', ['last_name', 'first_name']); - }); + $result = $this->schema->table($table) + ->id() + ->string('first_name', 100) + ->string('last_name', 100) + ->addIndex('idx_name', ['last_name', 'first_name']) + ->create(); $this->mysqlStatement($result->query); @@ -381,12 +380,12 @@ public function testCreateTableWithFullTextIndex(): void $table = 'test_fulltext_' . uniqid(); $this->trackMysqlTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->id(); - $bp->string('title', 200); - $bp->text('body'); - $bp->fulltextIndex(['title', 'body'], 'ft_title_body'); - }); + $result = $this->schema->table($table) + ->id() + ->string('title', 200) + ->text('body') + ->fulltextIndex(['title', 'body'], 'ft_title_body') + ->create(); $this->mysqlStatement($result->query); @@ -408,10 +407,10 @@ public function testTruncateTable(): void $table = 'test_truncate_' . uniqid(); $this->trackMysqlTable($table); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('name', 50); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->string('name', 50) + ->create(); $this->mysqlStatement($create->query); $pdo = $this->connectMysql(); @@ -419,7 +418,7 @@ public function testTruncateTable(): void \assert($insertStmt !== false); $insertStmt->execute(); - $truncate = $this->schema->truncate($table); + $truncate = $this->schema->table($table)->truncate(); $this->mysqlStatement($truncate->query); $stmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM `{$table}`"); diff --git a/tests/Integration/Schema/PostgreSQLIntegrationTest.php b/tests/Integration/Schema/PostgreSQLIntegrationTest.php index ca4dd3a..7b23898 100644 --- a/tests/Integration/Schema/PostgreSQLIntegrationTest.php +++ b/tests/Integration/Schema/PostgreSQLIntegrationTest.php @@ -5,7 +5,6 @@ use Tests\Integration\IntegrationTestCase; use Utopia\Query\Schema\ColumnType; use Utopia\Query\Schema\PostgreSQL; -use Utopia\Query\Schema\Table; class PostgreSQLIntegrationTest extends IntegrationTestCase { @@ -22,11 +21,11 @@ public function testCreateTableWithBasicColumns(): void $table = 'test_basic_' . uniqid(); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('age'); - $bp->string('name', 100); - $bp->float('score'); - }); + $result = $this->schema->table($table) + ->integer('age') + ->string('name', 100) + ->float('score') + ->create(); $this->postgresStatement($result->query); @@ -47,10 +46,10 @@ public function testCreateTableWithIdentityColumn(): void $table = 'test_identity_' . uniqid(); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->id(); - $bp->string('label', 50); - }); + $result = $this->schema->table($table) + ->id() + ->string('label', 50) + ->create(); $this->postgresStatement($result->query); @@ -72,10 +71,10 @@ public function testCreateTableWithJsonbColumn(): void $table = 'test_jsonb_' . uniqid(); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->json('metadata'); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->json('metadata') + ->create(); $this->postgresStatement($result->query); @@ -90,14 +89,14 @@ public function testAlterTableAddColumn(): void $table = 'test_alter_add_' . uniqid(); $this->trackPostgresTable($table); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->create(); $this->postgresStatement($create->query); - $alter = $this->schema->alter($table, function (Table $bp) { - $bp->addColumn('description', ColumnType::Text); - }); + $alter = $this->schema->table($table) + ->addColumn('description', ColumnType::Text) + ->alter(); $this->postgresStatement($alter->query); $columns = $this->fetchPostgresColumns($table); @@ -111,15 +110,15 @@ public function testAlterTableDropColumn(): void $table = 'test_alter_drop_' . uniqid(); $this->trackPostgresTable($table); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('temp', 100); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->string('temp', 100) + ->create(); $this->postgresStatement($create->query); - $alter = $this->schema->alter($table, function (Table $bp) { - $bp->dropColumn('temp'); - }); + $alter = $this->schema->table($table) + ->dropColumn('temp') + ->alter(); $this->postgresStatement($alter->query); $columns = $this->fetchPostgresColumns($table); @@ -132,12 +131,12 @@ public function testDropTable(): void { $table = 'test_drop_' . uniqid(); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->create(); $this->postgresStatement($create->query); - $drop = $this->schema->drop($table); + $drop = $this->schema->table($table)->drop(); $this->postgresStatement($drop->query); $pdo = $this->connectPostgres(); @@ -157,11 +156,11 @@ public function testCreateTableWithBooleanAndText(): void $table = 'test_bool_text_' . uniqid(); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->boolean('is_active'); - $bp->text('bio'); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->boolean('is_active') + ->text('bio') + ->create(); $this->postgresStatement($result->query); @@ -179,10 +178,10 @@ public function testCreateTableWithUniqueConstraint(): void $table = 'test_unique_' . uniqid(); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('email', 255)->unique(); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->string('email', 255)->unique() + ->create(); $this->postgresStatement($result->query); @@ -205,11 +204,11 @@ public function testCreateTableWithNullableAndDefault(): void $table = 'test_null_def_' . uniqid(); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('nickname', 100)->nullable()->default('anonymous'); - $bp->integer('score')->default(0); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->string('nickname', 100)->nullable()->default('anonymous') + ->integer('score')->default(0) + ->create(); $this->postgresStatement($result->query); @@ -228,10 +227,10 @@ public function testTruncateTable(): void $table = 'test_truncate_' . uniqid(); $this->trackPostgresTable($table); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('name', 50); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->string('name', 50) + ->create(); $this->postgresStatement($create->query); $pdo = $this->connectPostgres(); @@ -239,7 +238,7 @@ public function testTruncateTable(): void \assert($insertStmt !== false); $insertStmt->execute(); - $truncate = $this->schema->truncate($table); + $truncate = $this->schema->table($table)->truncate(); $this->postgresStatement($truncate->query); $stmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM \"{$table}\""); @@ -256,11 +255,11 @@ public function testCreateTableWithCheckConstraint(): void $table = 'test_check_' . uniqid(); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->integer('age'); - $bp->check('age_min', '"age" >= 18'); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->integer('age') + ->check('age_min', '"age" >= 18') + ->create(); $this->postgresStatement($result->query); @@ -280,12 +279,12 @@ public function testCreateTableWithGeneratedColumn(): void $table = 'test_generated_' . uniqid(); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->integer('price'); - $bp->integer('quantity'); - $bp->integer('total')->generatedAs('"price" * "quantity"')->stored(); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->integer('price') + ->integer('quantity') + ->integer('total')->generatedAs('"price" * "quantity"')->stored() + ->create(); $this->postgresStatement($result->query); @@ -312,10 +311,10 @@ public function testCreateTableWithSerial(): void $table = 'test_serial_' . uniqid(); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->bigSerial('id')->primary(); - $bp->string('label', 50); - }); + $result = $this->schema->table($table) + ->bigSerial('id')->primary() + ->string('label', 50) + ->create(); $this->assertStringContainsString('BIGSERIAL', $result->query); $this->postgresStatement($result->query); @@ -350,10 +349,10 @@ public function testCreateEnumType(): void $createType = $this->schema->createType($typeName, ['happy', 'sad', 'neutral']); $this->postgresStatement($createType->query); - $result = $this->schema->create($table, function (Table $bp) use ($typeName) { - $bp->integer('id')->primary(); - $bp->string('mood')->userType($typeName); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->string('mood')->userType($typeName) + ->create(); $this->assertStringContainsString('"mood" "' . $typeName . '"', $result->query); @@ -392,12 +391,12 @@ public function testCreatePartitionedTable(): void $this->trackPostgresTable($partition); $this->trackPostgresTable($table); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id'); - $bp->timestamp('created_at'); - $bp->primary(['id', 'created_at']); - $bp->partitionByRange('"created_at"'); - }); + $result = $this->schema->table($table) + ->integer('id') + ->timestamp('created_at') + ->primary(['id', 'created_at']) + ->partitionByRange('"created_at"') + ->create(); $this->postgresStatement($result->query); diff --git a/tests/Integration/Schema/SQLiteIntegrationTest.php b/tests/Integration/Schema/SQLiteIntegrationTest.php index 797157d..4dd285d 100644 --- a/tests/Integration/Schema/SQLiteIntegrationTest.php +++ b/tests/Integration/Schema/SQLiteIntegrationTest.php @@ -5,7 +5,6 @@ use Tests\Integration\IntegrationTestCase; use Utopia\Query\Schema\ColumnType; use Utopia\Query\Schema\SQLite; -use Utopia\Query\Schema\Table; class SQLiteIntegrationTest extends IntegrationTestCase { @@ -21,11 +20,11 @@ public function testCreateTableBasic(): void { $table = 'test_basic_' . uniqid(); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('age'); - $bp->string('name', 100); - $bp->float('score'); - }); + $result = $this->schema->table($table) + ->integer('age') + ->string('name', 100) + ->float('score') + ->create(); $this->sqliteStatement($result->query); @@ -44,10 +43,10 @@ public function testCreateTableWithPrimaryKeyAndUnique(): void { $table = 'test_pk_unique_' . uniqid(); - $result = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('email', 255)->unique(); - }); + $result = $this->schema->table($table) + ->integer('id')->primary() + ->string('email', 255)->unique() + ->create(); $this->sqliteStatement($result->query); @@ -83,14 +82,14 @@ public function testAlterTableAddColumn(): void { $table = 'test_alter_add_' . uniqid(); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->create(); $this->sqliteStatement($create->query); - $alter = $this->schema->alter($table, function (Table $bp) { - $bp->addColumn('description', ColumnType::Text); - }); + $alter = $this->schema->table($table) + ->addColumn('description', ColumnType::Text) + ->alter(); $this->sqliteStatement($alter->query); $columns = $this->fetchSqliteColumns($table); @@ -103,10 +102,10 @@ public function testCreateIndex(): void { $table = 'test_index_' . uniqid(); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - $bp->string('email', 255); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->string('email', 255) + ->create(); $this->sqliteStatement($create->query); $indexName = 'idx_' . $table . '_email'; @@ -129,12 +128,12 @@ public function testDropTable(): void { $table = 'test_drop_' . uniqid(); - $create = $this->schema->create($table, function (Table $bp) { - $bp->integer('id')->primary(); - }); + $create = $this->schema->table($table) + ->integer('id')->primary() + ->create(); $this->sqliteStatement($create->query); - $drop = $this->schema->drop($table); + $drop = $this->schema->table($table)->drop(); $this->sqliteStatement($drop->query); $pdo = $this->connectSqlite(); diff --git a/tests/Query/AggregationQueryTest.php b/tests/Query/API/AggregationTest.php similarity index 99% rename from tests/Query/AggregationQueryTest.php rename to tests/Query/API/AggregationTest.php index 9374036..804c4cd 100644 --- a/tests/Query/AggregationQueryTest.php +++ b/tests/Query/API/AggregationTest.php @@ -1,13 +1,13 @@ expectExceptionMessage('ALTER TABLE requires at least one alteration.'); $schema = new ClickHouseSchema(); - $schema->alter('events', function (Table $table): void { - // intentionally empty — triggers the guard - }); + $schema->table('events')->alter(); } public function testMySqlHashCommentReplacementEmitsValidDoubleDash(): void diff --git a/tests/Query/Regression/SecurityRegressionTest.php b/tests/Query/Regression/SecurityRegressionTest.php index 3e8261e..bf7dff9 100644 --- a/tests/Query/Regression/SecurityRegressionTest.php +++ b/tests/Query/Regression/SecurityRegressionTest.php @@ -122,9 +122,9 @@ public function testMongoBuilderRejectsEmptyFieldNameInSet(): void public function testMySqlCreateTableEnumEscapesTrailingBackslash(): void { $schema = new MySQLSchema(); - $plan = $schema->create('widgets', function (\Utopia\Query\Schema\Table $t): void { - $t->enum('grade', ['A', 'B', "bad\\"]); - }); + $plan = $schema->table('widgets') + ->enum('grade', ['A', 'B', "bad\\"]) + ->create(); // Pre-fix: the trailing backslash could escape the closing quote. After // the fix it must appear doubled inside the literal. @@ -155,9 +155,9 @@ public function testDdlStringLiteralEscapesBackslashes(): void // serialised with the backslash doubled so the closing quote cannot be // escaped by the payload under MySQL default SQL mode. $schema = new MySQLSchema(); - $plan = $schema->create('notes', function (\Utopia\Query\Schema\Table $t): void { - $t->string('body')->default("evil\\"); - }); + $plan = $schema->table('notes') + ->string('body')->default("evil\\") + ->create(); $this->assertSame("CREATE TABLE `notes` (`body` VARCHAR(255) NOT NULL DEFAULT 'evil\\\\')", $plan->query); } diff --git a/tests/Query/Schema/ClickHouseTest.php b/tests/Query/Schema/ClickHouseTest.php index 74c3d6c..013a5bf 100644 --- a/tests/Query/Schema/ClickHouseTest.php +++ b/tests/Query/Schema/ClickHouseTest.php @@ -10,27 +10,26 @@ use Utopia\Query\Query; use Utopia\Query\Schema\ClickHouse as Schema; use Utopia\Query\Schema\ClickHouse\Engine; +use Utopia\Query\Schema\ColumnType; use Utopia\Query\Schema\Feature\ColumnComments; use Utopia\Query\Schema\Feature\DropPartition; use Utopia\Query\Schema\Feature\ForeignKeys; use Utopia\Query\Schema\Feature\Procedures; use Utopia\Query\Schema\Feature\TableComments; use Utopia\Query\Schema\Feature\Triggers; -use Utopia\Query\Schema\Table; class ClickHouseTest extends TestCase { use AssertsBindingCount; - // CREATE TABLE public function testCreateTableBasic(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->string('name'); - $table->datetime('created_at', 3); - }); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->string('name') + ->datetime('created_at', 3) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` Int64, `name` String, `created_at` DateTime64(3)) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); @@ -39,17 +38,17 @@ public function testCreateTableBasic(): void public function testCreateTableColumnTypes(): void { $schema = new Schema(); - $result = $schema->create('test_types', function (Table $table) { - $table->integer('int_col'); - $table->integer('uint_col')->unsigned(); - $table->bigInteger('big_col'); - $table->bigInteger('ubig_col')->unsigned(); - $table->float('float_col'); - $table->boolean('bool_col'); - $table->text('text_col'); - $table->json('json_col'); - $table->binary('bin_col'); - }); + $result = $schema->table('test_types') + ->integer('int_col') + ->integer('uint_col')->unsigned() + ->bigInteger('big_col') + ->bigInteger('ubig_col')->unsigned() + ->float('float_col') + ->boolean('bool_col') + ->text('text_col') + ->json('json_col') + ->binary('bin_col') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `test_types` (`int_col` Int32, `uint_col` UInt32, `big_col` Int64, `ubig_col` UInt64, `float_col` Float64, `bool_col` UInt8, `text_col` String, `json_col` String, `bin_col` String) ENGINE = MergeTree() ORDER BY tuple()', $result->query); @@ -58,9 +57,9 @@ public function testCreateTableColumnTypes(): void public function testCreateTableNullableWrapping(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('name')->nullable(); - }); + $result = $schema->table('t') + ->string('name')->nullable() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`name` Nullable(String)) ENGINE = MergeTree() ORDER BY tuple()', $result->query); @@ -69,9 +68,9 @@ public function testCreateTableNullableWrapping(): void public function testCreateTableWithEnum(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->enum('status', ['active', 'inactive']); - }); + $result = $schema->table('t') + ->enum('status', ['active', 'inactive']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`status` Enum8(\'active\' = 1, \'inactive\' = 2)) ENGINE = MergeTree() ORDER BY tuple()', $result->query); @@ -80,9 +79,9 @@ public function testCreateTableWithEnum(): void public function testCreateTableWithVector(): void { $schema = new Schema(); - $result = $schema->create('embeddings', function (Table $table) { - $table->vector('embedding', 768); - }); + $result = $schema->table('embeddings') + ->vector('embedding', 768) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `embeddings` (`embedding` Array(Float64)) ENGINE = MergeTree() ORDER BY tuple()', $result->query); @@ -91,11 +90,11 @@ public function testCreateTableWithVector(): void public function testCreateTableWithSpatialTypes(): void { $schema = new Schema(); - $result = $schema->create('geo', function (Table $table) { - $table->point('coords'); - $table->linestring('path'); - $table->polygon('area'); - }); + $result = $schema->table('geo') + ->point('coords') + ->linestring('path') + ->polygon('area') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `geo` (`coords` Tuple(Float64, Float64), `path` Array(Tuple(Float64, Float64)), `area` Array(Array(Tuple(Float64, Float64)))) ENGINE = MergeTree() ORDER BY tuple()', $result->query); @@ -107,31 +106,30 @@ public function testCreateTableForeignKeyThrows(): void $this->expectExceptionMessage('Foreign keys are not supported in ClickHouse'); $schema = new Schema(); - $schema->create('t', function (Table $table) { - $table->foreignKey('user_id')->references('id')->on('users'); - }); + $schema->table('t') + ->foreignKey('user_id')->references('id')->on('users') + ->create(); } public function testCreateTableWithIndex(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->string('name'); - $table->index(['name']); - }); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->string('name') + ->index(['name']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` Int64, `name` String, INDEX `idx_name` `name` TYPE minmax GRANULARITY 3) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); } - // ALTER TABLE public function testAlterAddColumn(): void { $schema = new Schema(); - $result = $schema->alter('events', function (Table $table) { - $table->addColumn('score', 'float'); - }); + $result = $schema->table('events') + ->addColumn('score', ColumnType::Float) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `events` ADD COLUMN `score` Float64', $result->query); @@ -140,9 +138,9 @@ public function testAlterAddColumn(): void public function testAlterModifyColumn(): void { $schema = new Schema(); - $result = $schema->alter('events', function (Table $table) { - $table->modifyColumn('name', 'string'); - }); + $result = $schema->table('events') + ->modifyColumn('name', ColumnType::String) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `events` MODIFY COLUMN `name` String', $result->query); @@ -151,9 +149,9 @@ public function testAlterModifyColumn(): void public function testAlterRenameColumn(): void { $schema = new Schema(); - $result = $schema->alter('events', function (Table $table) { - $table->renameColumn('old', 'new'); - }); + $result = $schema->table('events') + ->renameColumn('old', 'new') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `events` RENAME COLUMN `old` TO `new`', $result->query); @@ -162,9 +160,9 @@ public function testAlterRenameColumn(): void public function testAlterDropColumn(): void { $schema = new Schema(); - $result = $schema->alter('events', function (Table $table) { - $table->dropColumn('old_col'); - }); + $result = $schema->table('events') + ->dropColumn('old_col') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `events` DROP COLUMN `old_col`', $result->query); @@ -176,16 +174,15 @@ public function testAlterForeignKeyThrows(): void $this->expectExceptionMessage('Foreign keys are not supported in ClickHouse'); $schema = new Schema(); - $schema->alter('events', function (Table $table) { - $table->addForeignKey('user_id')->references('id')->on('users'); - }); + $schema->table('events') + ->addForeignKey('user_id')->references('id')->on('users') + ->alter(); } - // DROP TABLE / TRUNCATE public function testDropTable(): void { $schema = new Schema(); - $result = $schema->drop('events'); + $result = $schema->table('events')->drop(); $this->assertBindingCount($result); $this->assertSame('DROP TABLE `events`', $result->query); @@ -194,12 +191,11 @@ public function testDropTable(): void public function testTruncateTable(): void { $schema = new Schema(); - $result = $schema->truncate('events'); + $result = $schema->table('events')->truncate(); $this->assertBindingCount($result); $this->assertSame('TRUNCATE TABLE `events`', $result->query); } - // VIEW public function testCreateView(): void { @@ -221,7 +217,6 @@ public function testDropView(): void $this->assertSame('DROP VIEW `active_events`', $result->query); } - // DROP INDEX (ClickHouse-specific) public function testDropIndex(): void { @@ -230,7 +225,6 @@ public function testDropIndex(): void $this->assertSame('ALTER TABLE `events` DROP INDEX `idx_name`', $result->query); } - // Feature interface checks — ClickHouse does NOT implement these public function testDoesNotImplementForeignKeys(): void { @@ -247,12 +241,10 @@ public function testDoesNotImplementTriggers(): void $this->assertNotInstanceOf(Triggers::class, new Schema()); // @phpstan-ignore method.alreadyNarrowedType } - // Edge cases - public function testDropIfExists(): void { $schema = new Schema(); - $result = $schema->dropIfExists('events'); + $result = $schema->table('events')->dropIfExists(); $this->assertSame('DROP TABLE IF EXISTS `events`', $result->query); } @@ -260,10 +252,10 @@ public function testDropIfExists(): void public function testCreateTableWithDefaultValue(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->integer('count')->default(0); - }); + $result = $schema->table('t') + ->bigInteger('id')->primary() + ->integer('count')->default(0) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`id` Int64, `count` Int32 DEFAULT 0) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); @@ -272,10 +264,10 @@ public function testCreateTableWithDefaultValue(): void public function testCreateTableWithComment(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->string('name')->comment('User name'); - }); + $result = $schema->table('t') + ->bigInteger('id')->primary() + ->string('name')->comment('User name') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`id` Int64, `name` String COMMENT \'User name\') ENGINE = MergeTree() ORDER BY (`id`)', $result->query); @@ -284,11 +276,11 @@ public function testCreateTableWithComment(): void public function testCreateTableMultiplePrimaryKeys(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->datetime('created_at', 3)->primary(); - $table->string('name'); - }); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->datetime('created_at', 3)->primary() + ->string('name') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` Int64, `created_at` DateTime64(3), `name` String) ENGINE = MergeTree() ORDER BY (`id`, `created_at`)', $result->query); @@ -297,12 +289,12 @@ public function testCreateTableMultiplePrimaryKeys(): void public function testCreateTableWithCompositePrimaryKey(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->bigInteger('id'); - $table->datetime('created_at', 3); - $table->string('name'); - $table->primary(['id', 'created_at']); - }); + $result = $schema->table('events') + ->bigInteger('id') + ->datetime('created_at', 3) + ->string('name') + ->primary(['id', 'created_at']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` Int64, `created_at` DateTime64(3), `name` String) ENGINE = MergeTree() ORDER BY (`id`, `created_at`)', $result->query); @@ -315,21 +307,21 @@ public function testCreateTableRejectsMixedColumnAndTablePrimary(): void $this->expectException(ValidationException::class); $this->expectExceptionMessage('Cannot combine column-level primary() with Table::primary() composite key.'); - $schema->create('events', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->datetime('created_at', 3); - $table->primary(['id', 'created_at']); - }); + $schema->table('events') + ->bigInteger('id')->primary() + ->datetime('created_at', 3) + ->primary(['id', 'created_at']) + ->create(); } public function testAlterMultipleOperations(): void { $schema = new Schema(); - $result = $schema->alter('events', function (Table $table) { - $table->addColumn('score', 'float'); - $table->dropColumn('old_col'); - $table->renameColumn('nm', 'name'); - }); + $result = $schema->table('events') + ->addColumn('score', ColumnType::Float) + ->dropColumn('old_col') + ->renameColumn('nm', 'name') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `events` ADD COLUMN `score` Float64, RENAME COLUMN `nm` TO `name`, DROP COLUMN `old_col`', $result->query); @@ -338,9 +330,9 @@ public function testAlterMultipleOperations(): void public function testAlterDropIndex(): void { $schema = new Schema(); - $result = $schema->alter('events', function (Table $table) { - $table->dropIndex('idx_name'); - }); + $result = $schema->table('events') + ->dropIndex('idx_name') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `events` DROP INDEX `idx_name`', $result->query); @@ -349,13 +341,13 @@ public function testAlterDropIndex(): void public function testCreateTableWithMultipleIndexes(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->string('name'); - $table->string('type'); - $table->index(['name']); - $table->index(['type']); - }); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->string('name') + ->string('type') + ->index(['name']) + ->index(['type']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` Int64, `name` String, `type` String, INDEX `idx_name` `name` TYPE minmax GRANULARITY 3, INDEX `idx_type` `type` TYPE minmax GRANULARITY 3) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); @@ -364,10 +356,10 @@ public function testCreateTableWithMultipleIndexes(): void public function testCreateTableTimestampWithoutPrecision(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->timestamp('ts_col'); - }); + $result = $schema->table('t') + ->bigInteger('id')->primary() + ->timestamp('ts_col') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`id` Int64, `ts_col` DateTime) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); @@ -377,10 +369,10 @@ public function testCreateTableTimestampWithoutPrecision(): void public function testCreateTableDatetimeWithoutPrecision(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->datetime('dt_col'); - }); + $result = $schema->table('t') + ->bigInteger('id')->primary() + ->datetime('dt_col') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`id` Int64, `dt_col` DateTime) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); @@ -390,15 +382,14 @@ public function testCreateTableDatetimeWithoutPrecision(): void public function testCreateTableWithCompositeIndex(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->string('name'); - $table->string('type'); - $table->index(['name', 'type']); - }); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->string('name') + ->string('type') + ->index(['name', 'type']) + ->create(); $this->assertBindingCount($result); - // Composite index wraps in parentheses $this->assertSame('CREATE TABLE `events` (`id` Int64, `name` String, `type` String, INDEX `idx_name_type` (`name`, `type`) TYPE minmax GRANULARITY 3) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); } @@ -407,20 +398,20 @@ public function testAlterForeignKeyStillThrows(): void $this->expectException(UnsupportedException::class); $schema = new Schema(); - $schema->alter('events', function (Table $table) { - $table->dropForeignKey('fk_old'); - }); + $schema->table('events') + ->dropForeignKey('fk_old') + ->alter(); } public function testExactCreateTableWithEngine(): void { $schema = new Schema(); - $result = $schema->create('metrics', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->string('name'); - $table->float('value'); - $table->datetime('recorded_at', 3); - }); + $result = $schema->table('metrics') + ->bigInteger('id')->primary() + ->string('name') + ->float('value') + ->datetime('recorded_at', 3) + ->create(); $this->assertSame( 'CREATE TABLE `metrics` (`id` Int64, `name` String, `value` Float64, `recorded_at` DateTime64(3)) ENGINE = MergeTree() ORDER BY (`id`)', @@ -433,9 +424,9 @@ public function testExactCreateTableWithEngine(): void public function testExactAlterTableAddColumn(): void { $schema = new Schema(); - $result = $schema->alter('metrics', function (Table $table) { - $table->addColumn('description', 'text')->nullable(); - }); + $result = $schema->table('metrics') + ->addColumn('description', ColumnType::Text)->nullable() + ->alter(); $this->assertSame( 'ALTER TABLE `metrics` ADD COLUMN `description` Nullable(String)', @@ -448,7 +439,7 @@ public function testExactAlterTableAddColumn(): void public function testExactDropTable(): void { $schema = new Schema(); - $result = $schema->drop('metrics'); + $result = $schema->table('metrics')->drop(); $this->assertSame('DROP TABLE `metrics`', $result->query); $this->assertSame([], $result->bindings); @@ -500,12 +491,12 @@ public function testDropPartition(): void public function testCreateTableWithPartition(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->string('name'); - $table->datetime('created_at', 3); - $table->partitionByRange('toYYYYMM(created_at)'); - }); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->string('name') + ->datetime('created_at', 3) + ->partitionByRange('toYYYYMM(created_at)') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` Int64, `name` String, `created_at` DateTime64(3)) ENGINE = MergeTree() PARTITION BY toYYYYMM(created_at) ORDER BY (`id`)', $result->query); @@ -514,10 +505,10 @@ public function testCreateTableWithPartition(): void public function testCreateTableIfNotExists(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->string('name'); - }, ifNotExists: true); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->string('name') + ->createIfNotExists(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE IF NOT EXISTS `events` (`id` Int64, `name` String) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); @@ -526,9 +517,9 @@ public function testCreateTableIfNotExists(): void public function testCompileAutoIncrementReturnsEmpty(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->bigInteger('id')->primary()->autoIncrement(); - }); + $result = $schema->table('t') + ->bigInteger('id')->primary()->autoIncrement() + ->create(); $this->assertBindingCount($result); $this->assertStringNotContainsString('AUTO_INCREMENT', $result->query); @@ -538,9 +529,9 @@ public function testCompileAutoIncrementReturnsEmpty(): void public function testCompileUnsignedReturnsEmpty(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->integer('val')->unsigned(); - }); + $result = $schema->table('t') + ->integer('val')->unsigned() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`val` UInt32) ENGINE = MergeTree() ORDER BY tuple()', $result->query); @@ -574,23 +565,20 @@ public function testDropPartitionEscapesSingleQuotes(): void public function testEnumEscapesBackslash(): void { $schema = new Schema(); - $result = $schema->create('items', function (Table $table) { - // Input: a\' ; backslash must be escaped BEFORE the quote - // so the quote-escape `\'` is not cancelled by a trailing `\`. - $table->enum('status', ["a\\'b"]); - }); + $result = $schema->table('items') + ->enum('status', ["a\\'b"]) + ->create(); - // Output literal: 'a\\\'b' (a, 2 backslashes, escaped quote, b) $this->assertSame("CREATE TABLE `items` (`status` Enum8('a\\\\\\'b' = 1)) ENGINE = MergeTree() ORDER BY tuple()", $result->query); } public function testCreateMergeTreeWithoutPrimaryKeysEmitsOrderByTuple(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->string('name'); - $table->integer('count'); - }); + $result = $schema->table('events') + ->string('name') + ->integer('count') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`name` String, `count` Int32) ENGINE = MergeTree() ORDER BY tuple()', $result->query); @@ -603,19 +591,17 @@ public function testAlterTableWithNoAlterationsThrows(): void $this->expectExceptionMessage('ALTER TABLE requires at least one alteration.'); $schema = new Schema(); - $schema->alter('events', function (Table $table) { - // no alterations - }); + $schema->table('events')->alter(); } public function testCreateReplacingMergeTreeEmitsEngineWithVersion(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->bigInteger('id')->primary(); - $table->integer('version'); - $table->engine(Engine::ReplacingMergeTree, 'version'); - }); + $result = $schema->table('events') + ->bigInteger('id')->primary() + ->integer('version') + ->engine(Engine::ReplacingMergeTree, 'version') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` Int64, `version` Int32) ENGINE = ReplacingMergeTree(`version`) ORDER BY (`id`)', $result->query); @@ -624,12 +610,12 @@ public function testCreateReplacingMergeTreeEmitsEngineWithVersion(): void public function testCreateSummingMergeTreeEmitsEngineWithColumns(): void { $schema = new Schema(); - $result = $schema->create('metrics', function (Table $table) { - $table->integer('key')->primary(); - $table->bigInteger('total')->unsigned(); - $table->bigInteger('count')->unsigned(); - $table->engine(Engine::SummingMergeTree, 'total', 'count'); - }); + $result = $schema->table('metrics') + ->integer('key')->primary() + ->bigInteger('total')->unsigned() + ->bigInteger('count')->unsigned() + ->engine(Engine::SummingMergeTree, 'total', 'count') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `metrics` (`key` Int32, `total` UInt64, `count` UInt64) ENGINE = SummingMergeTree(`total`, `count`) ORDER BY (`key`)', $result->query); @@ -641,20 +627,20 @@ public function testCreateCollapsingMergeTreeRejectsMissingSignColumn(): void $this->expectExceptionMessage('CollapsingMergeTree requires a sign column.'); $schema = new Schema(); - $schema->create('events', function (Table $table) { - $table->integer('id')->primary(); - $table->engine(Engine::CollapsingMergeTree); - }); + $schema->table('events') + ->integer('id')->primary() + ->engine(Engine::CollapsingMergeTree) + ->create(); } public function testCreateMemoryEngineSkipsOrderBy(): void { $schema = new Schema(); - $result = $schema->create('cache', function (Table $table) { - $table->integer('id')->primary(); - $table->string('value'); - $table->engine(Engine::Memory); - }); + $result = $schema->table('cache') + ->integer('id')->primary() + ->string('value') + ->engine(Engine::Memory) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `cache` (`id` Int32, `value` String) ENGINE = Memory', $result->query); @@ -664,10 +650,10 @@ public function testCreateMemoryEngineSkipsOrderBy(): void public function testCreateAggregatingMergeTreeEmitsEmptyArgs(): void { $schema = new Schema(); - $result = $schema->create('agg', function (Table $table) { - $table->integer('key')->primary(); - $table->engine(Engine::AggregatingMergeTree); - }); + $result = $schema->table('agg') + ->integer('key')->primary() + ->engine(Engine::AggregatingMergeTree) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `agg` (`key` Int32) ENGINE = AggregatingMergeTree() ORDER BY (`key`)', $result->query); @@ -678,20 +664,19 @@ public function testCreateReplicatedMergeTreeRejectsMissingArgs(): void $this->expectException(ValidationException::class); $schema = new Schema(); - $schema->create('events', function (Table $table) { - $table->integer('id')->primary(); - $table->engine(Engine::ReplicatedMergeTree, '/clickhouse/tables/events'); - }); + $schema->table('events') + ->integer('id')->primary() + ->engine(Engine::ReplicatedMergeTree, '/clickhouse/tables/events') + ->create(); } public function testTableLevelTTL(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->integer('id')->primary(); - $table->datetime('ts'); - $table->ttl('ts + INTERVAL 1 DAY'); - }); + $table = $schema->table('events'); + $table->integer('id')->primary(); + $table->datetime('ts'); + $result = $table->ttl('ts + INTERVAL 1 DAY')->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` Int32, `ts` DateTime) ENGINE = MergeTree() ORDER BY (`id`) TTL ts + INTERVAL 1 DAY', $result->query); @@ -702,20 +687,20 @@ public function testTableLevelTTLRejectsSemicolon(): void $this->expectException(ValidationException::class); $schema = new Schema(); - $schema->create('events', function (Table $table) { - $table->integer('id')->primary(); - $table->ttl('ts + INTERVAL 1 DAY;'); - }); + $schema->table('events') + ->integer('id')->primary() + ->ttl('ts + INTERVAL 1 DAY;') + ->create(); } public function testColumnLevelTTL(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->integer('id')->primary(); - $table->string('temporary')->ttl('ts + INTERVAL 1 DAY'); - $table->datetime('ts'); - }); + $result = $schema->table('events') + ->integer('id')->primary() + ->string('temporary')->ttl('ts + INTERVAL 1 DAY') + ->datetime('ts') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` Int32, `temporary` String TTL ts + INTERVAL 1 DAY, `ts` DateTime) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); diff --git a/tests/Query/Schema/FluentBuilderTest.php b/tests/Query/Schema/FluentBuilderTest.php new file mode 100644 index 0000000..fc05ecd --- /dev/null +++ b/tests/Query/Schema/FluentBuilderTest.php @@ -0,0 +1,1196 @@ +table('users'); + + $this->assertInstanceOf(Table::class, $table); + $this->assertSame('users', $table->name); + } + + public function testTableEntryPointReturnsFreshInstanceEachCall(): void + { + $schema = new MySQL(); + $a = $schema->table('a'); + $b = $schema->table('b'); + + $this->assertNotSame($a, $b); + $this->assertSame('a', $a->name); + $this->assertSame('b', $b->name); + } + + public function testColumnHoldsBackPointerToParentTable(): void + { + $schema = new MySQL(); + $bp = $schema->table('users'); + $col = $bp->string('name'); + + $this->assertSame($bp, $col->table); + } + + public function testForeignKeyHoldsBackPointerToParentTable(): void + { + $schema = new MySQL(); + $bp = $schema->table('orders'); + $fk = $bp->foreignKey('user_id'); + + $this->assertSame($bp, $fk->table); + } + + public function testColumnAddingMethodsAppendToTable(): void + { + $bp = new Table(); + $bp->string('a') + ->integer('b') + ->boolean('c') + ->datetime('d'); + + $this->assertCount(4, $bp->columns); + $this->assertSame(['a', 'b', 'c', 'd'], \array_column($bp->columns, 'name')); + } + + public function testColumnForwardsToEveryTableColumnFactory(): void + { + $bp = new Table(); + $bp->string('s') + ->text('t') + ->mediumText('mt') + ->longText('lt') + ->integer('i') + ->bigInteger('bi') + ->serial('sr') + ->bigSerial('bsr') + ->smallSerial('ssr') + ->float('f') + ->boolean('b') + ->datetime('dt') + ->timestamp('ts') + ->json('j') + ->binary('bin') + ->point('p') + ->linestring('ls') + ->polygon('pg') + ->vector('v', 8) + ->id('id_col'); + + $names = \array_column($bp->columns, 'name'); + $this->assertSame( + ['s', 't', 'mt', 'lt', 'i', 'bi', 'sr', 'bsr', 'ssr', 'f', 'b', 'dt', 'ts', 'j', 'bin', 'p', 'ls', 'pg', 'v', 'id_col'], + $names, + ); + + $types = \array_column($bp->columns, 'type'); + $this->assertSame([ + ColumnType::String, + ColumnType::Text, + ColumnType::MediumText, + ColumnType::LongText, + ColumnType::Integer, + ColumnType::BigInteger, + ColumnType::Serial, + ColumnType::BigSerial, + ColumnType::SmallSerial, + ColumnType::Float, + ColumnType::Boolean, + ColumnType::Datetime, + ColumnType::Timestamp, + ColumnType::Json, + ColumnType::Binary, + ColumnType::Point, + ColumnType::Linestring, + ColumnType::Polygon, + ColumnType::Vector, + ColumnType::BigInteger, + ], $types); + } + + public function testColumnForwardsTimestamps(): void + { + $bp = new Table(); + $bp->string('name')->timestamps(6); + + $this->assertCount(3, $bp->columns); + $this->assertSame('created_at', $bp->columns[1]->name); + $this->assertSame('updated_at', $bp->columns[2]->name); + $this->assertSame(6, $bp->columns[1]->precision); + } + + public function testColumnForwardsAddColumnAndModifyColumn(): void + { + $bp = new Table(); + $bp->string('name') + ->addColumn('phone', ColumnType::String, 30) + ->modifyColumn('email', ColumnType::String, 200); + + $this->assertCount(3, $bp->columns); + $this->assertSame('phone', $bp->columns[1]->name); + $this->assertFalse($bp->columns[1]->isModify); + $this->assertSame('email', $bp->columns[2]->name); + $this->assertTrue($bp->columns[2]->isModify); + } + + public function testColumnForwardsRenameColumnAndDropColumn(): void + { + $bp = new Table(); + $bp->string('name') + ->renameColumn('old', 'new') + ->dropColumn('legacy'); + + $this->assertSame([['from' => 'old', 'to' => 'new']], \array_map( + fn ($r) => ['from' => $r->from, 'to' => $r->to], + $bp->renameColumns, + )); + $this->assertSame(['legacy'], $bp->dropColumns); + } + + public function testColumnForwardsIndexFamily(): void + { + // Each call must hit a fresh Column: index methods return Table, so + // chaining a second method off a Column would route through Table. + $bp = new Table(); + $col = fn (): Column => $bp->string('name'); + + $col()->index(['name']); + $col()->uniqueIndex(['name'], 'uq_name'); + $col()->fulltextIndex(['name'], 'ft_name'); + $col()->spatialIndex(['name'], 'sp_name'); + $col()->addIndex('custom', ['name'], IndexType::Unique); + + $this->assertCount(5, $bp->indexes); + $this->assertSame('idx_name', $bp->indexes[0]->name); + $this->assertSame(IndexType::Index, $bp->indexes[0]->type); + $this->assertSame(IndexType::Unique, $bp->indexes[1]->type); + $this->assertSame(IndexType::Fulltext, $bp->indexes[2]->type); + $this->assertSame(IndexType::Spatial, $bp->indexes[3]->type); + $this->assertSame('custom', $bp->indexes[4]->name); + $this->assertSame(IndexType::Unique, $bp->indexes[4]->type); + } + + public function testColumnForwardsDropIndex(): void + { + $bp = new Table(); + $bp->string('name')->dropIndex('idx_old'); + + $this->assertSame(['idx_old'], $bp->dropIndexes); + } + + public function testColumnForwardsForeignKeyFactories(): void + { + $bp = new Table(); + $fk = $bp->string('name')->foreignKey('user_id'); + + $this->assertInstanceOf(ForeignKey::class, $fk); + $this->assertSame('user_id', $fk->column); + $this->assertCount(1, $bp->foreignKeys); + } + + public function testColumnForwardsAddAndDropForeignKey(): void + { + $bp = new Table(); + $fk = $bp->string('name')->addForeignKey('parent_id'); + $bp->string('other')->dropForeignKey('fk_old'); + + $this->assertSame('parent_id', $fk->column); + $this->assertCount(1, $bp->foreignKeys); + $this->assertSame(['fk_old'], $bp->dropForeignKeys); + } + + public function testColumnForwardsRawColumnAndRawIndex(): void + { + $bp = new Table(); + $bp->string('name') + ->rawColumn('`extra` JSON NOT NULL') + ->rawIndex('FULLTEXT INDEX `ft` (`name`)'); + + $this->assertSame(['`extra` JSON NOT NULL'], $bp->rawColumnDefs); + $this->assertSame(['FULLTEXT INDEX `ft` (`name`)'], $bp->rawIndexDefs); + } + + public function testColumnForwardsPartitionFamily(): void + { + $bp = new Table(); + $bp->integer('id')->partitionByRange('`id`'); + $this->assertNotNull($bp->partitionType); + + $bp2 = new Table(); + $bp2->integer('id')->partitionByList('`id`'); + $this->assertNotNull($bp2->partitionType); + + $bp3 = new Table(); + $bp3->integer('id')->partitionByHash('`id`', 4); + $this->assertSame(4, $bp3->partitionCount); + } + + public function testColumnForwardsEngineAndOrderByAndTtl(): void + { + $bp = new Table(); + $bp->integer('id') + ->engine(Engine::MergeTree) + ->orderBy(['id']) + ->ttl('id + INTERVAL 1 DAY'); + + $this->assertSame(Engine::MergeTree, $bp->engine); + $this->assertSame(['id'], $bp->orderBy); + $this->assertSame('id + INTERVAL 1 DAY', $bp->ttl); + } + + public function testColumnPrimaryNoArgsMarksColumn(): void + { + $bp = new Table(); + $col = $bp->integer('id')->primary(); + + $this->assertInstanceOf(Column::class, $col); + $this->assertTrue($col->isPrimary); + $this->assertSame([], $bp->compositePrimaryKey); + } + + public function testColumnPrimaryWithArrayDelegatesToTableCompositeKey(): void + { + $bp = new Table(); + $result = $bp->integer('a')->integer('b')->primary(['a', 'b']); + + $this->assertSame($bp, $result); + $this->assertSame(['a', 'b'], $bp->compositePrimaryKey); + $this->assertFalse($bp->columns[0]->isPrimary); + $this->assertFalse($bp->columns[1]->isPrimary); + } + + public function testColumnCheckOneArgIsColumnLevel(): void + { + $bp = new Table(); + $col = $bp->integer('age')->check('`age` >= 0'); + + $this->assertInstanceOf(Column::class, $col); + $this->assertSame('`age` >= 0', $col->checkExpression); + $this->assertCount(0, $bp->checks); + } + + public function testColumnCheckTwoArgsDelegatesToTableLevel(): void + { + $bp = new Table(); + $result = $bp->integer('age')->check('age_min', '`age` >= 18'); + + $this->assertSame($bp, $result); + $this->assertCount(1, $bp->checks); + $this->assertSame('age_min', $bp->checks[0]->name); + $this->assertSame('`age` >= 18', $bp->checks[0]->expression); + $this->assertNull($bp->columns[0]->checkExpression); + } + + public function testColumnEnumWithArraySetsValuesOnSelf(): void + { + $bp = new Table(); + $col = $bp->enum('status', ['draft']); + $col->enum(['draft', 'published', 'archived']); + + $this->assertSame(['draft', 'published', 'archived'], $col->enumValues); + $this->assertCount(1, $bp->columns); + } + + public function testColumnEnumWithNameAndValuesAddsNewColumn(): void + { + $bp = new Table(); + $a = $bp->string('first'); + $b = $a->enum('status', ['draft', 'live']); + + $this->assertCount(2, $bp->columns); + $this->assertSame('status', $b->name); + $this->assertSame(ColumnType::Enum, $b->type); + $this->assertSame(['draft', 'live'], $b->enumValues); + } + + public function testForeignKeyForwardsBackToTableForChaining(): void + { + $schema = new MySQL(); + $stmt = $schema->table('orders') + ->id() + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users')->onDelete(ForeignKeyAction::Cascade) + ->string('total') + ->create(); + + $this->assertBindingCount($stmt); + $this->assertStringContainsString('FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE', $stmt->query); + $this->assertStringContainsString('`total` VARCHAR(255)', $stmt->query); + } + + public function testForeignKeyForwardsTerminals(): void + { + $schema = new MySQL(); + $stmt = $schema->table('orders') + ->id() + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->create(); + + $this->assertBindingCount($stmt); + $this->assertStringContainsString('CREATE TABLE `orders`', $stmt->query); + } + + public function testForeignKeyExposesTableMethodsLikeIndex(): void + { + $bp = new Table(); + $bp->id() + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->index(['user_id']); + + $this->assertCount(1, $bp->foreignKeys); + $this->assertCount(1, $bp->indexes); + } + + public function testTableTerminalCreate(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users')->id()->create(); + + $this->assertBindingCount($stmt); + $this->assertStringContainsString('CREATE TABLE `users`', $stmt->query); + $this->assertStringNotContainsString('IF NOT EXISTS', $stmt->query); + } + + public function testTableTerminalCreateIfNotExists(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users')->id()->createIfNotExists(); + + $this->assertStringContainsString('CREATE TABLE IF NOT EXISTS `users`', $stmt->query); + } + + public function testTableTerminalAlter(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users') + ->addColumn('phone', ColumnType::String, 20)->nullable() + ->alter(); + + $this->assertSame('ALTER TABLE `users` ADD COLUMN `phone` VARCHAR(20) NULL', $stmt->query); + } + + public function testTableTerminalDropAndDropIfExists(): void + { + $schema = new MySQL(); + $this->assertSame('DROP TABLE `users`', $schema->table('users')->drop()->query); + $this->assertSame('DROP TABLE IF EXISTS `users`', $schema->table('users')->dropIfExists()->query); + } + + public function testTableTerminalRename(): void + { + $schema = new MySQL(); + $stmt = $schema->table('old')->rename('new'); + + $this->assertSame('RENAME TABLE `old` TO `new`', $stmt->query); + } + + public function testTableTerminalTruncate(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users')->truncate(); + + $this->assertSame('TRUNCATE TABLE `users`', $stmt->query); + } + + public function testDetachedTableThrowsOnCreate(): void + { + $bp = new Table(); + $bp->string('name'); + + $this->expectException(UnsupportedException::class); + $this->expectExceptionMessage('Cannot compile a Table without a Schema'); + $bp->create(); + } + + public function testDetachedTableThrowsOnAlter(): void + { + $bp = new Table(); + $bp->dropColumn('x'); + + $this->expectException(UnsupportedException::class); + $bp->alter(); + } + + public function testDetachedTableThrowsOnDrop(): void + { + $bp = new Table(); + + $this->expectException(UnsupportedException::class); + $bp->drop(); + } + + public function testDetachedTableThrowsOnTruncate(): void + { + $bp = new Table(); + + $this->expectException(UnsupportedException::class); + $bp->truncate(); + } + + public function testDetachedTableThrowsOnRename(): void + { + $bp = new Table(); + + $this->expectException(UnsupportedException::class); + $bp->rename('to'); + } + + public function testFluentChainEmitsCorrectMySqlCreate(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users') + ->id() + ->string('name', 100) + ->string('email', 255)->unique() + ->boolean('active')->default(true) + ->json('metadata')->nullable() + ->timestamps() + ->index(['name']) + ->create(); + + $this->assertBindingCount($stmt); + $this->assertSame( + 'CREATE TABLE `users` (' + . '`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, ' + . '`name` VARCHAR(100) NOT NULL, ' + . '`email` VARCHAR(255) NOT NULL, ' + . '`active` TINYINT(1) NOT NULL DEFAULT 1, ' + . '`metadata` JSON NULL, ' + . '`created_at` DATETIME(3) NOT NULL, ' + . '`updated_at` DATETIME(3) NOT NULL, ' + . 'PRIMARY KEY (`id`), ' + . 'UNIQUE (`email`), ' + . 'INDEX `idx_name` (`name`)' + . ')', + $stmt->query, + ); + } + + public function testFluentChainEmitsCorrectPostgreSqlCreate(): void + { + $schema = new PostgreSQL(); + $stmt = $schema->table('orders') + ->bigSerial('id')->primary() + ->integer('user_id') + ->json('metadata')->nullable() + ->foreignKey('user_id')->references('id')->on('users') + ->create(); + + $this->assertBindingCount($stmt); + $this->assertSame( + 'CREATE TABLE "orders" (' + . '"id" BIGSERIAL NOT NULL, ' + . '"user_id" INTEGER NOT NULL, ' + . '"metadata" JSONB NULL, ' + . 'PRIMARY KEY ("id"), ' + . 'FOREIGN KEY ("user_id") REFERENCES "users" ("id")' + . ')', + $stmt->query, + ); + } + + public function testFluentChainEmitsCorrectSqliteCreate(): void + { + $schema = new SQLite(); + $stmt = $schema->table('events') + ->integer('id')->primary() + ->string('payload') + ->datetime('ts') + ->index(['ts']) + ->create(); + + $this->assertBindingCount($stmt); + $this->assertStringContainsString('CREATE TABLE `events`', $stmt->query); + $this->assertStringContainsString('PRIMARY KEY (`id`)', $stmt->query); + $this->assertStringContainsString('INDEX `idx_ts` (`ts`)', $stmt->query); + } + + public function testFluentChainEmitsCorrectMongoDbCreate(): void + { + $schema = new MongoDB(); + $stmt = $schema->table('users') + ->string('name') + ->string('email')->nullable() + ->integer('age') + ->create(); + + $payload = \json_decode($stmt->query, true, 512, JSON_THROW_ON_ERROR); + \assert(\is_array($payload)); + $this->assertSame('createCollection', $payload['command']); + $this->assertSame('users', $payload['collection']); + } + + public function testClickHouseOrderByOverridesPrimaryKey(): void + { + $schema = new ClickHouse(); + $stmt = $schema->table('events') + ->integer('id')->primary() + ->datetime('ts') + ->orderBy(['ts', 'id']) + ->engine(Engine::MergeTree) + ->create(); + + $this->assertStringContainsString('ORDER BY (`ts`, `id`)', $stmt->query); + } + + public function testClickHouseOrderByFallsBackToPrimaryKey(): void + { + $schema = new ClickHouse(); + $stmt = $schema->table('events') + ->integer('id')->primary() + ->datetime('ts') + ->engine(Engine::MergeTree) + ->create(); + + $this->assertStringContainsString('ORDER BY (`id`)', $stmt->query); + } + + public function testClickHouseOrderByFallsBackToTupleWhenNoPrimaryKey(): void + { + $schema = new ClickHouse(); + $stmt = $schema->table('events') + ->integer('id') + ->engine(Engine::MergeTree) + ->create(); + + $this->assertStringContainsString('ORDER BY tuple()', $stmt->query); + } + + public function testClickHouseEndToEndExampleFromReadme(): void + { + $schema = new ClickHouse(); + $stmt = $schema->table('events') + ->string('tenantId') + ->string('eventId') + ->string('payload')->nullable() + ->datetime('createdAt') + ->primary(['tenantId', 'eventId']) + ->engine(Engine::MergeTree) + ->orderBy(['tenantId', 'createdAt']) + ->ttl('createdAt + INTERVAL 90 DAY') + ->create(); + + $this->assertSame( + 'CREATE TABLE `events` (' + . '`tenantId` String, ' + . '`eventId` String, ' + . '`payload` Nullable(String), ' + . '`createdAt` DateTime' + . ') ENGINE = MergeTree() ORDER BY (`tenantId`, `createdAt`) TTL createdAt + INTERVAL 90 DAY', + $stmt->query, + ); + } + + public function testTableOrderByRejectsInvalidIdentifier(): void + { + $bp = new Table(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Invalid column name in ORDER BY'); + $bp->orderBy(['bad name;']); + } + + public function testColumnPrimaryRejectsSingleColumnArrayViaTableGuard(): void + { + $bp = new Table(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('at least two columns'); + $bp->integer('id')->primary(['id']); + } + + public function testTablePrimaryArrayWithColumnLevelPrimaryThrowsOnCompile(): void + { + $schema = new MySQL(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Cannot combine column-level primary() with Table::primary()'); + + $schema->table('orders') + ->integer('a')->primary() + ->integer('b') + ->primary(['a', 'b']) + ->create(); + } + + public function testTableTtlOnNonClickHouseSchemaThrowsAtCompile(): void + { + $schema = new MySQL(); + + $this->expectException(UnsupportedException::class); + $this->expectExceptionMessage('TTL is only supported in ClickHouse'); + + $schema->table('events') + ->integer('id') + ->ttl('id + INTERVAL 1 DAY') + ->create(); + } + + public function testColumnTtlOnNonClickHouseSchemaThrowsAtCompile(): void + { + $schema = new MySQL(); + + $this->expectException(UnsupportedException::class); + $this->expectExceptionMessage('TTL is only supported in ClickHouse'); + + $schema->table('events') + ->datetime('ts')->ttl('ts + INTERVAL 1 DAY') + ->create(); + } + + public function testColumnAfterIsHonouredOnAlter(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users') + ->addColumn('phone', ColumnType::String, 20)->after('email') + ->alter(); + + $this->assertSame( + 'ALTER TABLE `users` ADD COLUMN `phone` VARCHAR(20) NOT NULL AFTER `email`', + $stmt->query, + ); + } + + public function testColumnGeneratedExpressionStored(): void + { + $schema = new MySQL(); + $stmt = $schema->table('boxes') + ->integer('width') + ->integer('height') + ->integer('area')->generatedAs('`width` * `height`')->stored() + ->create(); + + $this->assertStringContainsString( + "`area` INT GENERATED ALWAYS AS (`width` * `height`) STORED NOT NULL", + $stmt->query, + ); + } + + public function testColumnUnique(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users') + ->string('email')->unique() + ->create(); + + $this->assertStringContainsString('UNIQUE (`email`)', $stmt->query); + } + + public function testColumnNullableAndDefault(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users') + ->string('nickname')->nullable()->default('anon') + ->integer('score')->default(0) + ->boolean('active')->default(true) + ->create(); + + $this->assertStringContainsString("`nickname` VARCHAR(255) NULL DEFAULT 'anon'", $stmt->query); + $this->assertStringContainsString('`score` INT NOT NULL DEFAULT 0', $stmt->query); + $this->assertStringContainsString('`active` TINYINT(1) NOT NULL DEFAULT 1', $stmt->query); + } + + public function testTableCheckPropagatesThroughForeignKeyForwarder(): void + { + $bp = new Table(); + $bp->id() + ->integer('parent_id') + ->foreignKey('parent_id')->references('id')->on('parents') + ->check('positive_id', '`parent_id` > 0'); + + $this->assertCount(1, $bp->checks); + $this->assertSame('positive_id', $bp->checks[0]->name); + } + + public function testColumnEnumDispatchPreservesExistingValues(): void + { + $bp = new Table(); + $col = $bp->enum('status', ['a']); + $col->enum(['x', 'y']); + + $this->assertSame(['x', 'y'], $col->enumValues); + } + + public function testAddIndexAcceptsIndexTypeEnum(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users') + ->addIndex('ix_email', ['email'], IndexType::Unique) + ->alter(); + + $this->assertSame( + 'ALTER TABLE `users` ADD UNIQUE INDEX `ix_email` (`email`)', + $stmt->query, + ); + } + + public function testAddColumnAndModifyColumnAcceptColumnTypeEnum(): void + { + $schema = new MySQL(); + $stmt = $schema->table('users') + ->addColumn('age', ColumnType::Integer) + ->modifyColumn('name', ColumnType::String, 500) + ->alter(); + + $this->assertSame( + 'ALTER TABLE `users` ADD COLUMN `age` INT NOT NULL, ' + . 'MODIFY COLUMN `name` VARCHAR(500) NOT NULL', + $stmt->query, + ); + } + + public function testTableNameIsRetainedThroughCompile(): void + { + $schema = new MySQL(); + $stmt = $schema->table('with_underscores_123')->id()->create(); + + $this->assertStringContainsString('CREATE TABLE `with_underscores_123`', $stmt->query); + } + + public function testColumnReturnsItselfForChainingFluentMethods(): void + { + $bp = new Table(); + $col = $bp->string('name'); + + $this->assertSame($col, $col->nullable()); + $this->assertSame($col, $col->default('x')); + $this->assertSame($col, $col->unique()); + $this->assertSame($col, $col->primary()); + $this->assertSame($col, $col->autoIncrement()); + $this->assertSame($col, $col->comment('hi')); + $this->assertSame($col, $col->collation('utf8mb4_bin')); + $this->assertSame($col, $col->after('id')); + } + + public function testForeignKeyReturnsItselfForChainingFluentMethods(): void + { + $bp = new Table(); + $fk = $bp->foreignKey('user_id'); + + $this->assertSame($fk, $fk->references('id')); + $this->assertSame($fk, $fk->on('users')); + $this->assertSame($fk, $fk->onDelete(ForeignKeyAction::Cascade)); + $this->assertSame($fk, $fk->onUpdate(ForeignKeyAction::SetNull)); + } + + public function testMultipleTablesFromSameSchemaCompileIndependently(): void + { + $schema = new MySQL(); + $a = $schema->table('a')->id(); + $b = $schema->table('b')->id(); + + $this->assertNotSame($a->table, $b->table); + $this->assertSame('a', $a->table->name); + $this->assertSame('b', $b->table->name); + $this->assertStringContainsString('CREATE TABLE `a`', $a->create()->query); + $this->assertStringContainsString('CREATE TABLE `b`', $b->create()->query); + } + + public function testEngineWithRequiredArgsValidates(): void + { + $bp = new Table(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('CollapsingMergeTree requires a sign column'); + $bp->engine(Engine::CollapsingMergeTree); + } + + public function testReplicatedMergeTreeRequiresArgs(): void + { + $bp = new Table(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('ReplicatedMergeTree requires zookeeper_path and replica_name'); + $bp->engine(Engine::ReplicatedMergeTree, '/path'); + } + + public function testColumnForwardsTruncateAndRenameTerminals(): void + { + $schema = new MySQL(); + $truncate = $schema->table('users')->id()->truncate(); + $rename = $schema->table('old')->id()->rename('new'); + + $this->assertSame('TRUNCATE TABLE `users`', $truncate->query); + $this->assertSame('RENAME TABLE `old` TO `new`', $rename->query); + } + + public function testColumnForwardsDropAndDropIfExistsTerminals(): void + { + $schema = new MySQL(); + $drop = $schema->table('users')->string('name')->drop(); + $dropIfExists = $schema->table('users')->string('name')->dropIfExists(); + + $this->assertSame('DROP TABLE `users`', $drop->query); + $this->assertSame('DROP TABLE IF EXISTS `users`', $dropIfExists->query); + } + + public function testForeignKeyForwardsTruncateAndRenameTerminals(): void + { + $schema = new MySQL(); + $truncate = $schema->table('orders')->foreignKey('user_id')->references('id')->on('users')->truncate(); + $rename = $schema->table('orders')->foreignKey('user_id')->references('id')->on('users')->rename('orders_v2'); + + $this->assertSame('TRUNCATE TABLE `orders`', $truncate->query); + $this->assertSame('RENAME TABLE `orders` TO `orders_v2`', $rename->query); + } + + public function testAddForeignKeyIsAliasOfForeignKey(): void + { + $bp = new Table(); + $a = $bp->addForeignKey('user_id'); + $b = $bp->foreignKey('parent_id'); + + $this->assertCount(2, $bp->foreignKeys); + $this->assertSame($a, $bp->foreignKeys[0]); + $this->assertSame($b, $bp->foreignKeys[1]); + } + + public function testAddForeignKeySharesRegistryWithForeignKey(): void + { + // Calling both methods for the same column registers the FK twice; the + // alias does not deduplicate. Pinning this so callers don't accidentally + // double-register. + $bp = new Table(); + $bp->foreignKey('user_id'); + $bp->addForeignKey('user_id'); + + $this->assertCount(2, $bp->foreignKeys); + } + + public function testTableEnumRejectsEmptyValueList(): void + { + $bp = new Table(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('enum() requires at least one allowed value'); + $bp->enum('status', []); + } + + public function testColumnEnumArrayRejectsEmptyValueList(): void + { + $bp = new Table(); + $col = $bp->enum('status', ['draft']); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('enum() requires at least one allowed value'); + $col->enum([]); + } + + public function testColumnEnumStringDispatchRejectsMissingValueList(): void + { + $bp = new Table(); + $col = $bp->string('label'); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('enum() requires at least one allowed value'); + $col->enum('status'); + } + + public function testDeepChainOfHeterogeneousMethodsCompiles(): void + { + $schema = new MySQL(); + $stmt = $schema->table('mixed') + ->id() + ->string('a')->nullable() + ->integer('b')->default(7) + ->datetime('c', 6) + ->boolean('d') + ->json('e') + ->index(['a', 'b']) + ->uniqueIndex(['a']) + ->fulltextIndex(['e'], 'ft_e') + ->foreignKey('b')->references('id')->on('parents')->onDelete(ForeignKeyAction::Cascade) + ->check('b_positive', '`b` >= 0') + ->rawColumn('`raw_col` TEXT') + ->create(); + + $this->assertBindingCount($stmt); + $this->assertStringContainsString('PRIMARY KEY (`id`)', $stmt->query); + $this->assertStringContainsString('INDEX `idx_a_b` (`a`, `b`)', $stmt->query); + $this->assertStringContainsString('UNIQUE INDEX `uniq_a` (`a`)', $stmt->query); + $this->assertStringContainsString('FULLTEXT INDEX `ft_e` (`e`)', $stmt->query); + $this->assertStringContainsString('FOREIGN KEY (`b`) REFERENCES `parents` (`id`) ON DELETE CASCADE', $stmt->query); + $this->assertStringContainsString('CONSTRAINT `b_positive` CHECK (`b` >= 0)', $stmt->query); + $this->assertStringContainsString('`raw_col` TEXT', $stmt->query); + } + + public function testForeignKeyForwardsToEveryTableColumnFactory(): void + { + // Each call must hit a fresh ForeignKey: column factories return Column, + // so chaining a second factory off a FK would route through Column instead. + $bp = new Table(); + $bp->integer('user_id'); + $fk = fn (): ForeignKey => $bp->foreignKey('user_id'); + + $fk()->id('id_col'); + $fk()->string('s'); + $fk()->text('t'); + $fk()->mediumText('mt'); + $fk()->longText('lt'); + $fk()->integer('i'); + $fk()->bigInteger('bi'); + $fk()->serial('sr'); + $fk()->bigSerial('bsr'); + $fk()->smallSerial('ssr'); + $fk()->float('f'); + $fk()->boolean('b'); + $fk()->datetime('dt'); + $fk()->timestamp('ts'); + $fk()->json('j'); + $fk()->binary('bin'); + $enumCol = $fk()->enum('status', ['draft', 'live']); + $fk()->point('p'); + $fk()->linestring('ls'); + $fk()->polygon('pg'); + $fk()->vector('v', 8); + + $names = \array_column($bp->columns, 'name'); + $this->assertSame( + ['user_id', 'id_col', 's', 't', 'mt', 'lt', 'i', 'bi', 'sr', 'bsr', 'ssr', 'f', 'b', 'dt', 'ts', 'j', 'bin', 'status', 'p', 'ls', 'pg', 'v'], + $names, + ); + + $this->assertSame(ColumnType::Enum, $enumCol->type); + $this->assertSame(['draft', 'live'], $enumCol->enumValues); + } + + public function testForeignKeyForwardsTimestamps(): void + { + $bp = new Table(); + $bp->id() + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->timestamps(6); + + $this->assertCount(4, $bp->columns); + $this->assertSame('created_at', $bp->columns[2]->name); + $this->assertSame('updated_at', $bp->columns[3]->name); + $this->assertSame(6, $bp->columns[2]->precision); + } + + public function testForeignKeyForwardsAddColumnAndModifyColumn(): void + { + $bp = new Table(); + $bp->integer('user_id'); + $bp->foreignKey('user_id')->addColumn('phone', ColumnType::String, 30); + $bp->foreignKey('user_id')->modifyColumn('email', ColumnType::String, 200); + + $this->assertCount(3, $bp->columns); + $this->assertSame('phone', $bp->columns[1]->name); + $this->assertFalse($bp->columns[1]->isModify); + $this->assertSame('email', $bp->columns[2]->name); + $this->assertTrue($bp->columns[2]->isModify); + } + + public function testForeignKeyForwardsRenameColumnAndDropColumn(): void + { + $bp = new Table(); + $bp->integer('user_id'); + $bp->foreignKey('user_id')->renameColumn('old', 'new'); + $bp->foreignKey('user_id')->dropColumn('legacy'); + + $this->assertSame([['from' => 'old', 'to' => 'new']], \array_map( + fn ($r) => ['from' => $r->from, 'to' => $r->to], + $bp->renameColumns, + )); + $this->assertSame(['legacy'], $bp->dropColumns); + } + + public function testForeignKeyForwardsIndexFamily(): void + { + $bp = new Table(); + $bp->integer('user_id'); + $fk = fn (): ForeignKey => $bp->foreignKey('user_id'); + + $fk()->index(['user_id']); + $fk()->uniqueIndex(['user_id'], 'uq_user'); + $fk()->fulltextIndex(['user_id'], 'ft_user'); + $fk()->spatialIndex(['user_id'], 'sp_user'); + $fk()->addIndex('custom_user', ['user_id'], IndexType::Unique); + + $this->assertCount(5, $bp->indexes); + $this->assertSame(IndexType::Index, $bp->indexes[0]->type); + $this->assertSame(IndexType::Unique, $bp->indexes[1]->type); + $this->assertSame(IndexType::Fulltext, $bp->indexes[2]->type); + $this->assertSame(IndexType::Spatial, $bp->indexes[3]->type); + $this->assertSame('custom_user', $bp->indexes[4]->name); + } + + public function testForeignKeyForwardsDropIndex(): void + { + $bp = new Table(); + $bp->id() + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->dropIndex('idx_old'); + + $this->assertSame(['idx_old'], $bp->dropIndexes); + } + + public function testForeignKeyForwardsForeignKeyFactories(): void + { + $bp = new Table(); + $first = $bp->id() + ->integer('user_id') + ->integer('parent_id') + ->foreignKey('user_id')->references('id')->on('users'); + + $second = $first->foreignKey('parent_id'); + $third = $first->addForeignKey('user_id'); + + $this->assertInstanceOf(ForeignKey::class, $second); + $this->assertInstanceOf(ForeignKey::class, $third); + $this->assertCount(3, $bp->foreignKeys); + } + + public function testForeignKeyForwardsDropForeignKey(): void + { + $bp = new Table(); + $bp->id() + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->dropForeignKey('fk_old'); + + $this->assertSame(['fk_old'], $bp->dropForeignKeys); + } + + public function testForeignKeyForwardsPrimaryComposite(): void + { + $bp = new Table(); + $result = $bp->integer('a') + ->integer('b') + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->primary(['a', 'b']); + + $this->assertSame($bp, $result); + $this->assertSame(['a', 'b'], $bp->compositePrimaryKey); + } + + public function testForeignKeyForwardsCheck(): void + { + $bp = new Table(); + $result = $bp->integer('age') + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->check('age_min', '`age` >= 18'); + + $this->assertSame($bp, $result); + $this->assertCount(1, $bp->checks); + $this->assertSame('age_min', $bp->checks[0]->name); + $this->assertSame('`age` >= 18', $bp->checks[0]->expression); + } + + public function testForeignKeyForwardsRawColumnAndRawIndex(): void + { + $bp = new Table(); + $bp->integer('user_id'); + $bp->foreignKey('user_id')->rawColumn('`extra` JSON NOT NULL'); + $bp->foreignKey('user_id')->rawIndex('FULLTEXT INDEX `ft` (`extra`)'); + + $this->assertSame(['`extra` JSON NOT NULL'], $bp->rawColumnDefs); + $this->assertSame(['FULLTEXT INDEX `ft` (`extra`)'], $bp->rawIndexDefs); + } + + public function testForeignKeyForwardsPartitionFamily(): void + { + $a = new Table(); + $a->integer('id')->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->partitionByRange('`id`'); + $this->assertNotNull($a->partitionType); + + $b = new Table(); + $b->integer('id')->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->partitionByList('`id`'); + $this->assertNotNull($b->partitionType); + + $c = new Table(); + $c->integer('id')->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->partitionByHash('`id`', 4); + $this->assertSame(4, $c->partitionCount); + } + + public function testForeignKeyForwardsEngineAndOrderByAndTtl(): void + { + $bp = new Table(); + $bp->integer('id')->integer('user_id'); + $bp->foreignKey('user_id')->engine(Engine::MergeTree); + $bp->foreignKey('user_id')->orderBy(['id']); + $bp->foreignKey('user_id')->ttl('id + INTERVAL 1 DAY'); + + $this->assertSame(Engine::MergeTree, $bp->engine); + $this->assertSame(['id'], $bp->orderBy); + $this->assertSame('id + INTERVAL 1 DAY', $bp->ttl); + } + + public function testForeignKeyForwardsAllTerminals(): void + { + $schema = new MySQL(); + + $create = $schema->table('orders') + ->id() + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->create(); + $this->assertStringContainsString('CREATE TABLE `orders`', $create->query); + $this->assertStringNotContainsString('IF NOT EXISTS', $create->query); + + $createIfNotExists = $schema->table('orders') + ->id() + ->integer('user_id') + ->foreignKey('user_id')->references('id')->on('users') + ->createIfNotExists(); + $this->assertStringContainsString('CREATE TABLE IF NOT EXISTS `orders`', $createIfNotExists->query); + + $alter = $schema->table('orders') + ->integer('user_id') + ->addForeignKey('user_id')->references('id')->on('users') + ->alter(); + $this->assertStringContainsString('ALTER TABLE `orders`', $alter->query); + + $drop = $schema->table('orders') + ->foreignKey('user_id')->references('id')->on('users') + ->drop(); + $this->assertSame('DROP TABLE `orders`', $drop->query); + + $dropIfExists = $schema->table('orders') + ->foreignKey('user_id')->references('id')->on('users') + ->dropIfExists(); + $this->assertSame('DROP TABLE IF EXISTS `orders`', $dropIfExists->query); + } +} diff --git a/tests/Query/Schema/MongoDBTest.php b/tests/Query/Schema/MongoDBTest.php index 77adc31..25a760d 100644 --- a/tests/Query/Schema/MongoDBTest.php +++ b/tests/Query/Schema/MongoDBTest.php @@ -8,19 +8,18 @@ use Utopia\Query\Query; use Utopia\Query\Schema\ColumnType; use Utopia\Query\Schema\MongoDB as Schema; -use Utopia\Query\Schema\Table; class MongoDBTest extends TestCase { public function testCreateCollection(): void { $schema = new Schema(); - $result = $schema->create('users', function (Table $table) { - $table->id('id'); - $table->string('name'); - $table->string('email'); - $table->integer('age'); - }); + $result = $schema->table('users') + ->id('id') + ->string('name') + ->string('email') + ->integer('age') + ->create(); $op = $this->decode($result->query); $this->assertSame('createCollection', $op['command']); @@ -41,15 +40,15 @@ public function testCreateCollection(): void public function testCreateCollectionWithTypes(): void { $schema = new Schema(); - $result = $schema->create('posts', function (Table $table) { - $table->id('id'); - $table->string('title'); - $table->text('body'); - $table->integer('views'); - $table->float('rating'); - $table->boolean('published'); - $table->datetime('created_at'); - }); + $result = $schema->table('posts') + ->id('id') + ->string('title') + ->text('body') + ->integer('views') + ->float('rating') + ->boolean('published') + ->datetime('created_at') + ->create(); $op = $this->decode($result->query); /** @var array $validator */ @@ -70,10 +69,10 @@ public function testCreateCollectionWithTypes(): void public function testCreateCollectionWithEnumValidation(): void { $schema = new Schema(); - $result = $schema->create('tasks', function (Table $table) { - $table->id('id'); - $table->enum('status', ['pending', 'active', 'completed']); - }); + $result = $schema->table('tasks') + ->id('id') + ->enum('status', ['pending', 'active', 'completed']) + ->create(); $op = $this->decode($result->query); /** @var array $validator */ @@ -90,11 +89,11 @@ public function testCreateCollectionWithEnumValidation(): void public function testCreateCollectionWithRequired(): void { $schema = new Schema(); - $result = $schema->create('users', function (Table $table) { - $table->id('id'); - $table->string('name'); - $table->string('email')->nullable(); - }); + $result = $schema->table('users') + ->id('id') + ->string('name') + ->string('email')->nullable() + ->create(); $op = $this->decode($result->query); /** @var array $validator */ @@ -115,17 +114,17 @@ public function testCreateCollectionRejectsCompositePrimaryKey(): void $this->expectException(UnsupportedException::class); $this->expectExceptionMessage('Composite primary keys are not supported in MongoDB'); - $schema->create('order_items', function (Table $table) { - $table->integer('order_id'); - $table->integer('product_id'); - $table->primary(['order_id', 'product_id']); - }); + $schema->table('order_items') + ->integer('order_id') + ->integer('product_id') + ->primary(['order_id', 'product_id']) + ->create(); } public function testDrop(): void { $schema = new Schema(); - $result = $schema->drop('users'); + $result = $schema->table('users')->drop(); $op = $this->decode($result->query); $this->assertSame('drop', $op['command']); @@ -135,7 +134,7 @@ public function testDrop(): void public function testDropIfExists(): void { $schema = new Schema(); - $result = $schema->dropIfExists('users'); + $result = $schema->table('users')->dropIfExists(); $op = $this->decode($result->query); $this->assertSame('drop', $op['command']); @@ -145,7 +144,7 @@ public function testDropIfExists(): void public function testRename(): void { $schema = new Schema(); - $result = $schema->rename('old_users', 'new_users'); + $result = $schema->table('old_users')->rename('new_users'); $op = $this->decode($result->query); $this->assertSame('renameCollection', $op['command']); @@ -156,7 +155,7 @@ public function testRename(): void public function testTruncate(): void { $schema = new Schema(); - $result = $schema->truncate('users'); + $result = $schema->table('users')->truncate(); $op = $this->decode($result->query); $this->assertSame('deleteMany', $op['command']); @@ -238,10 +237,10 @@ public function testDropDatabase(): void public function testAlter(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->string('phone'); - $table->boolean('verified'); - }); + $result = $schema->table('users') + ->string('phone') + ->boolean('verified') + ->alter(); $op = $this->decode($result->query); $this->assertSame('collMod', $op['command']); @@ -260,9 +259,9 @@ public function testAlter(): void public function testColumnComment(): void { $schema = new Schema(); - $result = $schema->create('users', function (Table $table) { - $table->string('name')->comment('The display name'); - }); + $result = $schema->table('users') + ->string('name')->comment('The display name') + ->create(); $op = $this->decode($result->query); /** @var array $validator */ @@ -278,11 +277,11 @@ public function testColumnComment(): void public function testAlterWithMultipleColumns(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->string('phone'); - $table->integer('age'); - $table->boolean('verified'); - }); + $result = $schema->table('users') + ->string('phone') + ->integer('age') + ->boolean('verified') + ->alter(); $op = $this->decode($result->query); $this->assertSame('collMod', $op['command']); @@ -305,9 +304,9 @@ public function testAlterWithMultipleColumns(): void public function testAlterWithColumnComment(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->string('phone')->comment('User phone number'); - }); + $result = $schema->table('users') + ->string('phone')->comment('User phone number') + ->alter(); $op = $this->decode($result->query); /** @var array $validator */ @@ -325,9 +324,9 @@ public function testAlterDropColumnThrows(): void $this->expectExceptionMessage('MongoDB does not support dropping or renaming columns via schema'); $schema = new Schema(); - $schema->alter('users', function (Table $table) { - $table->dropColumn('old_field'); - }); + $schema->table('users') + ->dropColumn('old_field') + ->alter(); } public function testAlterRenameColumnThrows(): void @@ -336,9 +335,9 @@ public function testAlterRenameColumnThrows(): void $this->expectExceptionMessage('MongoDB does not support dropping or renaming columns via schema'); $schema = new Schema(); - $schema->alter('users', function (Table $table) { - $table->renameColumn('old_name', 'new_name'); - }); + $schema->table('users') + ->renameColumn('old_name', 'new_name') + ->alter(); } public function testCreateView(): void @@ -381,15 +380,15 @@ public function testCreateViewFromAggregation(): void public function testCreateCollectionWithAllBsonTypes(): void { $schema = new Schema(); - $result = $schema->create('all_types', function (Table $table) { - $table->json('meta'); - $table->binary('data'); - $table->point('location'); - $table->linestring('path'); - $table->polygon('area'); - $table->addColumn('uid', ColumnType::Uuid7); - $table->vector('embedding', 768); - }); + $result = $schema->table('all_types') + ->json('meta') + ->binary('data') + ->point('location') + ->linestring('path') + ->polygon('area') + ->addColumn('uid', ColumnType::Uuid7) + ->vector('embedding', 768) + ->create(); $op = $this->decode($result->query); /** @var array $validator */ diff --git a/tests/Query/Schema/MySQLTest.php b/tests/Query/Schema/MySQLTest.php index ea8376c..0db7e24 100644 --- a/tests/Query/Schema/MySQLTest.php +++ b/tests/Query/Schema/MySQLTest.php @@ -18,9 +18,9 @@ use Utopia\Query\Schema\Feature\Triggers; use Utopia\Query\Schema\ForeignKeyAction; use Utopia\Query\Schema\Index; +use Utopia\Query\Schema\IndexType; use Utopia\Query\Schema\MySQL as Schema; use Utopia\Query\Schema\ParameterDirection; -use Utopia\Query\Schema\Table; use Utopia\Query\Schema\TriggerEvent; use Utopia\Query\Schema\TriggerTiming; @@ -49,11 +49,11 @@ public function testImplementsTriggers(): void public function testCreateTableBasic(): void { $schema = new Schema(); - $result = $schema->create('users', function (Table $table) { - $table->id(); - $table->string('name', 255); - $table->string('email', 255)->unique(); - }); + $result = $schema->table('users') + ->id() + ->string('name', 255) + ->string('email', 255)->unique() + ->create(); $this->assertBindingCount($result); $this->assertSame( @@ -66,18 +66,18 @@ public function testCreateTableBasic(): void public function testCreateTableAllColumnTypes(): void { $schema = new Schema(); - $result = $schema->create('test_types', function (Table $table) { - $table->integer('int_col'); - $table->bigInteger('big_col'); - $table->float('float_col'); - $table->boolean('bool_col'); - $table->text('text_col'); - $table->datetime('dt_col', 3); - $table->timestamp('ts_col', 6); - $table->json('json_col'); - $table->binary('bin_col'); - $table->enum('status', ['active', 'inactive']); - }); + $result = $schema->table('test_types') + ->integer('int_col') + ->bigInteger('big_col') + ->float('float_col') + ->boolean('bool_col') + ->text('text_col') + ->datetime('dt_col', 3) + ->timestamp('ts_col', 6) + ->json('json_col') + ->binary('bin_col') + ->enum('status', ['active', 'inactive']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `test_types` (`int_col` INT NOT NULL, `big_col` BIGINT NOT NULL, `float_col` DOUBLE NOT NULL, `bool_col` TINYINT(1) NOT NULL, `text_col` TEXT NOT NULL, `dt_col` DATETIME(3) NOT NULL, `ts_col` TIMESTAMP(6) NOT NULL, `json_col` JSON NOT NULL, `bin_col` BLOB NOT NULL, `status` ENUM(\'active\',\'inactive\') NOT NULL)', $result->query); @@ -86,13 +86,13 @@ public function testCreateTableAllColumnTypes(): void public function testCreateTableWithNullableAndDefault(): void { $schema = new Schema(); - $result = $schema->create('posts', function (Table $table) { - $table->id(); - $table->text('bio')->nullable(); - $table->boolean('active')->default(true); - $table->integer('score')->default(0); - $table->string('status')->default('draft'); - }); + $result = $schema->table('posts') + ->id() + ->text('bio')->nullable() + ->boolean('active')->default(true) + ->integer('score')->default(0) + ->string('status')->default('draft') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `posts` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `bio` TEXT NULL, `active` TINYINT(1) NOT NULL DEFAULT 1, `score` INT NOT NULL DEFAULT 0, `status` VARCHAR(255) NOT NULL DEFAULT \'draft\', PRIMARY KEY (`id`))', $result->query); @@ -101,9 +101,9 @@ public function testCreateTableWithNullableAndDefault(): void public function testCreateTableWithUnsigned(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->integer('age')->unsigned(); - }); + $result = $schema->table('t') + ->integer('age')->unsigned() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`age` INT UNSIGNED NOT NULL)', $result->query); @@ -112,10 +112,10 @@ public function testCreateTableWithUnsigned(): void public function testCreateTableWithTimestamps(): void { $schema = new Schema(); - $result = $schema->create('posts', function (Table $table) { - $table->id(); - $table->timestamps(); - }); + $result = $schema->table('posts') + ->id() + ->timestamps() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `posts` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `created_at` DATETIME(3) NOT NULL, `updated_at` DATETIME(3) NOT NULL, PRIMARY KEY (`id`))', $result->query); @@ -124,12 +124,12 @@ public function testCreateTableWithTimestamps(): void public function testCreateTableWithForeignKey(): void { $schema = new Schema(); - $result = $schema->create('posts', function (Table $table) { - $table->id(); - $table->foreignKey('user_id') + $result = $schema->table('posts') + ->id() + ->foreignKey('user_id') ->references('id')->on('users') - ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::SetNull); - }); + ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::SetNull) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `posts` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE SET NULL)', $result->query); @@ -138,13 +138,13 @@ public function testCreateTableWithForeignKey(): void public function testCreateTableWithIndexes(): void { $schema = new Schema(); - $result = $schema->create('users', function (Table $table) { - $table->id(); - $table->string('name'); - $table->string('email'); - $table->index(['name', 'email']); - $table->uniqueIndex(['email']); - }); + $result = $schema->table('users') + ->id() + ->string('name') + ->string('email') + ->index(['name', 'email']) + ->uniqueIndex(['email']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `users` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `name` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`), INDEX `idx_name_email` (`name`, `email`), UNIQUE INDEX `uniq_email` (`email`))', $result->query); @@ -153,12 +153,12 @@ public function testCreateTableWithIndexes(): void public function testCreateTableWithSpatialTypes(): void { $schema = new Schema(); - $result = $schema->create('locations', function (Table $table) { - $table->id(); - $table->point('coords', 4326); - $table->linestring('path'); - $table->polygon('area'); - }); + $result = $schema->table('locations') + ->id() + ->point('coords', 4326) + ->linestring('path') + ->polygon('area') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `locations` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `coords` POINT SRID 4326 NOT NULL, `path` LINESTRING SRID 4326 NOT NULL, `area` POLYGON SRID 4326 NOT NULL, PRIMARY KEY (`id`))', $result->query); @@ -170,17 +170,17 @@ public function testCreateTableVectorThrows(): void $this->expectExceptionMessage('Vector type is not supported in MySQL.'); $schema = new Schema(); - $schema->create('embeddings', function (Table $table) { - $table->vector('embedding', 768); - }); + $schema->table('embeddings') + ->vector('embedding', 768) + ->create(); } public function testCreateTableWithComment(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('name')->comment('User display name'); - }); + $result = $schema->table('t') + ->string('name')->comment('User display name') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`name` VARCHAR(255) NOT NULL COMMENT \'User display name\')', $result->query); @@ -190,9 +190,9 @@ public function testCreateTableWithComment(): void public function testAlterAddColumn(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addColumn('avatar_url', 'string', 255)->nullable()->after('email'); - }); + $result = $schema->table('users') + ->addColumn('avatar_url', ColumnType::String, 255)->nullable()->after('email') + ->alter(); $this->assertBindingCount($result); $this->assertSame( @@ -204,9 +204,9 @@ public function testAlterAddColumn(): void public function testAlterModifyColumn(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->modifyColumn('name', 'string', 500); - }); + $result = $schema->table('users') + ->modifyColumn('name', ColumnType::String, 500) + ->alter(); $this->assertBindingCount($result); $this->assertSame( @@ -218,9 +218,9 @@ public function testAlterModifyColumn(): void public function testAlterRenameColumn(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->renameColumn('bio', 'biography'); - }); + $result = $schema->table('users') + ->renameColumn('bio', 'biography') + ->alter(); $this->assertBindingCount($result); $this->assertSame( @@ -232,9 +232,9 @@ public function testAlterRenameColumn(): void public function testAlterDropColumn(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->dropColumn('age'); - }); + $result = $schema->table('users') + ->dropColumn('age') + ->alter(); $this->assertBindingCount($result); $this->assertSame( @@ -246,9 +246,9 @@ public function testAlterDropColumn(): void public function testAlterAddIndex(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addIndex('idx_name', ['name']); - }); + $result = $schema->table('users') + ->addIndex('idx_name', ['name']) + ->alter(); $this->assertBindingCount($result); $this->assertSame( @@ -260,9 +260,9 @@ public function testAlterAddIndex(): void public function testAlterDropIndex(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->dropIndex('idx_old'); - }); + $result = $schema->table('users') + ->dropIndex('idx_old') + ->alter(); $this->assertBindingCount($result); $this->assertSame( @@ -274,10 +274,10 @@ public function testAlterDropIndex(): void public function testAlterAddForeignKey(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addForeignKey('dept_id') - ->references('id')->on('departments'); - }); + $result = $schema->table('users') + ->addForeignKey('dept_id') + ->references('id')->on('departments') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `users` ADD FOREIGN KEY (`dept_id`) REFERENCES `departments` (`id`)', $result->query); @@ -286,9 +286,9 @@ public function testAlterAddForeignKey(): void public function testAlterDropForeignKey(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->dropForeignKey('fk_old'); - }); + $result = $schema->table('users') + ->dropForeignKey('fk_old') + ->alter(); $this->assertBindingCount($result); $this->assertSame( @@ -300,11 +300,11 @@ public function testAlterDropForeignKey(): void public function testAlterMultipleOperations(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addColumn('avatar', 'string', 255)->nullable(); - $table->dropColumn('age'); - $table->renameColumn('bio', 'biography'); - }); + $result = $schema->table('users') + ->addColumn('avatar', ColumnType::String, 255)->nullable() + ->dropColumn('age') + ->renameColumn('bio', 'biography') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `users` ADD COLUMN `avatar` VARCHAR(255) NULL, RENAME COLUMN `bio` TO `biography`, DROP COLUMN `age`', $result->query); @@ -314,7 +314,7 @@ public function testAlterMultipleOperations(): void public function testDropTable(): void { $schema = new Schema(); - $result = $schema->drop('users'); + $result = $schema->table('users')->drop(); $this->assertBindingCount($result); $this->assertSame('DROP TABLE `users`', $result->query); @@ -324,7 +324,7 @@ public function testDropTable(): void public function testDropTableIfExists(): void { $schema = new Schema(); - $result = $schema->dropIfExists('users'); + $result = $schema->table('users')->dropIfExists(); $this->assertSame('DROP TABLE IF EXISTS `users`', $result->query); } @@ -333,7 +333,7 @@ public function testDropTableIfExists(): void public function testRenameTable(): void { $schema = new Schema(); - $result = $schema->rename('users', 'members'); + $result = $schema->table('users')->rename('members'); $this->assertBindingCount($result); $this->assertSame('RENAME TABLE `users` TO `members`', $result->query); @@ -343,7 +343,7 @@ public function testRenameTable(): void public function testTruncateTable(): void { $schema = new Schema(); - $result = $schema->truncate('users'); + $result = $schema->table('users')->truncate(); $this->assertBindingCount($result); $this->assertSame('TRUNCATE TABLE `users`', $result->query); @@ -522,11 +522,11 @@ public function testDropTrigger(): void public function testCreateTableWithMultiplePrimaryKeys(): void { $schema = new Schema(); - $result = $schema->create('order_items', function (Table $table) { - $table->integer('order_id')->primary(); - $table->integer('product_id')->primary(); - $table->integer('quantity'); - }); + $result = $schema->table('order_items') + ->integer('order_id')->primary() + ->integer('product_id')->primary() + ->integer('quantity') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `order_items` (`order_id` INT NOT NULL, `product_id` INT NOT NULL, `quantity` INT NOT NULL, PRIMARY KEY (`order_id`, `product_id`))', $result->query); @@ -535,12 +535,12 @@ public function testCreateTableWithMultiplePrimaryKeys(): void public function testCreateTableWithCompositePrimaryKey(): void { $schema = new Schema(); - $result = $schema->create('order_items', function (Table $table) { - $table->integer('order_id'); - $table->integer('product_id'); - $table->integer('quantity'); - $table->primary(['order_id', 'product_id']); - }); + $result = $schema->table('order_items') + ->integer('order_id') + ->integer('product_id') + ->integer('quantity') + ->primary(['order_id', 'product_id']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `order_items` (`order_id` INT NOT NULL, `product_id` INT NOT NULL, `quantity` INT NOT NULL, PRIMARY KEY (`order_id`, `product_id`))', $result->query); @@ -553,19 +553,19 @@ public function testCreateTableRejectsMixedColumnAndTablePrimary(): void $this->expectException(ValidationException::class); $this->expectExceptionMessage('Cannot combine column-level primary() with Table::primary() composite key.'); - $schema->create('order_items', function (Table $table) { - $table->integer('order_id')->primary(); - $table->integer('product_id'); - $table->primary(['order_id', 'product_id']); - }); + $schema->table('order_items') + ->integer('order_id')->primary() + ->integer('product_id') + ->primary(['order_id', 'product_id']) + ->create(); } public function testCreateTableWithDefaultNull(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('name')->nullable()->default(null); - }); + $result = $schema->table('t') + ->string('name')->nullable()->default(null) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`name` VARCHAR(255) NULL DEFAULT NULL)', $result->query); @@ -574,9 +574,9 @@ public function testCreateTableWithDefaultNull(): void public function testCreateTableWithNumericDefault(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->float('score')->default(0.5); - }); + $result = $schema->table('t') + ->float('score')->default(0.5) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`score` DOUBLE NOT NULL DEFAULT 0.5)', $result->query); @@ -585,7 +585,7 @@ public function testCreateTableWithNumericDefault(): void public function testDropIfExists(): void { $schema = new Schema(); - $result = $schema->dropIfExists('users'); + $result = $schema->table('users')->dropIfExists(); $this->assertSame('DROP TABLE IF EXISTS `users`', $result->query); } @@ -602,12 +602,12 @@ public function testCreateOrReplaceViewFromBuilder(): void public function testAlterMultipleColumnsAndIndexes(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addColumn('first_name', 'string', 100); - $table->addColumn('last_name', 'string', 100); - $table->dropColumn('name'); - $table->addIndex('idx_names', ['first_name', 'last_name']); - }); + $result = $schema->table('users') + ->addColumn('first_name', ColumnType::String, 100) + ->addColumn('last_name', ColumnType::String, 100) + ->dropColumn('name') + ->addIndex('idx_names', ['first_name', 'last_name']) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `users` ADD COLUMN `first_name` VARCHAR(100) NOT NULL, ADD COLUMN `last_name` VARCHAR(100) NOT NULL, DROP COLUMN `name`, ADD INDEX `idx_names` (`first_name`, `last_name`)', $result->query); @@ -616,12 +616,12 @@ public function testAlterMultipleColumnsAndIndexes(): void public function testCreateTableForeignKeyWithAllActions(): void { $schema = new Schema(); - $result = $schema->create('comments', function (Table $table) { - $table->id(); - $table->foreignKey('post_id') + $result = $schema->table('comments') + ->id() + ->foreignKey('post_id') ->references('id')->on('posts') - ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::Restrict); - }); + ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::Restrict) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `comments` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, PRIMARY KEY (`id`), FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT)', $result->query); @@ -647,9 +647,9 @@ public function testDropTriggerByName(): void public function testCreateTableTimestampWithoutPrecision(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->timestamp('ts_col'); - }); + $result = $schema->table('t') + ->timestamp('ts_col') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`ts_col` TIMESTAMP NOT NULL)', $result->query); @@ -659,9 +659,9 @@ public function testCreateTableTimestampWithoutPrecision(): void public function testCreateTableDatetimeWithoutPrecision(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->datetime('dt_col'); - }); + $result = $schema->table('t') + ->datetime('dt_col') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`dt_col` DATETIME NOT NULL)', $result->query); @@ -679,10 +679,10 @@ public function testCreateCompositeIndex(): void public function testAlterAddAndDropForeignKey(): void { $schema = new Schema(); - $result = $schema->alter('orders', function (Table $table) { - $table->addForeignKey('user_id')->references('id')->on('users'); - $table->dropForeignKey('fk_old_user'); - }); + $result = $schema->table('orders') + ->addForeignKey('user_id')->references('id')->on('users') + ->dropForeignKey('fk_old_user') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `orders` ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), DROP FOREIGN KEY `fk_old_user`', $result->query); @@ -691,11 +691,11 @@ public function testAlterAddAndDropForeignKey(): void public function testTableAutoGeneratedIndexName(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('first'); - $table->string('last'); - $table->index(['first', 'last']); - }); + $result = $schema->table('t') + ->string('first') + ->string('last') + ->index(['first', 'last']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`first` VARCHAR(255) NOT NULL, `last` VARCHAR(255) NOT NULL, INDEX `idx_first_last` (`first`, `last`))', $result->query); @@ -704,10 +704,10 @@ public function testTableAutoGeneratedIndexName(): void public function testTableAutoGeneratedUniqueIndexName(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('email'); - $table->uniqueIndex(['email']); - }); + $result = $schema->table('t') + ->string('email') + ->uniqueIndex(['email']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`email` VARCHAR(255) NOT NULL, UNIQUE INDEX `uniq_email` (`email`))', $result->query); @@ -716,14 +716,14 @@ public function testTableAutoGeneratedUniqueIndexName(): void public function testExactCreateTableWithColumnsAndIndexes(): void { $schema = new Schema(); - $result = $schema->create('products', function (Table $table) { - $table->id(); - $table->string('name', 100); - $table->integer('price'); - $table->boolean('active')->default(true); - $table->index(['name']); - $table->uniqueIndex(['price']); - }); + $result = $schema->table('products') + ->id() + ->string('name', 100) + ->integer('price') + ->boolean('active')->default(true) + ->index(['name']) + ->uniqueIndex(['price']) + ->create(); $this->assertSame( 'CREATE TABLE `products` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `name` VARCHAR(100) NOT NULL, `price` INT NOT NULL, `active` TINYINT(1) NOT NULL DEFAULT 1, PRIMARY KEY (`id`), INDEX `idx_name` (`name`), UNIQUE INDEX `uniq_price` (`price`))', @@ -736,10 +736,10 @@ public function testExactCreateTableWithColumnsAndIndexes(): void public function testExactAlterTableAddAndDropColumns(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addColumn('phone', 'string', 20)->nullable(); - $table->dropColumn('legacy_field'); - }); + $result = $schema->table('users') + ->addColumn('phone', ColumnType::String, 20)->nullable() + ->dropColumn('legacy_field') + ->alter(); $this->assertSame( 'ALTER TABLE `users` ADD COLUMN `phone` VARCHAR(20) NULL, DROP COLUMN `legacy_field`', @@ -752,13 +752,13 @@ public function testExactAlterTableAddAndDropColumns(): void public function testExactCreateTableWithForeignKey(): void { $schema = new Schema(); - $result = $schema->create('orders', function (Table $table) { - $table->id(); - $table->integer('customer_id'); - $table->foreignKey('customer_id') + $result = $schema->table('orders') + ->id() + ->integer('customer_id') + ->foreignKey('customer_id') ->references('id')->on('customers') - ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::Cascade); - }); + ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::Cascade) + ->create(); $this->assertSame( 'CREATE TABLE `orders` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `customer_id` INT NOT NULL, PRIMARY KEY (`id`), FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE)', @@ -771,7 +771,7 @@ public function testExactCreateTableWithForeignKey(): void public function testExactDropTable(): void { $schema = new Schema(); - $result = $schema->drop('sessions'); + $result = $schema->table('sessions')->drop(); $this->assertSame('DROP TABLE `sessions`', $result->query); $this->assertSame([], $result->bindings); @@ -886,10 +886,10 @@ public function testDropPartition(): void public function testCreateIfNotExists(): void { $schema = new Schema(); - $result = $schema->createIfNotExists('users', function (Table $table) { - $table->id(); - $table->string('name'); - }); + $result = $schema->table('users') + ->id() + ->string('name') + ->createIfNotExists(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE IF NOT EXISTS `users` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `name` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`))', $result->query); @@ -898,10 +898,10 @@ public function testCreateIfNotExists(): void public function testCreateTableWithRawColumnDefs(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->id(); - $table->rawColumn('`custom_col` VARCHAR(255) NOT NULL DEFAULT ""'); - }); + $result = $schema->table('t') + ->id() + ->rawColumn('`custom_col` VARCHAR(255) NOT NULL DEFAULT ""') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `custom_col` VARCHAR(255) NOT NULL DEFAULT "", PRIMARY KEY (`id`))', $result->query); @@ -910,11 +910,11 @@ public function testCreateTableWithRawColumnDefs(): void public function testCreateTableWithRawIndexDefs(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->id(); - $table->string('name'); - $table->rawIndex('INDEX `idx_custom` (`name`(10))'); - }); + $result = $schema->table('t') + ->id() + ->string('name') + ->rawIndex('INDEX `idx_custom` (`name`(10))') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `name` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`), INDEX `idx_custom` (`name`(10)))', $result->query); @@ -923,11 +923,11 @@ public function testCreateTableWithRawIndexDefs(): void public function testCreateTableWithPartitionByRange(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->id(); - $table->datetime('created_at'); - $table->partitionByRange('YEAR(created_at)'); - }); + $result = $schema->table('events') + ->id() + ->datetime('created_at') + ->partitionByRange('YEAR(created_at)') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `created_at` DATETIME NOT NULL, PRIMARY KEY (`id`)) PARTITION BY RANGE(YEAR(created_at))', $result->query); @@ -936,11 +936,11 @@ public function testCreateTableWithPartitionByRange(): void public function testCreateTableWithPartitionByList(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->id(); - $table->string('region'); - $table->partitionByList('region'); - }); + $result = $schema->table('events') + ->id() + ->string('region') + ->partitionByList('region') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `region` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`)) PARTITION BY LIST(region)', $result->query); @@ -949,10 +949,10 @@ public function testCreateTableWithPartitionByList(): void public function testCreateTableWithPartitionByHash(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->id(); - $table->partitionByHash('id'); - }); + $result = $schema->table('events') + ->id() + ->partitionByHash('id') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `events` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, PRIMARY KEY (`id`)) PARTITION BY HASH(id)', $result->query); @@ -961,12 +961,12 @@ public function testCreateTableWithPartitionByHash(): void public function testAlterWithForeignKeyOnDeleteAndUpdate(): void { $schema = new Schema(); - $result = $schema->alter('orders', function (Table $table) { - $table->addForeignKey('user_id') + $result = $schema->table('orders') + ->addForeignKey('user_id') ->references('id')->on('users') ->onDelete(ForeignKeyAction::Cascade) - ->onUpdate(ForeignKeyAction::SetNull); - }); + ->onUpdate(ForeignKeyAction::SetNull) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `orders` ADD FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE SET NULL', $result->query); @@ -1084,9 +1084,9 @@ public function testAnalyzeTable(): void public function testTableJsonColumn(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->json('metadata'); - }); + $result = $schema->table('t') + ->json('metadata') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`metadata` JSON NOT NULL)', $result->query); @@ -1095,9 +1095,9 @@ public function testTableJsonColumn(): void public function testTableBinaryColumn(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->binary('data'); - }); + $result = $schema->table('t') + ->binary('data') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`data` BLOB NOT NULL)', $result->query); @@ -1105,7 +1105,8 @@ public function testTableBinaryColumn(): void public function testColumnCollation(): void { - $col = new Column('name', ColumnType::String, 255); + $bp = (new Schema())->table('t'); + $col = new Column($bp, 'name', ColumnType::String, 255); $col->collation('utf8mb4_unicode_ci'); $this->assertSame('utf8mb4_unicode_ci', $col->collation); @@ -1113,18 +1114,19 @@ public function testColumnCollation(): void public function testColumnPrecision(): void { - $col = new Column('amount', ColumnType::Float, precision: 10); + $bp = (new Schema())->table('t'); + $col = new Column($bp, 'amount', ColumnType::Float, precision: 10); $this->assertSame(10, $col->precision); $this->assertNull($col->length); } - public function testTableAddIndexWithStringType(): void + public function testTableAddIndexWithEnumType(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addIndex('idx_name', ['name'], 'unique'); - }); + $result = $schema->table('users') + ->addIndex('idx_name', ['name'], IndexType::Unique) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `users` ADD UNIQUE INDEX `idx_name` (`name`)', $result->query); @@ -1157,10 +1159,10 @@ public function testIndexValidationInvalidCollation(): void public function testEnumBackslashEscaping(): void { $schema = new Schema(); - $result = $schema->create('items', function (Table $table) { + $result = $schema->table('items') // Input: `a\` and `b'c`. Expect backslash doubled and quote doubled. - $table->enum('status', ['a\\', "b'c"]); - }); + ->enum('status', ['a\\', "b'c"]) + ->create(); // Expect literal sequence: ENUM('a\\','b''c') (a + two backslashes) $this->assertSame("CREATE TABLE `items` (`status` ENUM('a\\\\','b''c') NOT NULL)", $result->query); @@ -1169,10 +1171,10 @@ public function testEnumBackslashEscaping(): void public function testDefaultValueBackslashEscaping(): void { $schema = new Schema(); - $result = $schema->create('items', function (Table $table) { + $result = $schema->table('items') // Input: a\' OR 1=1 -- . Expect backslash doubled, quote doubled. - $table->string('name')->default("a\\' OR 1=1 --"); - }); + ->string('name')->default("a\\' OR 1=1 --") + ->create(); $this->assertSame("CREATE TABLE `items` (`name` VARCHAR(255) NOT NULL DEFAULT 'a\\\\'' OR 1=1 --')", $result->query); } @@ -1180,9 +1182,9 @@ public function testDefaultValueBackslashEscaping(): void public function testCommentBackslashEscaping(): void { $schema = new Schema(); - $result = $schema->create('items', function (Table $table) { - $table->string('name')->comment('trailing\\'); - }); + $result = $schema->table('items') + ->string('name')->comment('trailing\\') + ->create(); $this->assertSame("CREATE TABLE `items` (`name` VARCHAR(255) NOT NULL COMMENT 'trailing\\\\')", $result->query); } @@ -1198,9 +1200,9 @@ public function testTableCommentBackslashEscaping(): void public function testSerialColumnMapsToIntWithAutoIncrement(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->serial('id')->primary(); - }); + $result = $schema->table('t') + ->serial('id')->primary() + ->create(); $this->assertSame('CREATE TABLE `t` (`id` INT AUTO_INCREMENT NOT NULL, PRIMARY KEY (`id`))', $result->query); } @@ -1208,9 +1210,9 @@ public function testSerialColumnMapsToIntWithAutoIncrement(): void public function testBigSerialColumnMapsToBigIntWithAutoIncrement(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->bigSerial('id')->primary(); - }); + $result = $schema->table('t') + ->bigSerial('id')->primary() + ->create(); $this->assertSame('CREATE TABLE `t` (`id` BIGINT AUTO_INCREMENT NOT NULL, PRIMARY KEY (`id`))', $result->query); } @@ -1220,9 +1222,9 @@ public function testUserTypeColumnThrowsUnsupported(): void $this->expectException(UnsupportedException::class); $schema = new Schema(); - $schema->create('t', function (Table $table) { - $table->integer('id')->primary(); - $table->string('mood')->userType('mood_type'); - }); + $schema->table('t') + ->integer('id')->primary() + ->string('mood')->userType('mood_type') + ->create(); } } diff --git a/tests/Query/Schema/PostgreSQLTest.php b/tests/Query/Schema/PostgreSQLTest.php index a4e00b4..a0f3ba3 100644 --- a/tests/Query/Schema/PostgreSQLTest.php +++ b/tests/Query/Schema/PostgreSQLTest.php @@ -23,14 +23,12 @@ use Utopia\Query\Schema\IndexType; use Utopia\Query\Schema\ParameterDirection; use Utopia\Query\Schema\PostgreSQL as Schema; -use Utopia\Query\Schema\Table; use Utopia\Query\Schema\TriggerEvent; use Utopia\Query\Schema\TriggerTiming; class PostgreSQLTest extends TestCase { use AssertsBindingCount; - // Feature interfaces public function testImplementsForeignKeys(): void { @@ -47,16 +45,14 @@ public function testImplementsTriggers(): void $this->assertInstanceOf(Triggers::class, new Schema()); } - // CREATE TABLE — PostgreSQL types - public function testCreateTableBasic(): void { $schema = new Schema(); - $result = $schema->create('users', function (Table $table) { - $table->id(); - $table->string('name', 255); - $table->string('email', 255)->unique(); - }); + $result = $schema->table('users') + ->id() + ->string('name', 255) + ->string('email', 255)->unique() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "users" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "name" VARCHAR(255) NOT NULL, "email" VARCHAR(255) NOT NULL, PRIMARY KEY ("id"), UNIQUE ("email"))', $result->query); @@ -65,18 +61,18 @@ public function testCreateTableBasic(): void public function testCreateTableColumnTypes(): void { $schema = new Schema(); - $result = $schema->create('test_types', function (Table $table) { - $table->integer('int_col'); - $table->bigInteger('big_col'); - $table->float('float_col'); - $table->boolean('bool_col'); - $table->text('text_col'); - $table->datetime('dt_col', 3); - $table->timestamp('ts_col', 6); - $table->json('json_col'); - $table->binary('bin_col'); - $table->enum('status', ['active', 'inactive']); - }); + $result = $schema->table('test_types') + ->integer('int_col') + ->bigInteger('big_col') + ->float('float_col') + ->boolean('bool_col') + ->text('text_col') + ->datetime('dt_col', 3) + ->timestamp('ts_col', 6) + ->json('json_col') + ->binary('bin_col') + ->enum('status', ['active', 'inactive']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "test_types" ("int_col" INTEGER NOT NULL, "big_col" BIGINT NOT NULL, "float_col" DOUBLE PRECISION NOT NULL, "bool_col" BOOLEAN NOT NULL, "text_col" TEXT NOT NULL, "dt_col" TIMESTAMP(3) NOT NULL, "ts_col" TIMESTAMP(6) WITHOUT TIME ZONE NOT NULL, "json_col" JSONB NOT NULL, "bin_col" BYTEA NOT NULL, "status" TEXT NOT NULL CHECK ("status" IN (\'active\', \'inactive\')))', $result->query); @@ -85,12 +81,12 @@ public function testCreateTableColumnTypes(): void public function testCreateTableSpatialTypes(): void { $schema = new Schema(); - $result = $schema->create('locations', function (Table $table) { - $table->id(); - $table->point('coords', 4326); - $table->linestring('path'); - $table->polygon('area'); - }); + $result = $schema->table('locations') + ->id() + ->point('coords', 4326) + ->linestring('path') + ->polygon('area') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "locations" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "coords" GEOMETRY(POINT, 4326) NOT NULL, "path" GEOMETRY(LINESTRING, 4326) NOT NULL, "area" GEOMETRY(POLYGON, 4326) NOT NULL, PRIMARY KEY ("id"))', $result->query); @@ -99,10 +95,10 @@ public function testCreateTableSpatialTypes(): void public function testCreateTableVectorType(): void { $schema = new Schema(); - $result = $schema->create('embeddings', function (Table $table) { - $table->id(); - $table->vector('embedding', 128); - }); + $result = $schema->table('embeddings') + ->id() + ->vector('embedding', 128) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "embeddings" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "embedding" VECTOR(128) NOT NULL, PRIMARY KEY ("id"))', $result->query); @@ -111,12 +107,11 @@ public function testCreateTableVectorType(): void public function testCreateTableUnsignedIgnored(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->integer('age')->unsigned(); - }); + $result = $schema->table('t') + ->integer('age')->unsigned() + ->create(); $this->assertBindingCount($result); - // PostgreSQL doesn't support UNSIGNED $this->assertStringNotContainsString('UNSIGNED', $result->query); $this->assertSame('CREATE TABLE "t" ("age" INTEGER NOT NULL)', $result->query); } @@ -124,28 +119,25 @@ public function testCreateTableUnsignedIgnored(): void public function testCreateTableNoInlineComment(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('name')->comment('User display name'); - }); + $result = $schema->table('t') + ->string('name')->comment('User display name') + ->create(); $this->assertBindingCount($result); - // PostgreSQL doesn't use inline COMMENT $this->assertStringNotContainsString('COMMENT', $result->query); } - // AUTO INCREMENT public function testAutoIncrementUsesIdentity(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->id(); - }); + $result = $schema->table('t') + ->id() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "t" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, PRIMARY KEY ("id"))', $result->query); $this->assertStringNotContainsString('AUTO_INCREMENT', $result->query); } - // DROP INDEX — no ON table public function testDropIndexNoOnTable(): void { @@ -154,7 +146,6 @@ public function testDropIndexNoOnTable(): void $this->assertSame('DROP INDEX "idx_email"', $result->query); } - // CREATE INDEX — USING method + operator class public function testCreateIndexWithGin(): void { @@ -188,7 +179,6 @@ public function testCreateIndexWithGist(): void $result->query ); } - // PROCEDURES — CREATE FUNCTION public function testCreateProcedureUsesFunction(): void { @@ -210,7 +200,6 @@ public function testDropProcedureUsesFunction(): void $this->assertSame('DROP FUNCTION "update_stats"', $result->query); } - // TRIGGERS — EXECUTE FUNCTION public function testCreateTriggerUsesExecuteFunction(): void { @@ -226,7 +215,6 @@ public function testCreateTriggerUsesExecuteFunction(): void $this->assertSame('CREATE FUNCTION "trg_updated_at_func"() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$; CREATE TRIGGER "trg_updated_at" BEFORE UPDATE ON "users" FOR EACH ROW EXECUTE FUNCTION "trg_updated_at_func"()', $result->query); $this->assertStringNotContainsString('BEGIN SET', $result->query); } - // FOREIGN KEY — DROP CONSTRAINT public function testDropForeignKeyUsesConstraint(): void { @@ -238,14 +226,13 @@ public function testDropForeignKeyUsesConstraint(): void $result->query ); } - // ALTER — PostgreSQL specifics public function testAlterModifyUsesAlterColumn(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->modifyColumn('name', 'string', 500); - }); + $result = $schema->table('users') + ->modifyColumn('name', ColumnType::String, 500) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "users" ALTER COLUMN "name" TYPE VARCHAR(500)', $result->query); @@ -254,9 +241,9 @@ public function testAlterModifyUsesAlterColumn(): void public function testAlterAddIndexUsesCreateIndex(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addIndex('idx_email', ['email']); - }); + $result = $schema->table('users') + ->addIndex('idx_email', ['email']) + ->alter(); $this->assertBindingCount($result); $this->assertStringNotContainsString('ADD INDEX', $result->query); @@ -266,9 +253,9 @@ public function testAlterAddIndexUsesCreateIndex(): void public function testAlterDropIndexIsStandalone(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->dropIndex('idx_email'); - }); + $result = $schema->table('users') + ->dropIndex('idx_email') + ->alter(); $this->assertBindingCount($result); $this->assertSame('DROP INDEX "idx_email"', $result->query); @@ -277,10 +264,10 @@ public function testAlterDropIndexIsStandalone(): void public function testAlterColumnAndIndexSeparateStatements(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addColumn('score', 'integer'); - $table->addIndex('idx_score', ['score']); - }); + $result = $schema->table('users') + ->addColumn('score', ColumnType::Integer) + ->addIndex('idx_score', ['score']) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "users" ADD COLUMN "score" INTEGER NOT NULL; CREATE INDEX "idx_score" ON "users" ("score")', $result->query); @@ -289,14 +276,13 @@ public function testAlterColumnAndIndexSeparateStatements(): void public function testAlterDropForeignKeyUsesConstraint(): void { $schema = new Schema(); - $result = $schema->alter('orders', function (Table $table) { - $table->dropForeignKey('fk_old'); - }); + $result = $schema->table('orders') + ->dropForeignKey('fk_old') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "orders" DROP CONSTRAINT "fk_old"', $result->query); } - // EXTENSIONS public function testCreateExtension(): void { @@ -313,7 +299,6 @@ public function testDropExtension(): void $this->assertSame('DROP EXTENSION IF EXISTS "vector"', $result->query); } - // Views — double-quote wrapping public function testCreateView(): void { @@ -334,12 +319,11 @@ public function testDropView(): void $this->assertSame('DROP VIEW "active_users"', $result->query); } - // Shared operations — still work with double quotes public function testDropTable(): void { $schema = new Schema(); - $result = $schema->drop('users'); + $result = $schema->table('users')->drop(); $this->assertBindingCount($result); $this->assertSame('DROP TABLE "users"', $result->query); @@ -348,7 +332,7 @@ public function testDropTable(): void public function testTruncateTable(): void { $schema = new Schema(); - $result = $schema->truncate('users'); + $result = $schema->table('users')->truncate(); $this->assertBindingCount($result); $this->assertSame('TRUNCATE TABLE "users"', $result->query); @@ -357,18 +341,16 @@ public function testTruncateTable(): void public function testRenameTableUsesAlterTable(): void { $schema = new Schema(); - $result = $schema->rename('users', 'members'); + $result = $schema->table('users')->rename('members'); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "users" RENAME TO "members"', $result->query); } - // Edge cases - public function testDropIfExists(): void { $schema = new Schema(); - $result = $schema->dropIfExists('users'); + $result = $schema->table('users')->dropIfExists(); $this->assertSame('DROP TABLE IF EXISTS "users"', $result->query); } @@ -385,10 +367,10 @@ public function testCreateOrReplaceView(): void public function testCreateTableWithMultiplePrimaryKeys(): void { $schema = new Schema(); - $result = $schema->create('order_items', function (Table $table) { - $table->integer('order_id')->primary(); - $table->integer('product_id')->primary(); - }); + $result = $schema->table('order_items') + ->integer('order_id')->primary() + ->integer('product_id')->primary() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "order_items" ("order_id" INTEGER NOT NULL, "product_id" INTEGER NOT NULL, PRIMARY KEY ("order_id", "product_id"))', $result->query); @@ -397,12 +379,12 @@ public function testCreateTableWithMultiplePrimaryKeys(): void public function testCreateTableWithCompositePrimaryKey(): void { $schema = new Schema(); - $result = $schema->create('order_items', function (Table $table) { - $table->integer('order_id'); - $table->integer('product_id'); - $table->integer('quantity'); - $table->primary(['order_id', 'product_id']); - }); + $result = $schema->table('order_items') + ->integer('order_id') + ->integer('product_id') + ->integer('quantity') + ->primary(['order_id', 'product_id']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "order_items" ("order_id" INTEGER NOT NULL, "product_id" INTEGER NOT NULL, "quantity" INTEGER NOT NULL, PRIMARY KEY ("order_id", "product_id"))', $result->query); @@ -415,19 +397,19 @@ public function testCreateTableRejectsMixedColumnAndTablePrimary(): void $this->expectException(ValidationException::class); $this->expectExceptionMessage('Cannot combine column-level primary() with Table::primary() composite key.'); - $schema->create('order_items', function (Table $table) { - $table->integer('order_id')->primary(); - $table->integer('product_id'); - $table->primary(['order_id', 'product_id']); - }); + $schema->table('order_items') + ->integer('order_id')->primary() + ->integer('product_id') + ->primary(['order_id', 'product_id']) + ->create(); } public function testCreateTableWithDefaultNull(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('name')->nullable()->default(null); - }); + $result = $schema->table('t') + ->string('name')->nullable()->default(null) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "t" ("name" VARCHAR(255) NULL DEFAULT NULL)', $result->query); @@ -436,11 +418,11 @@ public function testCreateTableWithDefaultNull(): void public function testAlterAddMultipleColumns(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addColumn('first_name', 'string', 100); - $table->addColumn('last_name', 'string', 100); - $table->dropColumn('name'); - }); + $result = $schema->table('users') + ->addColumn('first_name', ColumnType::String, 100) + ->addColumn('last_name', ColumnType::String, 100) + ->dropColumn('name') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "users" ADD COLUMN "first_name" VARCHAR(100) NOT NULL, ADD COLUMN "last_name" VARCHAR(100) NOT NULL, DROP COLUMN "name"', $result->query); @@ -449,9 +431,9 @@ public function testAlterAddMultipleColumns(): void public function testAlterAddForeignKey(): void { $schema = new Schema(); - $result = $schema->alter('orders', function (Table $table) { - $table->addForeignKey('user_id')->references('id')->on('users')->onDelete(ForeignKeyAction::Cascade); - }); + $result = $schema->table('orders') + ->addForeignKey('user_id')->references('id')->on('users')->onDelete(ForeignKeyAction::Cascade) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "orders" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE', $result->query); @@ -484,9 +466,9 @@ public function testCreateIndexMultiColumn(): void public function testAlterRenameColumn(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->renameColumn('bio', 'biography'); - }); + $result = $schema->table('users') + ->renameColumn('bio', 'biography') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "users" RENAME COLUMN "bio" TO "biography"', $result->query); @@ -495,10 +477,10 @@ public function testAlterRenameColumn(): void public function testCreateTableWithTimestamps(): void { $schema = new Schema(); - $result = $schema->create('posts', function (Table $table) { - $table->id(); - $table->timestamps(); - }); + $result = $schema->table('posts') + ->id() + ->timestamps() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "posts" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" TIMESTAMP(3) NOT NULL, "updated_at" TIMESTAMP(3) NOT NULL, PRIMARY KEY ("id"))', $result->query); @@ -507,12 +489,12 @@ public function testCreateTableWithTimestamps(): void public function testCreateTableWithForeignKey(): void { $schema = new Schema(); - $result = $schema->create('posts', function (Table $table) { - $table->id(); - $table->foreignKey('user_id') + $result = $schema->table('posts') + ->id() + ->foreignKey('user_id') ->references('id')->on('users') - ->onDelete(ForeignKeyAction::Cascade); - }); + ->onDelete(ForeignKeyAction::Cascade) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "posts" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, PRIMARY KEY ("id"), FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE)', $result->query); @@ -533,7 +515,6 @@ public function testDropTriggerFunction(): void { $schema = new Schema(); - // dropTrigger should use base SQL dropTrigger $result = $schema->dropTrigger('trg_old'); $this->assertSame('DROP TRIGGER "trg_old"', $result->query); @@ -542,25 +523,24 @@ public function testDropTriggerFunction(): void public function testAlterWithUniqueIndex(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addIndex('idx_email', ['email']); - $table->addIndex('idx_name', ['name']); - }); + $result = $schema->table('users') + ->addIndex('idx_email', ['email']) + ->addIndex('idx_name', ['name']) + ->alter(); $this->assertBindingCount($result); - // Both should be standalone CREATE INDEX statements $this->assertSame('CREATE INDEX "idx_email" ON "users" ("email"); CREATE INDEX "idx_name" ON "users" ("name")', $result->query); } public function testExactCreateTableWithTypes(): void { $schema = new Schema(); - $result = $schema->create('accounts', function (Table $table) { - $table->id(); - $table->string('username', 50); - $table->boolean('verified'); - $table->json('metadata'); - }); + $result = $schema->table('accounts') + ->id() + ->string('username', 50) + ->boolean('verified') + ->json('metadata') + ->create(); $this->assertSame( 'CREATE TABLE "accounts" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "username" VARCHAR(50) NOT NULL, "verified" BOOLEAN NOT NULL, "metadata" JSONB NOT NULL, PRIMARY KEY ("id"))', @@ -573,9 +553,9 @@ public function testExactCreateTableWithTypes(): void public function testExactAlterTableAddColumn(): void { $schema = new Schema(); - $result = $schema->alter('accounts', function (Table $table) { - $table->addColumn('bio', 'text')->nullable(); - }); + $result = $schema->table('accounts') + ->addColumn('bio', ColumnType::Text)->nullable() + ->alter(); $this->assertSame( 'ALTER TABLE "accounts" ADD COLUMN "bio" TEXT NULL', @@ -588,7 +568,7 @@ public function testExactAlterTableAddColumn(): void public function testExactDropTable(): void { $schema = new Schema(); - $result = $schema->drop('sessions'); + $result = $schema->table('sessions')->drop(); $this->assertSame('DROP TABLE "sessions"', $result->query); $this->assertSame([], $result->bindings); @@ -926,11 +906,11 @@ public function testCreateIndexInvalidOperatorClassThrows(): void public function testAlterAddColumnAndRenameAndDropCombined(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addColumn('phone', 'string', 20); - $table->renameColumn('bio', 'biography'); - $table->dropColumn('old_field'); - }); + $result = $schema->table('users') + ->addColumn('phone', ColumnType::String, 20) + ->renameColumn('bio', 'biography') + ->dropColumn('old_field') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "users" ADD COLUMN "phone" VARCHAR(20) NOT NULL, RENAME COLUMN "bio" TO "biography", DROP COLUMN "old_field"', $result->query); @@ -939,13 +919,13 @@ public function testAlterAddColumnAndRenameAndDropCombined(): void public function testAlterAddForeignKeyWithOnUpdate(): void { $schema = new Schema(); - $result = $schema->alter('orders', function (Table $table) { - $table->addForeignKey('user_id') + $result = $schema->table('orders') + ->addForeignKey('user_id') ->references('id') ->on('users') ->onDelete(ForeignKeyAction::Cascade) - ->onUpdate(ForeignKeyAction::SetNull); - }); + ->onUpdate(ForeignKeyAction::SetNull) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "orders" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE SET NULL', $result->query); @@ -954,9 +934,9 @@ public function testAlterAddForeignKeyWithOnUpdate(): void public function testAlterAddIndexWithMethod(): void { $schema = new Schema(); - $result = $schema->alter('docs', function (Table $table) { - $table->addIndex('idx_content', ['content'], IndexType::Index, method: 'gin'); - }); + $result = $schema->table('docs') + ->addIndex('idx_content', ['content'], IndexType::Index, method: 'gin') + ->alter(); $this->assertBindingCount($result); $this->assertSame('CREATE INDEX "idx_content" ON "docs" USING GIN ("content")', $result->query); @@ -965,9 +945,9 @@ public function testAlterAddIndexWithMethod(): void public function testColumnDefinitionUnsignedIgnored(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->integer('val')->unsigned(); - }); + $result = $schema->table('t') + ->integer('val')->unsigned() + ->create(); $this->assertBindingCount($result); $this->assertStringNotContainsString('UNSIGNED', $result->query); @@ -976,10 +956,10 @@ public function testColumnDefinitionUnsignedIgnored(): void public function testCreateIfNotExists(): void { $schema = new Schema(); - $result = $schema->createIfNotExists('users', function (Table $table) { - $table->id(); - $table->string('name'); - }); + $result = $schema->table('users') + ->id() + ->string('name') + ->createIfNotExists(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE IF NOT EXISTS "users" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "name" VARCHAR(255) NOT NULL, PRIMARY KEY ("id"))', $result->query); @@ -988,10 +968,10 @@ public function testCreateIfNotExists(): void public function testCreateTableWithRawColumnDefs(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->id(); - $table->rawColumn('"custom_col" TEXT NOT NULL DEFAULT \'\''); - }); + $result = $schema->table('t') + ->id() + ->rawColumn('"custom_col" TEXT NOT NULL DEFAULT \'\'') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "t" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "custom_col" TEXT NOT NULL DEFAULT \'\', PRIMARY KEY ("id"))', $result->query); @@ -1000,11 +980,11 @@ public function testCreateTableWithRawColumnDefs(): void public function testCreateTableWithRawIndexDefs(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->id(); - $table->string('name'); - $table->rawIndex('INDEX "idx_custom" ("name")'); - }); + $result = $schema->table('t') + ->id() + ->string('name') + ->rawIndex('INDEX "idx_custom" ("name")') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "t" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "name" VARCHAR(255) NOT NULL, PRIMARY KEY ("id"), INDEX "idx_custom" ("name"))', $result->query); @@ -1013,11 +993,11 @@ public function testCreateTableWithRawIndexDefs(): void public function testCreateTableWithPartitionByRange(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->id(); - $table->datetime('created_at'); - $table->partitionByRange('created_at'); - }); + $result = $schema->table('events') + ->id() + ->datetime('created_at') + ->partitionByRange('created_at') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "events" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" TIMESTAMP NOT NULL, PRIMARY KEY ("id")) PARTITION BY RANGE(created_at)', $result->query); @@ -1026,11 +1006,11 @@ public function testCreateTableWithPartitionByRange(): void public function testCreateTableWithPartitionByList(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->id(); - $table->string('region'); - $table->partitionByList('region'); - }); + $result = $schema->table('events') + ->id() + ->string('region') + ->partitionByList('region') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "events" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, "region" VARCHAR(255) NOT NULL, PRIMARY KEY ("id")) PARTITION BY LIST(region)', $result->query); @@ -1039,10 +1019,10 @@ public function testCreateTableWithPartitionByList(): void public function testCreateTableWithPartitionByHash(): void { $schema = new Schema(); - $result = $schema->create('events', function (Table $table) { - $table->id(); - $table->partitionByHash('id'); - }); + $result = $schema->table('events') + ->id() + ->partitionByHash('id') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "events" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, PRIMARY KEY ("id")) PARTITION BY HASH(id)', $result->query); @@ -1051,12 +1031,12 @@ public function testCreateTableWithPartitionByHash(): void public function testAlterWithForeignKeyOnDeleteAndUpdate(): void { $schema = new Schema(); - $result = $schema->alter('orders', function (Table $table) { - $table->addForeignKey('user_id') + $result = $schema->table('orders') + ->addForeignKey('user_id') ->references('id')->on('users') ->onDelete(ForeignKeyAction::Cascade) - ->onUpdate(ForeignKeyAction::SetNull); - }); + ->onUpdate(ForeignKeyAction::SetNull) + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE "orders" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE SET NULL', $result->query); @@ -1224,9 +1204,9 @@ public function testCreateProcedureRejectsDollarQuoteTerminatorInBody(): void public function testTableAddIndexWithStringType(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addIndex('idx_name', ['name'], 'unique'); - }); + $result = $schema->table('users') + ->addIndex('idx_name', ['name'], IndexType::Unique) + ->alter(); $this->assertBindingCount($result); $this->assertSame('CREATE UNIQUE INDEX "idx_name" ON "users" ("name")', $result->query); @@ -1235,9 +1215,9 @@ public function testTableAddIndexWithStringType(): void public function testCreateTableWithSerialColumnEmitsSerial(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->serial('id')->primary(); - }); + $result = $schema->table('t') + ->serial('id')->primary() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "t" ("id" SERIAL NOT NULL, PRIMARY KEY ("id"))', $result->query); @@ -1248,9 +1228,9 @@ public function testCreateTableWithSerialColumnEmitsSerial(): void public function testCreateTableWithBigSerialColumnEmitsBigSerial(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->bigSerial('id')->primary(); - }); + $result = $schema->table('t') + ->bigSerial('id')->primary() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "t" ("id" BIGSERIAL NOT NULL, PRIMARY KEY ("id"))', $result->query); @@ -1260,9 +1240,9 @@ public function testCreateTableWithBigSerialColumnEmitsBigSerial(): void public function testCreateTableWithSmallSerialColumnEmitsSmallSerial(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->smallSerial('id')->primary(); - }); + $result = $schema->table('t') + ->smallSerial('id')->primary() + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "t" ("id" SMALLSERIAL NOT NULL, PRIMARY KEY ("id"))', $result->query); @@ -1271,10 +1251,10 @@ public function testCreateTableWithSmallSerialColumnEmitsSmallSerial(): void public function testReferenceUserDefinedType(): void { $schema = new Schema(); - $result = $schema->create('surveys', function (Table $table) { - $table->integer('id')->primary(); - $table->string('mood')->userType('mood_type'); - }); + $result = $schema->table('surveys') + ->integer('id')->primary() + ->string('mood')->userType('mood_type') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE "surveys" ("id" INTEGER NOT NULL, "mood" "mood_type" NOT NULL, PRIMARY KEY ("id"))', $result->query); @@ -1285,7 +1265,8 @@ public function testUserTypeRejectsInvalidIdentifier(): void $this->expectException(ValidationException::class); $this->expectExceptionMessage('Invalid user-defined type name'); - $col = new Column('mood', ColumnType::String); + $bp = (new Schema())->table('t'); + $col = new Column($bp, 'mood', ColumnType::String); $col->userType('bad; DROP TABLE users'); } } diff --git a/tests/Query/Schema/SQLiteTest.php b/tests/Query/Schema/SQLiteTest.php index 3363433..246ed52 100644 --- a/tests/Query/Schema/SQLiteTest.php +++ b/tests/Query/Schema/SQLiteTest.php @@ -8,13 +8,13 @@ use Utopia\Query\Exception\UnsupportedException; use Utopia\Query\Exception\ValidationException; use Utopia\Query\Query; +use Utopia\Query\Schema\ColumnType; use Utopia\Query\Schema\Feature\ForeignKeys; use Utopia\Query\Schema\Feature\Procedures; use Utopia\Query\Schema\Feature\Triggers; use Utopia\Query\Schema\ForeignKeyAction; use Utopia\Query\Schema\ParameterDirection; use Utopia\Query\Schema\SQLite as Schema; -use Utopia\Query\Schema\Table; use Utopia\Query\Schema\TriggerEvent; use Utopia\Query\Schema\TriggerTiming; @@ -40,15 +40,15 @@ public function testImplementsTriggers(): void public function testCreateTableBasic(): void { $schema = new Schema(); - $result = $schema->create('users', function (Table $table) { - $table->id(); - $table->string('name', 255); - $table->string('email', 255)->unique(); - }); + $result = $schema->table('users') + ->id() + ->string('name', 255) + ->string('email', 255)->unique() + ->create(); $this->assertBindingCount($result); $this->assertSame( - 'CREATE TABLE `users` (`id` INTEGER AUTOINCREMENT NOT NULL, `name` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE (`email`))', + 'CREATE TABLE `users` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL, UNIQUE (`email`))', $result->query ); $this->assertSame([], $result->bindings); @@ -57,18 +57,18 @@ public function testCreateTableBasic(): void public function testCreateTableAllColumnTypes(): void { $schema = new Schema(); - $result = $schema->create('test_types', function (Table $table) { - $table->integer('int_col'); - $table->bigInteger('big_col'); - $table->float('float_col'); - $table->boolean('bool_col'); - $table->text('text_col'); - $table->datetime('dt_col', 3); - $table->timestamp('ts_col', 6); - $table->json('json_col'); - $table->binary('bin_col'); - $table->enum('status', ['active', 'inactive']); - }); + $result = $schema->table('test_types') + ->integer('int_col') + ->bigInteger('big_col') + ->float('float_col') + ->boolean('bool_col') + ->text('text_col') + ->datetime('dt_col', 3) + ->timestamp('ts_col', 6) + ->json('json_col') + ->binary('bin_col') + ->enum('status', ['active', 'inactive']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `test_types` (`int_col` INTEGER NOT NULL, `big_col` INTEGER NOT NULL, `float_col` REAL NOT NULL, `bool_col` INTEGER NOT NULL, `text_col` TEXT NOT NULL, `dt_col` TEXT NOT NULL, `ts_col` TEXT NOT NULL, `json_col` TEXT NOT NULL, `bin_col` BLOB NOT NULL, `status` TEXT NOT NULL)', $result->query); @@ -77,9 +77,9 @@ public function testCreateTableAllColumnTypes(): void public function testColumnTypeStringMapsToVarchar(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('name', 100); - }); + $result = $schema->table('t') + ->string('name', 100) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`name` VARCHAR(100) NOT NULL)', $result->query); @@ -88,9 +88,9 @@ public function testColumnTypeStringMapsToVarchar(): void public function testColumnTypeBooleanMapsToInteger(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->boolean('active'); - }); + $result = $schema->table('t') + ->boolean('active') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`active` INTEGER NOT NULL)', $result->query); @@ -99,9 +99,9 @@ public function testColumnTypeBooleanMapsToInteger(): void public function testColumnTypeDatetimeMapsToText(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->datetime('created_at'); - }); + $result = $schema->table('t') + ->datetime('created_at') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`created_at` TEXT NOT NULL)', $result->query); @@ -110,9 +110,9 @@ public function testColumnTypeDatetimeMapsToText(): void public function testColumnTypeTimestampMapsToText(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->timestamp('updated_at'); - }); + $result = $schema->table('t') + ->timestamp('updated_at') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`updated_at` TEXT NOT NULL)', $result->query); @@ -121,9 +121,9 @@ public function testColumnTypeTimestampMapsToText(): void public function testColumnTypeJsonMapsToText(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->json('data'); - }); + $result = $schema->table('t') + ->json('data') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`data` TEXT NOT NULL)', $result->query); @@ -132,9 +132,9 @@ public function testColumnTypeJsonMapsToText(): void public function testColumnTypeBinaryMapsToBlob(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->binary('content'); - }); + $result = $schema->table('t') + ->binary('content') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`content` BLOB NOT NULL)', $result->query); @@ -143,9 +143,9 @@ public function testColumnTypeBinaryMapsToBlob(): void public function testColumnTypeEnumMapsToText(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->enum('status', ['a', 'b']); - }); + $result = $schema->table('t') + ->enum('status', ['a', 'b']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`status` TEXT NOT NULL)', $result->query); @@ -154,11 +154,11 @@ public function testColumnTypeEnumMapsToText(): void public function testColumnTypeSpatialMapsToText(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->point('coords', 4326); - $table->linestring('path'); - $table->polygon('area'); - }); + $result = $schema->table('t') + ->point('coords', 4326) + ->linestring('path') + ->polygon('area') + ->create(); $this->assertBindingCount($result); $count = substr_count($result->query, 'TEXT NOT NULL'); @@ -168,9 +168,9 @@ public function testColumnTypeSpatialMapsToText(): void public function testColumnTypeUuid7MapsToVarchar36(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('uid', 36); - }); + $result = $schema->table('t') + ->string('uid', 36) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`uid` VARCHAR(36) NOT NULL)', $result->query); @@ -182,29 +182,29 @@ public function testColumnTypeVectorThrowsUnsupported(): void $this->expectExceptionMessage('Vector type is not supported in SQLite.'); $schema = new Schema(); - $schema->create('t', function (Table $table) { - $table->vector('embedding', 768); - }); + $schema->table('t') + ->vector('embedding', 768) + ->create(); } public function testAutoIncrementUsesAutoincrement(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->id(); - }); + $result = $schema->table('t') + ->id() + ->create(); $this->assertBindingCount($result); - $this->assertSame('CREATE TABLE `t` (`id` INTEGER AUTOINCREMENT NOT NULL, PRIMARY KEY (`id`))', $result->query); + $this->assertSame('CREATE TABLE `t` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)', $result->query); $this->assertStringNotContainsString('AUTO_INCREMENT', $result->query); } public function testUnsignedIsEmptyString(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->integer('age')->unsigned(); - }); + $result = $schema->table('t') + ->integer('age')->unsigned() + ->create(); $this->assertBindingCount($result); $this->assertStringNotContainsString('UNSIGNED', $result->query); @@ -231,7 +231,7 @@ public function testDropDatabaseThrowsUnsupported(): void public function testRenameUsesAlterTable(): void { $schema = new Schema(); - $result = $schema->rename('old_table', 'new_table'); + $result = $schema->table('old_table')->rename('new_table'); $this->assertBindingCount($result); $this->assertSame( @@ -244,7 +244,7 @@ public function testRenameUsesAlterTable(): void public function testTruncateUsesDeleteFrom(): void { $schema = new Schema(); - $result = $schema->truncate('users'); + $result = $schema->table('users')->truncate(); $this->assertBindingCount($result); $this->assertSame('DELETE FROM `users`', $result->query); @@ -273,51 +273,51 @@ public function testRenameIndexThrowsUnsupported(): void public function testCreateTableWithNullableAndDefault(): void { $schema = new Schema(); - $result = $schema->create('posts', function (Table $table) { - $table->id(); - $table->text('bio')->nullable(); - $table->boolean('active')->default(true); - $table->integer('score')->default(0); - $table->string('status')->default('draft'); - }); + $result = $schema->table('posts') + ->id() + ->text('bio')->nullable() + ->boolean('active')->default(true) + ->integer('score')->default(0) + ->string('status')->default('draft') + ->create(); $this->assertBindingCount($result); - $this->assertSame('CREATE TABLE `posts` (`id` INTEGER AUTOINCREMENT NOT NULL, `bio` TEXT NULL, `active` INTEGER NOT NULL DEFAULT 1, `score` INTEGER NOT NULL DEFAULT 0, `status` VARCHAR(255) NOT NULL DEFAULT \'draft\', PRIMARY KEY (`id`))', $result->query); + $this->assertSame('CREATE TABLE `posts` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bio` TEXT NULL, `active` INTEGER NOT NULL DEFAULT 1, `score` INTEGER NOT NULL DEFAULT 0, `status` VARCHAR(255) NOT NULL DEFAULT \'draft\')', $result->query); } public function testCreateTableWithForeignKey(): void { $schema = new Schema(); - $result = $schema->create('posts', function (Table $table) { - $table->id(); - $table->foreignKey('user_id') + $result = $schema->table('posts') + ->id() + ->foreignKey('user_id') ->references('id')->on('users') - ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::SetNull); - }); + ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::SetNull) + ->create(); $this->assertBindingCount($result); - $this->assertSame('CREATE TABLE `posts` (`id` INTEGER AUTOINCREMENT NOT NULL, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE SET NULL)', $result->query); + $this->assertSame('CREATE TABLE `posts` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE SET NULL)', $result->query); } public function testCreateTableWithIndexes(): void { $schema = new Schema(); - $result = $schema->create('users', function (Table $table) { - $table->id(); - $table->string('name'); - $table->string('email'); - $table->index(['name', 'email']); - $table->uniqueIndex(['email']); - }); + $result = $schema->table('users') + ->id() + ->string('name') + ->string('email') + ->index(['name', 'email']) + ->uniqueIndex(['email']) + ->create(); $this->assertBindingCount($result); - $this->assertSame('CREATE TABLE `users` (`id` INTEGER AUTOINCREMENT NOT NULL, `name` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL, PRIMARY KEY (`id`), INDEX `idx_name_email` (`name`, `email`), UNIQUE INDEX `uniq_email` (`email`))', $result->query); + $this->assertSame('CREATE TABLE `users` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL, INDEX `idx_name_email` (`name`, `email`), UNIQUE INDEX `uniq_email` (`email`))', $result->query); } public function testDropTable(): void { $schema = new Schema(); - $result = $schema->drop('users'); + $result = $schema->table('users')->drop(); $this->assertBindingCount($result); $this->assertSame('DROP TABLE `users`', $result->query); @@ -327,7 +327,7 @@ public function testDropTable(): void public function testDropTableIfExists(): void { $schema = new Schema(); - $result = $schema->dropIfExists('users'); + $result = $schema->table('users')->dropIfExists(); $this->assertSame('DROP TABLE IF EXISTS `users`', $result->query); } @@ -335,9 +335,9 @@ public function testDropTableIfExists(): void public function testAlterAddColumn(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addColumn('avatar_url', 'string', 255)->nullable(); - }); + $result = $schema->table('users') + ->addColumn('avatar_url', ColumnType::String, 255)->nullable() + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `users` ADD COLUMN `avatar_url` VARCHAR(255) NULL', $result->query); @@ -346,9 +346,9 @@ public function testAlterAddColumn(): void public function testAlterDropColumn(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->dropColumn('age'); - }); + $result = $schema->table('users') + ->dropColumn('age') + ->alter(); $this->assertBindingCount($result); $this->assertSame( @@ -360,9 +360,9 @@ public function testAlterDropColumn(): void public function testAlterRenameColumn(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->renameColumn('bio', 'biography'); - }); + $result = $schema->table('users') + ->renameColumn('bio', 'biography') + ->alter(); $this->assertBindingCount($result); $this->assertSame( @@ -511,11 +511,11 @@ public function testDropTrigger(): void public function testCreateTableWithMultiplePrimaryKeys(): void { $schema = new Schema(); - $result = $schema->create('order_items', function (Table $table) { - $table->integer('order_id')->primary(); - $table->integer('product_id')->primary(); - $table->integer('quantity'); - }); + $result = $schema->table('order_items') + ->integer('order_id')->primary() + ->integer('product_id')->primary() + ->integer('quantity') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `order_items` (`order_id` INTEGER NOT NULL, `product_id` INTEGER NOT NULL, `quantity` INTEGER NOT NULL, PRIMARY KEY (`order_id`, `product_id`))', $result->query); @@ -524,12 +524,12 @@ public function testCreateTableWithMultiplePrimaryKeys(): void public function testCreateTableWithCompositePrimaryKey(): void { $schema = new Schema(); - $result = $schema->create('order_items', function (Table $table) { - $table->integer('order_id'); - $table->integer('product_id'); - $table->integer('quantity'); - $table->primary(['order_id', 'product_id']); - }); + $result = $schema->table('order_items') + ->integer('order_id') + ->integer('product_id') + ->integer('quantity') + ->primary(['order_id', 'product_id']) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `order_items` (`order_id` INTEGER NOT NULL, `product_id` INTEGER NOT NULL, `quantity` INTEGER NOT NULL, PRIMARY KEY (`order_id`, `product_id`))', $result->query); @@ -542,19 +542,19 @@ public function testCreateTableRejectsMixedColumnAndTablePrimary(): void $this->expectException(ValidationException::class); $this->expectExceptionMessage('Cannot combine column-level primary() with Table::primary() composite key.'); - $schema->create('order_items', function (Table $table) { - $table->integer('order_id')->primary(); - $table->integer('product_id'); - $table->primary(['order_id', 'product_id']); - }); + $schema->table('order_items') + ->integer('order_id')->primary() + ->integer('product_id') + ->primary(['order_id', 'product_id']) + ->create(); } public function testCreateTableWithDefaultNull(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->string('name')->nullable()->default(null); - }); + $result = $schema->table('t') + ->string('name')->nullable()->default(null) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`name` VARCHAR(255) NULL DEFAULT NULL)', $result->query); @@ -563,9 +563,9 @@ public function testCreateTableWithDefaultNull(): void public function testCreateTableWithNumericDefault(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->float('score')->default(0.5); - }); + $result = $schema->table('t') + ->float('score')->default(0.5) + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`score` REAL NOT NULL DEFAULT 0.5)', $result->query); @@ -574,28 +574,28 @@ public function testCreateTableWithNumericDefault(): void public function testCreateTableWithTimestamps(): void { $schema = new Schema(); - $result = $schema->create('posts', function (Table $table) { - $table->id(); - $table->timestamps(); - }); + $result = $schema->table('posts') + ->id() + ->timestamps() + ->create(); $this->assertBindingCount($result); - $this->assertSame('CREATE TABLE `posts` (`id` INTEGER AUTOINCREMENT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY (`id`))', $result->query); + $this->assertSame('CREATE TABLE `posts` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL)', $result->query); } public function testExactCreateTableWithColumnsAndIndexes(): void { $schema = new Schema(); - $result = $schema->create('products', function (Table $table) { - $table->id(); - $table->string('name', 100); - $table->integer('price'); - $table->index(['name']); - }); + $result = $schema->table('products') + ->id() + ->string('name', 100) + ->integer('price') + ->index(['name']) + ->create(); $this->assertBindingCount($result); $this->assertSame( - 'CREATE TABLE `products` (`id` INTEGER AUTOINCREMENT NOT NULL, `name` VARCHAR(100) NOT NULL, `price` INTEGER NOT NULL, PRIMARY KEY (`id`), INDEX `idx_name` (`name`))', + 'CREATE TABLE `products` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` VARCHAR(100) NOT NULL, `price` INTEGER NOT NULL, INDEX `idx_name` (`name`))', $result->query ); $this->assertSame([], $result->bindings); @@ -604,7 +604,7 @@ public function testExactCreateTableWithColumnsAndIndexes(): void public function testExactDropTable(): void { $schema = new Schema(); - $result = $schema->drop('sessions'); + $result = $schema->table('sessions')->drop(); $this->assertSame('DROP TABLE `sessions`', $result->query); $this->assertSame([], $result->bindings); @@ -614,7 +614,7 @@ public function testExactDropTable(): void public function testExactRenameTable(): void { $schema = new Schema(); - $result = $schema->rename('old_name', 'new_name'); + $result = $schema->table('old_name')->rename('new_name'); $this->assertSame('ALTER TABLE `old_name` RENAME TO `new_name`', $result->query); $this->assertSame([], $result->bindings); @@ -624,7 +624,7 @@ public function testExactRenameTable(): void public function testExactTruncateTable(): void { $schema = new Schema(); - $result = $schema->truncate('logs'); + $result = $schema->table('logs')->truncate(); $this->assertSame('DELETE FROM `logs`', $result->query); $this->assertSame([], $result->bindings); @@ -644,16 +644,16 @@ public function testExactDropIndex(): void public function testExactCreateTableWithForeignKey(): void { $schema = new Schema(); - $result = $schema->create('orders', function (Table $table) { - $table->id(); - $table->integer('customer_id'); - $table->foreignKey('customer_id') + $result = $schema->table('orders') + ->id() + ->integer('customer_id') + ->foreignKey('customer_id') ->references('id')->on('customers') - ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::Cascade); - }); + ->onDelete(ForeignKeyAction::Cascade)->onUpdate(ForeignKeyAction::Cascade) + ->create(); $this->assertSame( - 'CREATE TABLE `orders` (`id` INTEGER AUTOINCREMENT NOT NULL, `customer_id` INTEGER NOT NULL, PRIMARY KEY (`id`), FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE)', + 'CREATE TABLE `orders` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `customer_id` INTEGER NOT NULL, FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE)', $result->query ); $this->assertSame([], $result->bindings); @@ -663,9 +663,9 @@ public function testExactCreateTableWithForeignKey(): void public function testColumnTypeFloatMapsToReal(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->float('ratio'); - }); + $result = $schema->table('t') + ->float('ratio') + ->create(); $this->assertBindingCount($result); $this->assertSame('CREATE TABLE `t` (`ratio` REAL NOT NULL)', $result->query); @@ -674,9 +674,9 @@ public function testColumnTypeFloatMapsToReal(): void public function testCreateIfNotExists(): void { $schema = new Schema(); - $result = $schema->createIfNotExists('t', function (Table $table) { - $table->integer('id')->primary(); - }); + $result = $schema->table('t') + ->integer('id')->primary() + ->createIfNotExists(); $this->assertBindingCount($result); $this->assertStringStartsWith('CREATE TABLE IF NOT EXISTS', $result->query); @@ -685,11 +685,11 @@ public function testCreateIfNotExists(): void public function testAlterMultipleOperations(): void { $schema = new Schema(); - $result = $schema->alter('users', function (Table $table) { - $table->addColumn('avatar', 'string', 255)->nullable(); - $table->dropColumn('age'); - $table->renameColumn('bio', 'biography'); - }); + $result = $schema->table('users') + ->addColumn('avatar', ColumnType::String, 255)->nullable() + ->dropColumn('age') + ->renameColumn('bio', 'biography') + ->alter(); $this->assertBindingCount($result); $this->assertSame('ALTER TABLE `users` ADD COLUMN `avatar` VARCHAR(255) NULL, RENAME COLUMN `bio` TO `biography`, DROP COLUMN `age`', $result->query); @@ -698,11 +698,11 @@ public function testAlterMultipleOperations(): void public function testSerialColumnMapsToInteger(): void { $schema = new Schema(); - $result = $schema->create('t', function (Table $table) { - $table->serial('id')->primary(); - }); + $result = $schema->table('t') + ->serial('id')->primary() + ->create(); - $this->assertSame('CREATE TABLE `t` (`id` INTEGER AUTOINCREMENT NOT NULL, PRIMARY KEY (`id`))', $result->query); + $this->assertSame('CREATE TABLE `t` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)', $result->query); } public function testUserTypeColumnThrowsUnsupported(): void @@ -710,9 +710,9 @@ public function testUserTypeColumnThrowsUnsupported(): void $this->expectException(UnsupportedException::class); $schema = new Schema(); - $schema->create('t', function (Table $table) { - $table->integer('id')->primary(); - $table->string('mood')->userType('mood_type'); - }); + $schema->table('t') + ->integer('id')->primary() + ->string('mood')->userType('mood_type') + ->create(); } } diff --git a/tests/Query/Schema/TableTest.php b/tests/Query/Schema/TableTest.php index fc96d7a..873a0f7 100644 --- a/tests/Query/Schema/TableTest.php +++ b/tests/Query/Schema/TableTest.php @@ -52,7 +52,7 @@ public function testColumnsPropertyNotWritableExternally(): void $this->expectException(\Error::class); /** @phpstan-ignore-next-line */ - $bp->columns = [new Column('x', ColumnType::String)]; + $bp->columns = [new Column($bp, 'x', ColumnType::String)]; } public function testColumnsPopulatedById(): void @@ -79,7 +79,7 @@ public function testColumnsPopulatedByAddColumn(): void public function testColumnsPopulatedByModifyColumn(): void { $bp = new Table(); - $bp->modifyColumn('score', 'integer'); + $bp->modifyColumn('score', ColumnType::Integer); $this->assertCount(1, $bp->columns); $this->assertTrue($bp->columns[0]->isModify); @@ -437,7 +437,8 @@ public function testColumnCheckAttachesExpression(): void public function testColumnGeneratedAsDefaultsToVirtualOnCompile(): void { - $col = new Column('area', ColumnType::Integer); + $bp = new Table(); + $col = new Column($bp, 'area', ColumnType::Integer); $col->generatedAs('`width` * `height`'); $this->assertSame('`width` * `height`', $col->generatedExpression); @@ -446,7 +447,8 @@ public function testColumnGeneratedAsDefaultsToVirtualOnCompile(): void public function testColumnStoredAndVirtualAreMutuallyExclusive(): void { - $col = new Column('area', ColumnType::Integer); + $bp = new Table(); + $col = new Column($bp, 'area', ColumnType::Integer); $col->generatedAs('`width` * `height`')->stored(); $this->assertTrue($col->generatedStored);