diff --git a/README.md b/README.md index 2263ce1..d594b25 100644 --- a/README.md +++ b/README.md @@ -1983,9 +1983,9 @@ $result = $schema->table('users') ->createIfNotExists(); ``` -Available column types: `id`, `uuid`, `string`, `text`, `mediumText`, `longText`, `tinyInteger`, `smallInteger`, `integer`, `bigInteger`, `serial`, `bigSerial`, `smallSerial`, `float`, `decimal`, `boolean`, `datetime`, `timestamp`, `json`, `binary`, `enum`, `point`, `linestring`, `polygon`, `vector` (PostgreSQL, ClickHouse, MongoDB), `timestamps`. +Available column types: `id`, `uuid`, `string`, `text`, `mediumText`, `longText`, `tinyInteger`, `smallInteger`, `integer`, `bigInteger`, `serial`, `bigSerial`, `smallSerial`, `float`, `decimal`, `boolean`, `datetime`, `timestamp`, `json`, `binary`, `enum`, `point`, `linestring`, `polygon`, `vector` (PostgreSQL, ClickHouse, MongoDB — only PostgreSQL takes a `$dimensions` argument), `timestamps`. -Column modifiers available on every dialect: `nullable()`, `default($value)`, `defaultRaw($expression)`, `primary()`, `unsigned()`, `autoIncrement()`, `srid($srid)` (spatial columns), `dimensions($dimensions)` (vector columns). +Column modifiers available on every dialect: `nullable()`, `default($value)`, `defaultRaw($expression)`, `primary()`, `unsigned()`. The rest are only on the dialects that emit them, so an unsupported combination is a type error rather than something silently dropped: @@ -2000,10 +2000,15 @@ The rest are only on the dialects that emit them, so an unsupported combination | `comment($text)` | MySQL, MariaDB, SQLite, ClickHouse, MongoDB | PostgreSQL needs a separate statement — use `commentOnColumn()` | | `ttl($expression)` | ClickHouse | — | | `userType($name)` | PostgreSQL | — | +| `srid($srid)` | MySQL, MariaDB, PostgreSQL | SQLite stores geometry as `TEXT`, ClickHouse as a `Tuple`, MongoDB as an object — none carry an SRID | +| `dimensions($dimensions)` | PostgreSQL | ClickHouse's `Array(Float64)` and MongoDB's array bsonType are unsized | +| `autoIncrement()` | MySQL, MariaDB, PostgreSQL, SQLite | ClickHouse has none; MongoDB assigns `_id` itself | Likewise `serial()` / `bigSerial()` / `smallSerial()` are absent from the ClickHouse table, and `dropColumn()` / `renameColumn()` are absent from the MongoDB table. -> **Known gaps.** `unsigned()` is accepted everywhere but renders nothing on PostgreSQL and SQLite, which have no unsigned integer types — you get a signed column. `srid()`, `autoIncrement()` and the `$dimensions` argument to `vector()` are likewise accepted on dialects that cannot express them, because the library's own column factories set them internally. Treat those four as advisory rather than guaranteed. +The column factories still work everywhere: `point($name, $srid)`, `id()` and `serial()` set the SRID and auto-increment flag intrinsically, so portable schema code is unaffected — only the standalone modifiers are scoped. `Table\ClickHouse::vector()` and `Table\MongoDB::vector()` take no `$dimensions` argument, since neither type records one. + +> **Known gap.** `unsigned()` is accepted everywhere but renders nothing on PostgreSQL and SQLite, which have no unsigned integer types — you get a signed column. This one is a deliberate dialect mapping via an overridable `compileUnsigned()` hook, not an oversight. **Raw default expressions** — use `defaultRaw($expression)` for dialect-specific server-generated defaults that `default()` would otherwise quote as a string literal (`now()`, `CURRENT_TIMESTAMP`, `gen_random_uuid()`, `generateUUIDv4()`, `UUID()`, …). The expression is emitted verbatim and must come from a trusted source; it must not be empty or contain a semicolon. Takes precedence over `default()` when both are set. diff --git a/src/Query/Schema/Column.php b/src/Query/Schema/Column.php index 20c356f..57ee980 100644 --- a/src/Query/Schema/Column.php +++ b/src/Query/Schema/Column.php @@ -62,6 +62,12 @@ class Column public protected(set) ?string $userTypeName = null; /** + * $srid, $dimensions and $autoIncrement are intrinsic to the column the way + * $length is, and are set by the Table factories that create it (point(), + * vector(), id(), serial()). They are constructor parameters rather than + * modifier calls so that the factories do not depend on modifier methods + * that only some dialects expose. + * * @param TTable $table */ public function __construct( @@ -71,7 +77,13 @@ public function __construct( public ?int $length = null, public ?int $precision = null, public ?int $scale = null, + ?int $srid = null, + ?int $dimensions = null, + bool $autoIncrement = false, ) { + $this->srid = $srid; + $this->dimensions = $dimensions; + $this->isAutoIncrement = $autoIncrement; } public function nullable(): static @@ -135,13 +147,6 @@ public function primary(): static|Table return $this; } - public function autoIncrement(): static - { - $this->isAutoIncrement = true; - - return $this; - } - /** * 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 @@ -167,20 +172,6 @@ public function enum(string|array $nameOrValues, ?array $values = null): static| return $this->table->enum($nameOrValues, $values ?? []); } - public function srid(int $srid): static - { - $this->srid = $srid; - - return $this; - } - - public function dimensions(int $dimensions): static - { - $this->dimensions = $dimensions; - - return $this; - } - public function modify(): static { $this->isModify = true; diff --git a/src/Query/Schema/Column/MySQL.php b/src/Query/Schema/Column/MySQL.php index 986c862..acb93e5 100644 --- a/src/Query/Schema/Column/MySQL.php +++ b/src/Query/Schema/Column/MySQL.php @@ -11,6 +11,8 @@ */ class MySQL extends Column { + use Trait\Srid; + use Trait\AutoIncrement; use Trait\Collation; use Trait\Positioning; use Trait\Comment; diff --git a/src/Query/Schema/Column/PostgreSQL.php b/src/Query/Schema/Column/PostgreSQL.php index d93c9aa..34c3acc 100644 --- a/src/Query/Schema/Column/PostgreSQL.php +++ b/src/Query/Schema/Column/PostgreSQL.php @@ -12,6 +12,9 @@ */ class PostgreSQL extends Column { + use Trait\Srid; + use Trait\Dimensions; + use Trait\AutoIncrement; use Trait\Collation; use Trait\Unique; use Trait\Generated; diff --git a/src/Query/Schema/Column/SQLite.php b/src/Query/Schema/Column/SQLite.php index 12b7d70..71592aa 100644 --- a/src/Query/Schema/Column/SQLite.php +++ b/src/Query/Schema/Column/SQLite.php @@ -11,6 +11,7 @@ */ class SQLite extends Column { + use Trait\AutoIncrement; use Trait\Collation; use Trait\Comment; use Trait\Unique; diff --git a/src/Query/Schema/Column/Trait/AutoIncrement.php b/src/Query/Schema/Column/Trait/AutoIncrement.php new file mode 100644 index 0000000..cf89a6a --- /dev/null +++ b/src/Query/Schema/Column/Trait/AutoIncrement.php @@ -0,0 +1,17 @@ +isAutoIncrement = true; + + return $this; + } +} diff --git a/src/Query/Schema/Column/Trait/Dimensions.php b/src/Query/Schema/Column/Trait/Dimensions.php new file mode 100644 index 0000000..c427bfe --- /dev/null +++ b/src/Query/Schema/Column/Trait/Dimensions.php @@ -0,0 +1,18 @@ +dimensions = $dimensions; + + return $this; + } +} diff --git a/src/Query/Schema/Column/Trait/Srid.php b/src/Query/Schema/Column/Trait/Srid.php new file mode 100644 index 0000000..b33a218 --- /dev/null +++ b/src/Query/Schema/Column/Trait/Srid.php @@ -0,0 +1,21 @@ +srid = $srid; + + return $this; + } +} diff --git a/src/Query/Schema/Forwarder/ClickHouse.php b/src/Query/Schema/Forwarder/ClickHouse.php index 49dcc0a..27c50f4 100644 --- a/src/Query/Schema/Forwarder/ClickHouse.php +++ b/src/Query/Schema/Forwarder/ClickHouse.php @@ -14,9 +14,9 @@ */ trait ClickHouse { - public function vector(string $name, int $dimensions): Column\ClickHouse + public function vector(string $name): Column\ClickHouse { - return $this->table->vector($name, $dimensions); + return $this->table->vector($name); } public function fixedString(string $name, int $length): Column\ClickHouse diff --git a/src/Query/Schema/Forwarder/MongoDB.php b/src/Query/Schema/Forwarder/MongoDB.php index a77597e..b2712d5 100644 --- a/src/Query/Schema/Forwarder/MongoDB.php +++ b/src/Query/Schema/Forwarder/MongoDB.php @@ -11,9 +11,9 @@ */ trait MongoDB { - public function vector(string $name, int $dimensions): Column\MongoDB + public function vector(string $name): Column\MongoDB { - return $this->table->vector($name, $dimensions); + return $this->table->vector($name); } public function serial(string $name): Column\MongoDB { diff --git a/src/Query/Schema/Table.php b/src/Query/Schema/Table.php index 92f0f2d..36e644e 100644 --- a/src/Query/Schema/Table.php +++ b/src/Query/Schema/Table.php @@ -119,10 +119,10 @@ private function requireSchema(): Schema * * @return TColumn */ - protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null): Column + protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null, ?int $srid = null, ?int $dimensions = null, bool $autoIncrement = false): Column { /** @var TColumn */ - return new Column($this, $name, $type, $length, $precision, $scale); + return new Column($this, $name, $type, $length, $precision, $scale, $srid, $dimensions, $autoIncrement); } /** @@ -141,9 +141,8 @@ protected function newForeignKey(string $column): ForeignKey /** @return TColumn */ public function id(string $name = 'id'): Column { - $col = $this->newColumn($name, ColumnType::BigInteger); + $col = $this->newColumn($name, ColumnType::BigInteger, autoIncrement: true); $col->unsigned(); - $col->autoIncrement(); $col->primary(); $this->columns[] = $col; @@ -348,8 +347,7 @@ public function enum(string $name, array $values): Column /** @return TColumn */ public function point(string $name, int $srid = 4326): Column { - $col = $this->newColumn($name, ColumnType::Point); - $col->srid($srid); + $col = $this->newColumn($name, ColumnType::Point, srid: $srid); $this->columns[] = $col; return $col; @@ -358,8 +356,7 @@ public function point(string $name, int $srid = 4326): Column /** @return TColumn */ public function linestring(string $name, int $srid = 4326): Column { - $col = $this->newColumn($name, ColumnType::Linestring); - $col->srid($srid); + $col = $this->newColumn($name, ColumnType::Linestring, srid: $srid); $this->columns[] = $col; return $col; @@ -368,8 +365,7 @@ public function linestring(string $name, int $srid = 4326): Column /** @return TColumn */ public function polygon(string $name, int $srid = 4326): Column { - $col = $this->newColumn($name, ColumnType::Polygon); - $col->srid($srid); + $col = $this->newColumn($name, ColumnType::Polygon, srid: $srid); $this->columns[] = $col; return $col; diff --git a/src/Query/Schema/Table/ClickHouse.php b/src/Query/Schema/Table/ClickHouse.php index 8db2775..fe81bb1 100644 --- a/src/Query/Schema/Table/ClickHouse.php +++ b/src/Query/Schema/Table/ClickHouse.php @@ -24,15 +24,14 @@ class ClickHouse extends Table public protected(set) ?string $orderByRaw = null; #[\Override] - protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null): Column\ClickHouse + protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null, ?int $srid = null, ?int $dimensions = null, bool $autoIncrement = false): Column\ClickHouse { - return new Column\ClickHouse($this, $name, $type, $length, $precision, $scale); + return new Column\ClickHouse($this, $name, $type, $length, $precision, $scale, $srid, $dimensions, $autoIncrement); } - public function vector(string $name, int $dimensions): Column\ClickHouse + public function vector(string $name): Column\ClickHouse { $col = $this->newColumn($name, ColumnType::Vector); - $col->dimensions($dimensions); $this->columns[] = $col; return $col; diff --git a/src/Query/Schema/Table/MongoDB.php b/src/Query/Schema/Table/MongoDB.php index 9b1ee1e..41408c4 100644 --- a/src/Query/Schema/Table/MongoDB.php +++ b/src/Query/Schema/Table/MongoDB.php @@ -15,15 +15,14 @@ class MongoDB extends Table /** @use Trait\Serial */ use Trait\Serial; #[\Override] - protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null): Column\MongoDB + protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null, ?int $srid = null, ?int $dimensions = null, bool $autoIncrement = false): Column\MongoDB { - return new Column\MongoDB($this, $name, $type, $length, $precision, $scale); + return new Column\MongoDB($this, $name, $type, $length, $precision, $scale, $srid, $dimensions, $autoIncrement); } - public function vector(string $name, int $dimensions): Column\MongoDB + public function vector(string $name): Column\MongoDB { $col = $this->newColumn($name, ColumnType::Vector); - $col->dimensions($dimensions); $this->columns[] = $col; return $col; diff --git a/src/Query/Schema/Table/MySQL.php b/src/Query/Schema/Table/MySQL.php index 6ff618c..6f00856 100644 --- a/src/Query/Schema/Table/MySQL.php +++ b/src/Query/Schema/Table/MySQL.php @@ -23,9 +23,9 @@ class MySQL extends Table use Trait\StandardPartitioning; #[\Override] - protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null): Column\MySQL + protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null, ?int $srid = null, ?int $dimensions = null, bool $autoIncrement = false): Column\MySQL { - return new Column\MySQL($this, $name, $type, $length, $precision, $scale); + return new Column\MySQL($this, $name, $type, $length, $precision, $scale, $srid, $dimensions, $autoIncrement); } #[\Override] diff --git a/src/Query/Schema/Table/PostgreSQL.php b/src/Query/Schema/Table/PostgreSQL.php index 5087cf0..f7361b8 100644 --- a/src/Query/Schema/Table/PostgreSQL.php +++ b/src/Query/Schema/Table/PostgreSQL.php @@ -23,9 +23,9 @@ class PostgreSQL extends Table use Trait\StandardPartitioning; #[\Override] - protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null): Column\PostgreSQL + protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null, ?int $srid = null, ?int $dimensions = null, bool $autoIncrement = false): Column\PostgreSQL { - return new Column\PostgreSQL($this, $name, $type, $length, $precision, $scale); + return new Column\PostgreSQL($this, $name, $type, $length, $precision, $scale, $srid, $dimensions, $autoIncrement); } #[\Override] @@ -36,8 +36,7 @@ protected function newForeignKey(string $column): ForeignKey\PostgreSQL public function vector(string $name, int $dimensions): Column\PostgreSQL { - $col = $this->newColumn($name, ColumnType::Vector); - $col->dimensions($dimensions); + $col = $this->newColumn($name, ColumnType::Vector, dimensions: $dimensions); $this->columns[] = $col; return $col; diff --git a/src/Query/Schema/Table/SQLite.php b/src/Query/Schema/Table/SQLite.php index 9e7dce4..20a5e14 100644 --- a/src/Query/Schema/Table/SQLite.php +++ b/src/Query/Schema/Table/SQLite.php @@ -21,9 +21,9 @@ class SQLite extends Table use Trait\InlineForeignKey; #[\Override] - protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null): Column\SQLite + protected function newColumn(string $name, ColumnType $type, ?int $length = null, ?int $precision = null, ?int $scale = null, ?int $srid = null, ?int $dimensions = null, bool $autoIncrement = false): Column\SQLite { - return new Column\SQLite($this, $name, $type, $length, $precision, $scale); + return new Column\SQLite($this, $name, $type, $length, $precision, $scale, $srid, $dimensions, $autoIncrement); } #[\Override] diff --git a/src/Query/Schema/Table/Trait/Serial.php b/src/Query/Schema/Table/Trait/Serial.php index 453941a..c026a6b 100644 --- a/src/Query/Schema/Table/Trait/Serial.php +++ b/src/Query/Schema/Table/Trait/Serial.php @@ -25,8 +25,7 @@ trait Serial */ public function serial(string $name): Column { - $col = $this->newColumn($name, ColumnType::Serial); - $col->autoIncrement(); + $col = $this->newColumn($name, ColumnType::Serial, autoIncrement: true); $this->columns[] = $col; return $col; @@ -41,8 +40,7 @@ public function serial(string $name): Column */ public function bigSerial(string $name): Column { - $col = $this->newColumn($name, ColumnType::BigSerial); - $col->autoIncrement(); + $col = $this->newColumn($name, ColumnType::BigSerial, autoIncrement: true); $this->columns[] = $col; return $col; @@ -57,8 +55,7 @@ public function bigSerial(string $name): Column */ public function smallSerial(string $name): Column { - $col = $this->newColumn($name, ColumnType::SmallSerial); - $col->autoIncrement(); + $col = $this->newColumn($name, ColumnType::SmallSerial, autoIncrement: true); $this->columns[] = $col; return $col; diff --git a/tests/Query/Schema/ClickHouseTest.php b/tests/Query/Schema/ClickHouseTest.php index be6cb81..a2b49b2 100644 --- a/tests/Query/Schema/ClickHouseTest.php +++ b/tests/Query/Schema/ClickHouseTest.php @@ -112,7 +112,7 @@ public function testCreateTableWithVector(): void { $schema = new Schema(); $result = $schema->table('embeddings') - ->vector('embedding', 768) + ->vector('embedding') ->create(); $this->assertBindingCount($result); @@ -514,11 +514,19 @@ public function testCreateTableIfNotExists(): void $this->assertSame('CREATE TABLE IF NOT EXISTS `events` (`id` Int64, `name` String) ENGINE = MergeTree() ORDER BY (`id`)', $result->query); } - public function testCompileAutoIncrementReturnsEmpty(): void + public function testDoesNotExposeAutoIncrementAndEmitsNone(): void { $schema = new Schema(); - $result = $schema->table('t') - ->bigInteger('id')->primary()->autoIncrement() + $column = $schema->table('t')->bigInteger('id'); + + // ClickHouse has no auto-increment, so the modifier is absent rather + // than accepted and ignored. id() still works and emits nothing for it. + $this->assertNotContains('autoIncrement', \get_class_methods($column)); + + $result = $schema->table('t2') + ->id() + ->engine(Engine::MergeTree) + ->orderBy(['id']) ->create(); $this->assertBindingCount($result); diff --git a/tests/Query/Schema/FluentBuilderTest.php b/tests/Query/Schema/FluentBuilderTest.php index d36e29a..275f8a6 100644 --- a/tests/Query/Schema/FluentBuilderTest.php +++ b/tests/Query/Schema/FluentBuilderTest.php @@ -17,8 +17,11 @@ use Utopia\Query\Schema\PostgreSQL; use Utopia\Query\Schema\SQLite; use Utopia\Query\Schema\Table as BaseTable; +use Utopia\Query\Schema\Table\ClickHouse as ClickHouseTable; +use Utopia\Query\Schema\Table\MongoDB as MongoTable; use Utopia\Query\Schema\Table\MySQL as MySQLTable; use Utopia\Query\Schema\Table\PostgreSQL as Table; +use Utopia\Query\Schema\Table\SQLite as SQLiteTable; /** * Behavioural tests for the fluent Schema builder. Covers: @@ -815,6 +818,48 @@ public function testMySQLColumnReturnsItselfForDialectScopedFluentMethods(): voi $this->assertSame($col, $col->collation('utf8mb4_bin')); } + /** + * srid, dimensions and autoIncrement are intrinsic column properties set by + * the factories, so the factories keep working on every dialect while the + * modifiers exist only where the value is emitted. + */ + public function testFactoriesStillSetIntrinsicsWhereModifiersAreAbsent(): void + { + // SQLite has no srid() modifier, but point() still records the SRID. + $sqlite = (new SQLiteTable())->point('g', 3857); + $this->assertSame(3857, $sqlite->srid); + $this->assertNotContains('srid', \get_class_methods($sqlite)); + + // ClickHouse has no autoIncrement() modifier, but id() still flags it. + $clickhouse = (new ClickHouseTable())->id(); + $this->assertTrue($clickhouse->isAutoIncrement); + $this->assertNotContains('autoIncrement', \get_class_methods($clickhouse)); + + // MongoDB has no autoIncrement() modifier, but serial() still flags it. + $mongo = (new MongoTable())->serial('s'); + $this->assertTrue($mongo->isAutoIncrement); + $this->assertNotContains('autoIncrement', \get_class_methods($mongo)); + } + + public function testIntrinsicModifiersAreScopedToDialectsThatEmitThem(): void + { + $mysql = \get_class_methods((new MySQLTable())->integer('a')); + $pg = \get_class_methods((new Table())->integer('a')); + $sqlite = \get_class_methods((new SQLiteTable())->integer('a')); + $clickhouse = \get_class_methods((new ClickHouseTable())->integer('a')); + + $this->assertContains('srid', $mysql); + $this->assertContains('srid', $pg); + $this->assertNotContains('srid', $sqlite); + + $this->assertContains('dimensions', $pg); + $this->assertNotContains('dimensions', $mysql); + $this->assertNotContains('dimensions', $clickhouse); + + $this->assertContains('autoIncrement', $sqlite); + $this->assertNotContains('autoIncrement', $clickhouse); + } + public function testColumnModifiersAreScopedToDialectsThatEmitThem(): void { $mysql = \get_class_methods((new MySQLTable())->string('s')); diff --git a/tests/Query/Schema/ForwarderTest.php b/tests/Query/Schema/ForwarderTest.php index 2c90497..814f8fe 100644 --- a/tests/Query/Schema/ForwarderTest.php +++ b/tests/Query/Schema/ForwarderTest.php @@ -180,9 +180,10 @@ public function testClickHouseColumnForwarderEngineOrderBySettingsPartition(): v $this->assertInstanceOf(Column\ClickHouse::class, $col); - $vector = $col->vector('embedding', 768); + $vector = $col->vector('embedding'); $this->assertInstanceOf(Column\ClickHouse::class, $vector); - $this->assertSame(768, $vector->dimensions); + // ClickHouse vectors are Array(Float64) and carry no declared width. + $this->assertNull($vector->dimensions); $this->assertSame($table, $col->engine(Engine::MergeTree)); $this->assertSame(Engine::MergeTree, $table->engine); @@ -219,9 +220,10 @@ public function testMongoDBColumnForwarderExposesVectorOnly(): void $this->assertInstanceOf(Column\MongoDB::class, $col); - $vector = $col->vector('embedding', 384); + $vector = $col->vector('embedding'); $this->assertInstanceOf(Column\MongoDB::class, $vector); - $this->assertSame(384, $vector->dimensions); + // MongoDB's array bsonType is unsized. + $this->assertNull($vector->dimensions); } public function testTableEntryPointReturnsDialectSpecificType(): void diff --git a/tests/Query/Schema/MongoDBTest.php b/tests/Query/Schema/MongoDBTest.php index 7e40b9c..145feb5 100644 --- a/tests/Query/Schema/MongoDBTest.php +++ b/tests/Query/Schema/MongoDBTest.php @@ -396,7 +396,7 @@ public function testCreateCollectionWithAllBsonTypes(): void ->linestring('path') ->polygon('area') ->addColumn('uid', ColumnType::Uuid7) - ->vector('embedding', 768) + ->vector('embedding') ->create(); $op = $this->decode($result->query);