Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,8 @@ $result = (new Builder())
// WHERE `status` IN (?) AND `tenant_id` IN (?)
```

> **SQL builders only.** `Hook\Filter` and `Hook\Join\Filter` return a `Condition` — a raw SQL expression plus bindings — so there is nowhere to put one in a MongoDB operation document. `MongoDB::addHook()` rejects them with `UnsupportedException` rather than accepting and dropping them; a silently ignored `Hook\Filter\Tenant` would leave every query unscoped. `Hook\Attribute` and `Hook\Write` are dialect-neutral and work everywhere. On MongoDB, express the constraint as a `Query` passed to `filter()`.

**Custom filter hooks** implement `Hook\Filter`:

```php
Expand Down Expand Up @@ -1983,20 +1985,26 @@ $result = $schema->table('users')

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`.

Column modifiers available on every dialect: `nullable()`, `default($value)`, `defaultRaw($expression)`, `unsigned()`, `unique()`, `primary()`, `autoIncrement()`, `after($column)`, `comment($text)`, `collation($collation)`, `srid($srid)` (spatial columns), `dimensions($dimensions)` (vector columns).
Column modifiers available on every dialect: `nullable()`, `default($value)`, `defaultRaw($expression)`, `primary()`, `unsigned()`, `autoIncrement()`, `srid($srid)` (spatial columns), `dimensions($dimensions)` (vector columns).

The rest are only on the dialects that honour them, so an unsupported combination is a type error rather than a runtime one:
The rest are only on the dialects that emit them, so an unsupported combination is a type error rather than something silently dropped:

| Modifier | Dialects |
|---|---|
| `check($expression)` | MySQL, MariaDB, PostgreSQL, SQLite |
| `generatedAs($expression)`, `stored()` | MySQL, MariaDB, PostgreSQL, SQLite |
| `virtual()` | MySQL, MariaDB, SQLite (PostgreSQL supports `STORED` only) |
| `ttl($expression)` | ClickHouse |
| `userType($name)` | PostgreSQL |
| Modifier | Dialects | Why not the others |
|---|---|---|
| `check($expression)` | MySQL, MariaDB, PostgreSQL, SQLite | ClickHouse and MongoDB have no CHECK |
| `generatedAs($expression)`, `stored()` | MySQL, MariaDB, PostgreSQL, SQLite | — |
| `virtual()` | MySQL, MariaDB, SQLite | PostgreSQL supports `STORED` only |
| `collation($collation)` | MySQL, MariaDB, PostgreSQL, SQLite | ClickHouse collates in `ORDER BY`; MongoDB per collection |
| `unique()` | MySQL, MariaDB, PostgreSQL, SQLite | ClickHouse enforces no uniqueness; MongoDB uses a unique index |
| `after($column)` | MySQL, MariaDB | PostgreSQL cannot order columns; SQLite's `ADD COLUMN` has no `AFTER` clause; MongoDB documents have no order |
| `comment($text)` | MySQL, MariaDB, SQLite, ClickHouse, MongoDB | PostgreSQL needs a separate statement — use `commentOnColumn()` |
| `ttl($expression)` | ClickHouse | — |
| `userType($name)` | PostgreSQL | — |

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.

**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.

**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`; MongoDB maps them to the BSON `int` type. ClickHouse has no sequence type, so `Table\ClickHouse` does not expose these factories at all:
Expand Down
27 changes: 27 additions & 0 deletions src/Query/Builder/MongoDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
use Utopia\Query\Builder\MongoDB\UpdateOperator;
use Utopia\Query\Exception\UnsupportedException;
use Utopia\Query\Exception\ValidationException;
use Utopia\Query\Hook;
use Utopia\Query\Hook\Filter;
use Utopia\Query\Hook\Join\Filter as JoinFilter;
use Utopia\Query\Method;
use Utopia\Query\Query;

Expand Down Expand Up @@ -221,6 +224,30 @@ public function reset(): static
return $this;
}

/**
* {@inheritDoc}
*
* Hook\Filter and Hook\Join\Filter return a {@see Condition}: a raw SQL
* expression plus bindings. A MongoDB operation document has nowhere to put
* one, so they are rejected rather than accepted and dropped -- silently
* ignoring a Hook\Filter\Tenant would leave every query unscoped.
*
* Hook\Attribute and Hook\Write are dialect-neutral and still apply.
*/
#[\Override]
public function addHook(Hook $hook): static
{
if ($hook instanceof Filter || $hook instanceof JoinFilter) {
throw new UnsupportedException(
'Filter hooks are not supported on the MongoDB builder: '
. Filter::class . ' returns a SQL expression, which has no place in an '
. 'operation document. Express the constraint as a Query passed to filter() instead.'
);
}

return parent::addHook($hook);
}

#[\Override]
public function build(): Statement
{
Expand Down
33 changes: 28 additions & 5 deletions src/Query/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,9 @@ public function compileCreate(Table $table, bool $ifNotExists = false): Statemen
$sql = 'CREATE TABLE ' . ($ifNotExists ? 'IF NOT EXISTS ' : '') . $this->quote($table->name)
. ' (' . \implode(', ', $columnDefs) . ')';

if ($this instanceof Schema\Feature\Partitioning) {
$partitioning = $this->compileCreatePartitioning($table);
if ($partitioning !== '') {
$sql .= ' ' . $partitioning;
}
$suffix = $this->compileCreateSuffix($table);
if ($suffix !== '') {
$sql .= ' ' . $suffix;
}

return new Statement($sql, [], executor: $this->executor);
Expand Down Expand Up @@ -273,6 +271,10 @@ protected function compileColumnDefinition(Column $column): string
$this->compileColumnType($column),
];

if ($column->collation !== null) {
$parts[] = 'COLLATE ' . $this->quoteCollation($column->collation);
}

if ($column->isUnsigned) {
$unsigned = $this->compileUnsigned();
if ($unsigned !== '') {
Expand Down Expand Up @@ -356,6 +358,27 @@ protected function compileDefaultValue(mixed $value): string
return "'" . \str_replace(['\\', "'"], ['\\\\', "''"], (string) $value) . "'";
}

/**
* Dialect-specific clauses appended after the closing paren of CREATE TABLE.
*
* Overridden by capability traits (e.g. Trait\Partitioning) rather than
* gated on the schema's own type, so the base class does not need to know
* which features exist.
*/
protected function compileCreateSuffix(Table $table): string
{
return '';
}

/**
* Render a column COLLATE name. MySQL and SQLite take a bare identifier;
* PostgreSQL requires a quoted one.
*/
protected function quoteCollation(string $collation): string
{
return $collation;
}

protected function compileUnsigned(): string
{
return 'UNSIGNED';
Expand Down
28 changes: 0 additions & 28 deletions src/Query/Schema/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,6 @@ public function unsigned(): static
return $this;
}

public function unique(): static
{
$this->isUnique = true;

return $this;
}

/**
* Mark this column as a primary key. Dialect Column subclasses that
* support composite primary keys also accept a list of column names to
Expand All @@ -142,34 +135,13 @@ public function primary(): static|Table
return $this;
}

public function after(string $column): static
{
$this->after = $column;

return $this;
}

public function autoIncrement(): static
{
$this->isAutoIncrement = true;

return $this;
}

public function comment(string $comment): static
{
$this->comment = $comment;

return $this;
}

public function collation(string $collation): static
{
$this->collation = $collation;

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
Expand Down
1 change: 1 addition & 0 deletions src/Query/Schema/Column/ClickHouse.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
class ClickHouse extends Column
{
use Trait\Comment;
use Forwarder\ClickHouse;

public protected(set) bool $isLowCardinality = false;
Expand Down
1 change: 1 addition & 0 deletions src/Query/Schema/Column/MongoDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
*/
class MongoDB extends Column
{
use Trait\Comment;
use Forwarder\MongoDB;
}
4 changes: 4 additions & 0 deletions src/Query/Schema/Column/MySQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
*/
class MySQL extends Column
{
use Trait\Collation;
use Trait\Positioning;
use Trait\Comment;
use Trait\Unique;
use Trait\Generated;
use Trait\VirtualGenerated;
use Forwarder\MySQL;
Expand Down
2 changes: 2 additions & 0 deletions src/Query/Schema/Column/PostgreSQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
class PostgreSQL extends Column
{
use Trait\Collation;
use Trait\Unique;
use Trait\Generated;
use Forwarder\PostgreSQL;

Expand Down
3 changes: 3 additions & 0 deletions src/Query/Schema/Column/SQLite.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
*/
class SQLite extends Column
{
use Trait\Collation;
use Trait\Comment;
use Trait\Unique;
use Trait\Generated;
use Trait\VirtualGenerated;
use Forwarder\SQLite;
Expand Down
18 changes: 18 additions & 0 deletions src/Query/Schema/Column/Trait/Collation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Utopia\Query\Schema\Column\Trait;

/**
* Column-level COLLATE. ClickHouse has no per-column collation (it applies
* collation in ORDER BY) and MongoDB sets it per collection, so neither
* exposes this.
*/
trait Collation
{
public function collation(string $collation): static
{
$this->collation = $collation;

return $this;
}
}
18 changes: 18 additions & 0 deletions src/Query/Schema/Column/Trait/Comment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Utopia\Query\Schema\Column\Trait;

/**
* Inline column comments. PostgreSQL cannot express one inside CREATE TABLE --
* it needs a separate COMMENT ON statement -- so use
* {@see \Utopia\Query\Schema\PostgreSQL::commentOnColumn()} there instead.
*/
trait Comment
{
public function comment(string $comment): static
{
$this->comment = $comment;

return $this;
}
}
21 changes: 21 additions & 0 deletions src/Query/Schema/Column/Trait/Positioning.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Utopia\Query\Schema\Column\Trait;

/**
* Positioning a column relative to another via AFTER, which only MySQL and
* MariaDB accept in ALTER TABLE.
*
* PostgreSQL cannot control column order and MongoDB documents have none.
* SQLite's ALTER TABLE ADD COLUMN has no AFTER clause -- emitting one is a
* syntax error -- and ClickHouse supports it but the compiler does not emit it.
*/
trait Positioning
{
public function after(string $column): static
{
$this->after = $column;

return $this;
}
}
17 changes: 17 additions & 0 deletions src/Query/Schema/Column/Trait/Unique.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Utopia\Query\Schema\Column\Trait;

/**
* Column-level UNIQUE. ClickHouse enforces no uniqueness constraints, and
* MongoDB expresses it as a unique index rather than in the validator.
*/
trait Unique
{
public function unique(): static
{
$this->isUnique = true;

return $this;
}
}
10 changes: 10 additions & 0 deletions src/Query/Schema/PostgreSQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ protected function compileAutoIncrement(): string
return 'GENERATED BY DEFAULT AS IDENTITY';
}

#[\Override]
protected function quoteCollation(string $collation): string
{
return $this->quote($collation);
}

protected function compileUnsigned(): string
{
return '';
Expand All @@ -103,6 +109,10 @@ protected function compileColumnDefinition(Column $column): string
$this->compileColumnType($column),
];

if ($column->collation !== null) {
$parts[] = 'COLLATE ' . $this->quoteCollation($column->collation);
}

if ($column->isUnsigned) {
$unsigned = $this->compileUnsigned();
if ($unsigned !== '') {
Expand Down
6 changes: 6 additions & 0 deletions src/Query/Schema/Trait/Partitioning.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

trait Partitioning
{
#[\Override]
protected function compileCreateSuffix(Table $table): string
{
return $this->compileCreatePartitioning($table);
}

public function compileCreatePartitioning(Table $table): string
{
if ($table->partitionType === null) {
Expand Down
15 changes: 15 additions & 0 deletions tests/Integration/Schema/MySQLIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ public function testCreateTableWithBasicColumns(): void
$this->assertSame('100', (string) $nameCol['CHARACTER_MAXIMUM_LENGTH']); // @phpstan-ignore cast.string
}

public function testColumnCollationIsAppliedByTheServer(): void
{
$table = 'test_collation_' . uniqid();
$this->trackMysqlTable($table);

$result = $this->schema->table($table)
->string('name', 100)->collation('utf8mb4_bin')
->create();

$this->mysqlStatement($result->query);

$nameCol = $this->findColumn($this->fetchMysqlColumns($table), 'name');
$this->assertSame('utf8mb4_bin', $nameCol['COLLATION_NAME']);
}

public function testCreateTableWithPrimaryKeyAndUnique(): void
{
$table = 'test_pk_uniq_' . uniqid();
Expand Down
15 changes: 15 additions & 0 deletions tests/Integration/Schema/PostgreSQLIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ public function testCreateTableWithBasicColumns(): void
$this->assertSame('100', (string) $nameCol['character_maximum_length']); // @phpstan-ignore cast.string
}

public function testColumnCollationIsAppliedByTheServer(): void
{
$table = 'test_collation_' . uniqid();
$this->trackPostgresTable($table);

$result = $this->schema->table($table)
->string('name', 100)->collation('C')
->create();

$this->postgresStatement($result->query);

$nameCol = $this->findColumn($this->fetchPostgresColumns($table), 'name');
$this->assertSame('C', $nameCol['collation_name']);
}

public function testCreateTableWithIdentityColumn(): void
{
$table = 'test_identity_' . uniqid();
Expand Down
Loading
Loading