diff --git a/packages/database/src/Config/DatabaseDialect.php b/packages/database/src/Config/DatabaseDialect.php index 4c468dfe5..743047c03 100644 --- a/packages/database/src/Config/DatabaseDialect.php +++ b/packages/database/src/Config/DatabaseDialect.php @@ -21,6 +21,14 @@ public function tableNotFoundCode(): string }; } + public function quoteIdentifier(string $identifier): string + { + return match ($this) { + self::MYSQL, self::SQLITE => sprintf('`%s`', $identifier), + self::POSTGRESQL => sprintf('"%s"', $identifier), + }; + } + public function isTableNotFoundError(QueryWasInvalid $queryWasInvalid): bool { $pdoException = $queryWasInvalid->pdoException; diff --git a/packages/database/src/Query.php b/packages/database/src/Query.php index 958ee743d..6cf14d7df 100644 --- a/packages/database/src/Query.php +++ b/packages/database/src/Query.php @@ -76,7 +76,7 @@ public function compile(): ImmutableString } if ($dialect === DatabaseDialect::POSTGRESQL) { - $sql = str_replace('`', '', $sql); + $sql = str_replace('`', '"', $sql); } return new ImmutableString($sql); diff --git a/packages/database/src/QueryStatements/AlterTableStatement.php b/packages/database/src/QueryStatements/AlterTableStatement.php index d1d7530c0..7250ee754 100644 --- a/packages/database/src/QueryStatements/AlterTableStatement.php +++ b/packages/database/src/QueryStatements/AlterTableStatement.php @@ -4,7 +4,6 @@ namespace Tempest\Database\QueryStatements; -use Tempest\Database\Builder\TableDefinition; use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\HasTrailingStatements; use Tempest\Database\QueryStatement; @@ -92,7 +91,7 @@ public function compile(DatabaseDialect $dialect): string if ($this->statements !== []) { return sprintf( 'ALTER TABLE %s %s;', - new TableDefinition($this->tableName), + $dialect->quoteIdentifier($this->tableName), arr($this->statements) ->map(fn (QueryStatement $queryStatement) => str($queryStatement->compile($dialect))->trim()->replace(' ', ' ')) ->filter(fn (ImmutableString $line) => $line->isNotEmpty()) diff --git a/packages/database/src/QueryStatements/BooleanStatement.php b/packages/database/src/QueryStatements/BooleanStatement.php index 52c624088..6bb9d7984 100644 --- a/packages/database/src/QueryStatements/BooleanStatement.php +++ b/packages/database/src/QueryStatements/BooleanStatement.php @@ -27,8 +27,8 @@ public function compile(DatabaseDialect $dialect): string } return sprintf( - '`%s` BOOLEAN %s %s', - $this->name, + '%s BOOLEAN %s %s', + $dialect->quoteIdentifier($this->name), $default !== null ? "DEFAULT {$default}" : '', $this->nullable ? '' : 'NOT NULL', ); diff --git a/packages/database/src/QueryStatements/CharStatement.php b/packages/database/src/QueryStatements/CharStatement.php index 8e1fe53db..56a5d75af 100644 --- a/packages/database/src/QueryStatements/CharStatement.php +++ b/packages/database/src/QueryStatements/CharStatement.php @@ -18,8 +18,8 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { return sprintf( - '`%s` CHAR %s %s', - $this->name, + '%s CHAR %s %s', + $dialect->quoteIdentifier($this->name), $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', ); diff --git a/packages/database/src/QueryStatements/CountStatement.php b/packages/database/src/QueryStatements/CountStatement.php index 353d1f354..b41756191 100644 --- a/packages/database/src/QueryStatements/CountStatement.php +++ b/packages/database/src/QueryStatements/CountStatement.php @@ -33,7 +33,7 @@ public function compile(DatabaseDialect $dialect): string $query = arr([ sprintf('SELECT %s', $countField->compile($dialect)), - sprintf('FROM `%s`', $this->table->name), + sprintf('FROM %s', $dialect->quoteIdentifier($this->table->name)), ]); if ($this->joins->isNotEmpty()) { diff --git a/packages/database/src/QueryStatements/CreateTableStatement.php b/packages/database/src/QueryStatements/CreateTableStatement.php index 5b9a1fb34..81e66153c 100644 --- a/packages/database/src/QueryStatements/CreateTableStatement.php +++ b/packages/database/src/QueryStatements/CreateTableStatement.php @@ -5,7 +5,6 @@ namespace Tempest\Database\QueryStatements; use BackedEnum; -use Tempest\Database\Builder\TableDefinition; use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Enums\DatabaseTextLength; use Tempest\Database\HasTrailingStatements; @@ -400,7 +399,7 @@ public function compile(DatabaseDialect $dialect): string { return sprintf( 'CREATE TABLE %s (%s);', - new TableDefinition($this->tableName), + $dialect->quoteIdentifier($this->tableName), arr($this->statements) // Remove BelongsTo for sqlLite as it does not support those queries ->filter(fn (QueryStatement $queryStatement) => ! ($dialect === DatabaseDialect::SQLITE && $queryStatement instanceof BelongsToStatement)) diff --git a/packages/database/src/QueryStatements/DateStatement.php b/packages/database/src/QueryStatements/DateStatement.php index d3f6bdf78..37cc37966 100644 --- a/packages/database/src/QueryStatements/DateStatement.php +++ b/packages/database/src/QueryStatements/DateStatement.php @@ -18,8 +18,8 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { return sprintf( - '`%s` DATE %s %s', - $this->name, + '%s DATE %s %s', + $dialect->quoteIdentifier($this->name), $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', ); diff --git a/packages/database/src/QueryStatements/DatetimeStatement.php b/packages/database/src/QueryStatements/DatetimeStatement.php index c4aa33997..6acc42345 100644 --- a/packages/database/src/QueryStatements/DatetimeStatement.php +++ b/packages/database/src/QueryStatements/DatetimeStatement.php @@ -29,16 +29,18 @@ public function compile(DatabaseDialect $dialect): string default => null, }; + $name = $dialect->quoteIdentifier($this->name); + return match ($dialect) { DatabaseDialect::POSTGRESQL => sprintf( - '`%s` TIMESTAMP %s %s', - $this->name, + '%s TIMESTAMP %s %s', + $name, $default !== null ? "DEFAULT {$default}" : '', $this->nullable ? '' : 'NOT NULL', ), default => sprintf( - '`%s` DATETIME %s %s', - $this->name, + '%s DATETIME %s %s', + $name, $default !== null ? "DEFAULT {$default}" : '', $this->nullable ? '' : 'NOT NULL', ), diff --git a/packages/database/src/QueryStatements/DeleteStatement.php b/packages/database/src/QueryStatements/DeleteStatement.php index a8a880a5d..f7e88c2f3 100644 --- a/packages/database/src/QueryStatements/DeleteStatement.php +++ b/packages/database/src/QueryStatements/DeleteStatement.php @@ -25,7 +25,7 @@ public function compile(DatabaseDialect $dialect): string } $query = arr([ - sprintf('DELETE FROM `%s`', $this->table->name), + sprintf('DELETE FROM %s', $dialect->quoteIdentifier($this->table->name)), ]); if ($this->where->isNotEmpty()) { diff --git a/packages/database/src/QueryStatements/DropConstraintStatement.php b/packages/database/src/QueryStatements/DropConstraintStatement.php index ffe7da51a..85d3e5431 100644 --- a/packages/database/src/QueryStatements/DropConstraintStatement.php +++ b/packages/database/src/QueryStatements/DropConstraintStatement.php @@ -26,8 +26,8 @@ public function compile(DatabaseDialect $dialect): string return match ($dialect) { DatabaseDialect::MYSQL => sprintf( - 'ALTER TABLE `%s` DROP CONSTRAINT %s', - $foreignTable, + 'ALTER TABLE %s DROP CONSTRAINT %s', + $dialect->quoteIdentifier($foreignTable), $constraintName, ), default => '', diff --git a/packages/database/src/QueryStatements/DropTableStatement.php b/packages/database/src/QueryStatements/DropTableStatement.php index a836b1744..259b374cd 100644 --- a/packages/database/src/QueryStatements/DropTableStatement.php +++ b/packages/database/src/QueryStatements/DropTableStatement.php @@ -36,8 +36,8 @@ public static function forModel(string $modelClass): self public function compile(DatabaseDialect $dialect): string { return match ($dialect) { - DatabaseDialect::POSTGRESQL => sprintf('DROP TABLE IF EXISTS `%s` CASCADE', $this->tableName), - default => sprintf('DROP TABLE IF EXISTS `%s`', $this->tableName), + DatabaseDialect::POSTGRESQL => sprintf('DROP TABLE IF EXISTS %s CASCADE', $dialect->quoteIdentifier($this->tableName)), + default => sprintf('DROP TABLE IF EXISTS %s', $dialect->quoteIdentifier($this->tableName)), }; } } diff --git a/packages/database/src/QueryStatements/EnumStatement.php b/packages/database/src/QueryStatements/EnumStatement.php index 26ba7f5eb..afcbd27c1 100644 --- a/packages/database/src/QueryStatements/EnumStatement.php +++ b/packages/database/src/QueryStatements/EnumStatement.php @@ -35,23 +35,25 @@ public function compile(DatabaseDialect $dialect): string $defaultValue = null; } + $name = $dialect->quoteIdentifier($this->name); + return match ($dialect) { DatabaseDialect::MYSQL => sprintf( - '`%s` ENUM(%s) %s %s', - $this->name, + '%s ENUM(%s) %s %s', + $name, $cases->implode(', '), $defaultValue !== null ? "DEFAULT '{$defaultValue}'" : '', $this->nullable ? '' : 'NOT NULL', ), DatabaseDialect::SQLITE => sprintf( - '`%s` TEXT %s %s', - $this->name, + '%s TEXT %s %s', + $name, $defaultValue !== null ? "DEFAULT '{$defaultValue}'" : '', $this->nullable ? '' : 'NOT NULL', ), DatabaseDialect::POSTGRESQL => sprintf( - '"%s" "%s" %s %s', - $this->name, + '%s "%s" %s %s', + $name, str($this->enumClass)->replace('\\\\', '_'), $defaultValue !== null ? "DEFAULT ('{$defaultValue}')" : '', $this->nullable ? '' : 'NOT NULL', diff --git a/packages/database/src/QueryStatements/FieldStatement.php b/packages/database/src/QueryStatements/FieldStatement.php index 26dd22cfd..bad8067f2 100644 --- a/packages/database/src/QueryStatements/FieldStatement.php +++ b/packages/database/src/QueryStatements/FieldStatement.php @@ -29,27 +29,18 @@ public function compile(DatabaseDialect $dialect): string $aliasPrefix = $this->aliasPrefix ? "{$this->aliasPrefix}." : ''; if ($this->alias === true) { - $alias = sprintf( - '`%s%s`', - $aliasPrefix, - str_replace('`', '', $field), - ); + $alias = $aliasPrefix . str_replace('`', '', $field); } elseif ($this->alias) { - $alias = sprintf( - '`%s%s`', - $aliasPrefix, - $this->alias, - ); + $alias = $aliasPrefix . $this->alias; } } else { - $alias = $parts[1]; + $alias = trim($parts[1], '`" '); } $field = arr(explode('.', $field)) - ->map(fn (string $part) => trim($part, '` ')) + ->map(fn (string $part) => trim($part, '`" ')) ->map( function (string $part) use ($dialect) { - // Function calls are never wrapped in backticks. if (str_contains($part, '(')) { return $part; } @@ -58,7 +49,7 @@ function (string $part) use ($dialect) { return $part; } - return sprintf('`%s`', $part); + return $dialect->quoteIdentifier($part); }, ) ->implode('.'); @@ -67,10 +58,7 @@ function (string $part) use ($dialect) { return $field; } - return match ($dialect) { - DatabaseDialect::POSTGRESQL => sprintf('%s AS "%s"', $field, trim($alias, '`')), - default => sprintf('%s AS `%s`', $field, trim($alias, '`')), - }; + return sprintf('%s AS %s', $field, $dialect->quoteIdentifier($alias)); } public function withAliasPrefix(?string $prefix = null): self diff --git a/packages/database/src/QueryStatements/FloatStatement.php b/packages/database/src/QueryStatements/FloatStatement.php index 02ba890dd..d09f6b537 100644 --- a/packages/database/src/QueryStatements/FloatStatement.php +++ b/packages/database/src/QueryStatements/FloatStatement.php @@ -18,8 +18,8 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { return sprintf( - '`%s` FLOAT %s %s', - $this->name, + '%s FLOAT %s %s', + $dialect->quoteIdentifier($this->name), $this->default !== null ? "DEFAULT {$this->default}" : '', $this->nullable ? '' : 'NOT NULL', ); diff --git a/packages/database/src/QueryStatements/IdentityStatement.php b/packages/database/src/QueryStatements/IdentityStatement.php index 5f2d7e38c..c1cdb44fc 100644 --- a/packages/database/src/QueryStatements/IdentityStatement.php +++ b/packages/database/src/QueryStatements/IdentityStatement.php @@ -15,6 +15,6 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { - return sprintf('`%s`', $this->name); + return $dialect->quoteIdentifier($this->name); } } diff --git a/packages/database/src/QueryStatements/IndexStatement.php b/packages/database/src/QueryStatements/IndexStatement.php index 2d2685e72..c2cdf97f6 100644 --- a/packages/database/src/QueryStatements/IndexStatement.php +++ b/packages/database/src/QueryStatements/IndexStatement.php @@ -19,12 +19,15 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { - $columns = arr($this->columns)->implode('`, `')->wrap('`', '`'); + $columns = arr($this->columns) + ->map($dialect->quoteIdentifier(...)) + ->implode(', '); - $indexName = str($this->tableName . ' ' . $columns->replace(',', '')->snake())->snake()->toString(); + $rawColumns = arr($this->columns)->implode('_'); + $indexName = str($this->tableName . '_' . $rawColumns)->snake()->toString(); - $on = sprintf('`%s` (%s)', $this->tableName, $columns); + $on = sprintf('%s (%s)', $dialect->quoteIdentifier($this->tableName), $columns); - return sprintf('CREATE INDEX `%s` ON %s', $indexName, $on); + return sprintf('CREATE INDEX %s ON %s', $dialect->quoteIdentifier($indexName), $on); } } diff --git a/packages/database/src/QueryStatements/InsertStatement.php b/packages/database/src/QueryStatements/InsertStatement.php index 8b95d0c6f..899d1be62 100644 --- a/packages/database/src/QueryStatements/InsertStatement.php +++ b/packages/database/src/QueryStatements/InsertStatement.php @@ -55,7 +55,7 @@ public function compile(DatabaseDialect $dialect): string $sql = sprintf( 'INSERT INTO %s (%s) VALUES %s', $this->table, - $columns->map(fn (string $column) => "`{$column}`")->implode(', '), + $columns->map($dialect->quoteIdentifier(...))->implode(', '), $entryPlaceholders, ); } diff --git a/packages/database/src/QueryStatements/IntegerStatement.php b/packages/database/src/QueryStatements/IntegerStatement.php index 873cf9228..9a274ee29 100644 --- a/packages/database/src/QueryStatements/IntegerStatement.php +++ b/packages/database/src/QueryStatements/IntegerStatement.php @@ -19,17 +19,19 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { + $name = $dialect->quoteIdentifier($this->name); + return match ($dialect) { DatabaseDialect::SQLITE => sprintf( - '`%s` INTEGER %s %s %s', - $this->name, + '%s INTEGER %s %s %s', + $name, $this->unsigned ? 'UNSIGNED' : '', $this->default !== null ? "DEFAULT {$this->default}" : '', $this->nullable ? '' : 'NOT NULL', ), default => sprintf( - '`%s` %s %s %s %s', - $this->name, + '%s %s %s %s %s', + $name, is_int($this->size) ? DatabaseIntegerSize::fromBytes($this->size)->toString() : $this->size->toString(), $this->unsigned ? 'UNSIGNED' : '', $this->default !== null ? "DEFAULT {$this->default}" : '', diff --git a/packages/database/src/QueryStatements/JsonStatement.php b/packages/database/src/QueryStatements/JsonStatement.php index f1238f22f..2abfc6a6c 100644 --- a/packages/database/src/QueryStatements/JsonStatement.php +++ b/packages/database/src/QueryStatements/JsonStatement.php @@ -22,21 +22,23 @@ public function compile(DatabaseDialect $dialect): string throw new DefaultValueWasInvalid($this->name, $this->default); } + $name = $dialect->quoteIdentifier($this->name); + return match ($dialect) { DatabaseDialect::MYSQL => sprintf( - '`%s` JSON %s', - $this->name, + '%s JSON %s', + $name, $this->nullable ? '' : 'NOT NULL', ), DatabaseDialect::SQLITE => sprintf( - '`%s` TEXT %s %s', - $this->name, + '%s TEXT %s %s', + $name, $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', ), DatabaseDialect::POSTGRESQL => sprintf( - '`%s` JSONB %s %s', - $this->name, + '%s JSONB %s %s', + $name, $this->default !== null ? "DEFAULT ('{$this->default}')" : '', $this->nullable ? '' : 'NOT NULL', ), diff --git a/packages/database/src/QueryStatements/PrimaryKeyStatement.php b/packages/database/src/QueryStatements/PrimaryKeyStatement.php index 07153a071..528cdc6fa 100644 --- a/packages/database/src/QueryStatements/PrimaryKeyStatement.php +++ b/packages/database/src/QueryStatements/PrimaryKeyStatement.php @@ -15,10 +15,12 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { + $name = $dialect->quoteIdentifier($this->name); + return match ($dialect) { - DatabaseDialect::MYSQL => sprintf('`%s` INTEGER PRIMARY KEY AUTO_INCREMENT', $this->name), - DatabaseDialect::POSTGRESQL => sprintf('`%s` SERIAL PRIMARY KEY', $this->name), - DatabaseDialect::SQLITE => sprintf('`%s` INTEGER PRIMARY KEY AUTOINCREMENT', $this->name), + DatabaseDialect::MYSQL => "{$name} INTEGER PRIMARY KEY AUTO_INCREMENT", + DatabaseDialect::POSTGRESQL => "{$name} SERIAL PRIMARY KEY", + DatabaseDialect::SQLITE => "{$name} INTEGER PRIMARY KEY AUTOINCREMENT", }; } } diff --git a/packages/database/src/QueryStatements/SetStatement.php b/packages/database/src/QueryStatements/SetStatement.php index 934f3101b..d33a8c699 100644 --- a/packages/database/src/QueryStatements/SetStatement.php +++ b/packages/database/src/QueryStatements/SetStatement.php @@ -27,8 +27,8 @@ public function compile(DatabaseDialect $dialect): string return match ($dialect) { DatabaseDialect::MYSQL => sprintf( - '`%s` SET (%s) %s %s', - $this->name, + '%s SET (%s) %s %s', + $dialect->quoteIdentifier($this->name), "'" . implode("', '", $this->values) . "'", $this->default ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', diff --git a/packages/database/src/QueryStatements/TextStatement.php b/packages/database/src/QueryStatements/TextStatement.php index 88c544158..a6c581686 100644 --- a/packages/database/src/QueryStatements/TextStatement.php +++ b/packages/database/src/QueryStatements/TextStatement.php @@ -19,16 +19,18 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { + $name = $dialect->quoteIdentifier($this->name); + return match ($dialect) { DatabaseDialect::MYSQL => sprintf( - '`%s` %s %s', - $this->name, + '%s %s %s', + $name, $this->getSQLTypeDeclaration($this->length), $this->nullable ? '' : 'NOT NULL', ), default => sprintf( - '`%s` TEXT %s %s', - $this->name, + '%s TEXT %s %s', + $name, $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', ), diff --git a/packages/database/src/QueryStatements/UniqueStatement.php b/packages/database/src/QueryStatements/UniqueStatement.php index 90526fee1..40f5dad61 100644 --- a/packages/database/src/QueryStatements/UniqueStatement.php +++ b/packages/database/src/QueryStatements/UniqueStatement.php @@ -19,12 +19,15 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { - $columns = arr($this->columns)->implode('`, `')->wrap('`', '`'); + $columns = arr($this->columns) + ->map($dialect->quoteIdentifier(...)) + ->implode(', '); - $indexName = str($this->tableName . ' ' . $columns->replace(',', '')->snake())->snake()->toString(); + $rawColumns = arr($this->columns)->implode('_'); + $indexName = str($this->tableName . '_' . $rawColumns)->snake()->toString(); - $on = sprintf('`%s` (%s)', $this->tableName, $columns); + $on = sprintf('%s (%s)', $dialect->quoteIdentifier($this->tableName), $columns); - return sprintf('CREATE UNIQUE INDEX `%s` ON %s', $indexName, $on); + return sprintf('CREATE UNIQUE INDEX %s ON %s', $dialect->quoteIdentifier($indexName), $on); } } diff --git a/packages/database/src/QueryStatements/UpdateStatement.php b/packages/database/src/QueryStatements/UpdateStatement.php index 0a46b886d..7ed6814d7 100644 --- a/packages/database/src/QueryStatements/UpdateStatement.php +++ b/packages/database/src/QueryStatements/UpdateStatement.php @@ -27,7 +27,7 @@ public function compile(DatabaseDialect $dialect): string } $query = arr([ - sprintf('UPDATE `%s`', $this->table->name), + sprintf('UPDATE %s', $dialect->quoteIdentifier($this->table->name)), ]); if ($this->values->isEmpty()) { @@ -35,7 +35,7 @@ public function compile(DatabaseDialect $dialect): string } $query[] = 'SET ' . $this->values - ->map(fn (mixed $_, mixed $key) => "`{$key}` = ?") + ->map(fn (mixed $_, mixed $key) => $dialect->quoteIdentifier($key) . ' = ?') ->implode(', '); if ($this->where->isNotEmpty()) { diff --git a/packages/database/src/QueryStatements/UuidPrimaryKeyStatement.php b/packages/database/src/QueryStatements/UuidPrimaryKeyStatement.php index 9ed2e5bab..425aa9d20 100644 --- a/packages/database/src/QueryStatements/UuidPrimaryKeyStatement.php +++ b/packages/database/src/QueryStatements/UuidPrimaryKeyStatement.php @@ -15,10 +15,12 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { + $name = $dialect->quoteIdentifier($this->name); + return match ($dialect) { - DatabaseDialect::MYSQL => sprintf('`%s` CHAR(36) PRIMARY KEY', $this->name), - DatabaseDialect::POSTGRESQL => sprintf('`%s` UUID PRIMARY KEY', $this->name), - DatabaseDialect::SQLITE => sprintf('`%s` TEXT PRIMARY KEY', $this->name), + DatabaseDialect::MYSQL => "{$name} CHAR(36) PRIMARY KEY", + DatabaseDialect::POSTGRESQL => "{$name} UUID PRIMARY KEY", + DatabaseDialect::SQLITE => "{$name} TEXT PRIMARY KEY", }; } } diff --git a/packages/database/src/QueryStatements/VarcharStatement.php b/packages/database/src/QueryStatements/VarcharStatement.php index 4f89e43c5..5356fee95 100644 --- a/packages/database/src/QueryStatements/VarcharStatement.php +++ b/packages/database/src/QueryStatements/VarcharStatement.php @@ -19,8 +19,8 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { return sprintf( - '`%s` VARCHAR(%s) %s %s', - $this->name, + '%s VARCHAR(%s) %s %s', + $dialect->quoteIdentifier($this->name), $this->size, $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', diff --git a/packages/database/tests/QueryCompileTest.php b/packages/database/tests/QueryCompileTest.php new file mode 100644 index 000000000..3f77d55af --- /dev/null +++ b/packages/database/tests/QueryCompileTest.php @@ -0,0 +1,177 @@ +singleton( + className: Database::class, + definition: $database, + ); + GenericContainer::setInstance(instance: $container); + } + + private function createDatabaseWithPostgresDialect(): void + { + $config = new PostgresConfig(); + $connection = new PDOConnection(config: $config); + + $database = new GenericDatabase( + connection: $connection, + transactionManager: new GenericTransactionManager(connection: $connection), + serializerFactory: new SerializerFactory(container: new GenericContainer()), + eventBus: new GenericEventBus( + container: new GenericContainer(), + eventBusConfig: new EventBusConfig(), + ), + ); + + $container = new GenericContainer(); + $container->singleton( + className: Database::class, + definition: $database, + ); + GenericContainer::setInstance(instance: $container); + } + + private function createDatabaseWithSqliteDialect(): void + { + $config = new SQLiteConfig(path: ':memory:'); + $connection = new PDOConnection(config: $config); + + $database = new GenericDatabase( + connection: $connection, + transactionManager: new GenericTransactionManager(connection: $connection), + serializerFactory: new SerializerFactory(container: new GenericContainer()), + eventBus: new GenericEventBus( + container: new GenericContainer(), + eventBusConfig: new EventBusConfig(), + ), + ); + + $container = new GenericContainer(); + $container->singleton( + className: Database::class, + definition: $database, + ); + GenericContainer::setInstance(instance: $container); + } + + #[Test] + public function postgresql_converts_backticks_to_double_quotes(): void + { + $this->createDatabaseWithPostgresDialect(); + + $query = new Query(sql: 'SELECT `name`, `email` FROM `users` WHERE `id` = ?'); + + $this->assertSame( + 'SELECT "name", "email" FROM "users" WHERE "id" = ?', + $query->compile()->toString(), + ); + } + + #[Test] + public function postgresql_handles_qualified_identifiers(): void + { + $this->createDatabaseWithPostgresDialect(); + + $query = new Query(sql: 'SELECT `users`.`name` FROM `users`'); + + $this->assertSame( + 'SELECT "users"."name" FROM "users"', + $query->compile()->toString(), + ); + } + + #[Test] + public function postgresql_handles_reserved_words(): void + { + $this->createDatabaseWithPostgresDialect(); + + $query = new Query(sql: 'SELECT `order`, `user`, `type` FROM `group`'); + + $this->assertSame( + 'SELECT "order", "user", "type" FROM "group"', + $query->compile()->toString(), + ); + } + + #[Test] + public function postgresql_passes_through_sql_without_backticks(): void + { + $this->createDatabaseWithPostgresDialect(); + + $query = new Query(sql: 'SELECT 1'); + + $this->assertSame( + 'SELECT 1', + $query->compile()->toString(), + ); + } + + #[Test] + public function mysql_retains_backticks(): void + { + $this->createDatabaseWithMysqlDialect(); + + $query = new Query(sql: 'SELECT `name` FROM `users` WHERE `id` = ?'); + + $this->assertSame( + 'SELECT `name` FROM `users` WHERE `id` = ?', + $query->compile()->toString(), + ); + } + + #[Test] + public function sqlite_retains_backticks(): void + { + $this->createDatabaseWithSqliteDialect(); + + $query = new Query(sql: 'SELECT `name` FROM `users` WHERE `id` = ?'); + + $this->assertSame( + 'SELECT `name` FROM `users` WHERE `id` = ?', + $query->compile()->toString(), + ); + } +} diff --git a/packages/database/tests/QueryStatements/AlterTableStatementTest.php b/packages/database/tests/QueryStatements/AlterTableStatementTest.php index e06f14e6f..0eea1847e 100644 --- a/packages/database/tests/QueryStatements/AlterTableStatementTest.php +++ b/packages/database/tests/QueryStatements/AlterTableStatementTest.php @@ -26,8 +26,16 @@ public function test_alter_for_only_indexes(DatabaseDialect $dialect): void ->index('foo') ->unique('bar'); - $this->assertEqualsIgnoringCase('CREATE INDEX `table_foo` ON `table` (`foo`)', $alterStatement->trailingStatements[0]->compile($dialect)); - $this->assertEqualsIgnoringCase('CREATE UNIQUE INDEX `table_bar` ON `table` (`bar`)', $alterStatement->trailingStatements[1]->compile($dialect)); + $q = $dialect->quoteIdentifier(...); + + $this->assertEqualsIgnoringCase( + 'CREATE INDEX ' . $q('table_foo') . ' ON ' . $q('table') . ' (' . $q('foo') . ')', + $alterStatement->trailingStatements[0]->compile($dialect), + ); + $this->assertEqualsIgnoringCase( + 'CREATE UNIQUE INDEX ' . $q('table_bar') . ' ON ' . $q('table') . ' (' . $q('bar') . ')', + $alterStatement->trailingStatements[1]->compile($dialect), + ); } #[TestWith([DatabaseDialect::MYSQL])] @@ -35,7 +43,8 @@ public function test_alter_for_only_indexes(DatabaseDialect $dialect): void #[TestWith([DatabaseDialect::SQLITE])] public function test_alter_add_column(DatabaseDialect $dialect): void { - $expected = "ALTER TABLE `table` ADD `bar` VARCHAR(42) DEFAULT 'xx' ;"; + $q = $dialect->quoteIdentifier(...); + $expected = 'ALTER TABLE ' . $q('table') . ' ADD ' . $q('bar') . " VARCHAR(42) DEFAULT 'xx' ;"; $statement = new AlterTableStatement('table') ->add(new VarcharStatement('bar', 42, true, 'xx')) ->compile($dialect); @@ -61,7 +70,7 @@ public function test_alter_add_belongs_to_mysql(DatabaseDialect $dialect): void #[TestWith([DatabaseDialect::POSTGRESQL])] public function test_alter_add_belongs_to_postgresql(DatabaseDialect $dialect): void { - $expected = 'ALTER TABLE `table` ADD CONSTRAINT `fk_parent_table_foo` FOREIGN KEY(foo) REFERENCES parent(bar) ON DELETE RESTRICT ON UPDATE NO ACTION ;'; + $expected = 'ALTER TABLE "table" ADD CONSTRAINT "fk_parent_table_foo" FOREIGN KEY(foo) REFERENCES parent(bar) ON DELETE RESTRICT ON UPDATE NO ACTION ;'; $statement = new AlterTableStatement('table') ->add(new BelongsToStatement('table.foo', 'parent.bar')) ->compile($dialect); @@ -86,7 +95,8 @@ public function test_alter_add_belongs_to_unsupported(DatabaseDialect $dialect): #[TestWith([DatabaseDialect::SQLITE])] public function test_alter_table_drop_column(DatabaseDialect $dialect): void { - $expected = 'ALTER TABLE `table` DROP COLUMN `foo` ;'; + $q = $dialect->quoteIdentifier(...); + $expected = 'ALTER TABLE ' . $q('table') . ' DROP COLUMN ' . $q('foo') . ' ;'; $statement = new AlterTableStatement('table') ->dropColumn('foo') ->compile($dialect); @@ -97,7 +107,7 @@ public function test_alter_table_drop_column(DatabaseDialect $dialect): void } #[TestWith([DatabaseDialect::MYSQL, 'ALTER TABLE `table` DROP CONSTRAINT `foo` ;'])] - #[TestWith([DatabaseDialect::POSTGRESQL, 'ALTER TABLE `table` DROP CONSTRAINT `foo` ;'])] + #[TestWith([DatabaseDialect::POSTGRESQL, 'ALTER TABLE "table" DROP CONSTRAINT "foo" ;'])] public function test_alter_table_drop_constraint(DatabaseDialect $dialect, string $expected): void { $statement = new AlterTableStatement('table') @@ -121,7 +131,7 @@ public function test_alter_table_drop_constraint_unsupported_dialects(DatabaseDi #[TestWith([DatabaseDialect::MYSQL, "ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT 'bar' NOT NULL ;"])] #[TestWith([ DatabaseDialect::POSTGRESQL, - "ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT 'bar' NOT NULL ;", + "ALTER TABLE \"table\" ADD \"foo\" VARCHAR(42) DEFAULT 'bar' NOT NULL ;", ])] #[TestWith([DatabaseDialect::SQLITE, "ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT 'bar' NOT NULL ;"])] public function test_alter_table_add_column(DatabaseDialect $dialect, string $expected): void @@ -140,7 +150,8 @@ public function test_alter_table_add_column(DatabaseDialect $dialect, string $ex #[TestWith([DatabaseDialect::SQLITE])] public function test_alter_table_rename_column(DatabaseDialect $dialect): void { - $expected = 'ALTER TABLE `table` RENAME COLUMN `foo` TO `bar` ;'; + $q = $dialect->quoteIdentifier(...); + $expected = 'ALTER TABLE ' . $q('table') . ' RENAME COLUMN ' . $q('foo') . ' TO ' . $q('bar') . ' ;'; $statement = new AlterTableStatement('table') ->rename('foo', 'bar') ->compile($dialect); @@ -151,7 +162,7 @@ public function test_alter_table_rename_column(DatabaseDialect $dialect): void } #[TestWith([DatabaseDialect::MYSQL, "ALTER TABLE `table` MODIFY COLUMN `foo` VARCHAR(42) DEFAULT 'bar' NOT NULL ;"])] - #[TestWith([DatabaseDialect::POSTGRESQL, "ALTER TABLE `table` ALTER COLUMN `foo` VARCHAR(42) DEFAULT 'bar' NOT NULL ;"])] + #[TestWith([DatabaseDialect::POSTGRESQL, "ALTER TABLE \"table\" ALTER COLUMN \"foo\" VARCHAR(42) DEFAULT 'bar' NOT NULL ;"])] public function test_alter_table_modify_column(DatabaseDialect $dialect, string $expected): void { $statement = new AlterTableStatement('table') diff --git a/packages/database/tests/QueryStatements/CreateTableStatementTest.php b/packages/database/tests/QueryStatements/CreateTableStatementTest.php index b3989ba11..56d4e087b 100644 --- a/packages/database/tests/QueryStatements/CreateTableStatementTest.php +++ b/packages/database/tests/QueryStatements/CreateTableStatementTest.php @@ -46,8 +46,8 @@ public static function provide_create_table_database_dialects(): iterable yield 'postgresql' => [ DatabaseDialect::POSTGRESQL, << [ DatabaseDialect::POSTGRESQL, << [ DatabaseDialect::POSTGRESQL, << [ DatabaseDialect::POSTGRESQL, << [ DatabaseDialect::POSTGRESQL, <<assertSame($expected, $statement->compile(DatabaseDialect::MYSQL)); $this->assertSame($expected, $statement->compile(DatabaseDialect::SQLITE)); - $this->assertSame($expected, $statement->compile(DatabaseDialect::POSTGRESQL)); + $expectedPostgres = 'DELETE FROM "foo" WHERE `bar` = "1"'; + + $this->assertSame($expectedPostgres, $statement->compile(DatabaseDialect::POSTGRESQL)); } public function test_exception_when_no_condition_is_set(): void diff --git a/packages/database/tests/QueryStatements/FieldStatementTest.php b/packages/database/tests/QueryStatements/FieldStatementTest.php index 6524d25ce..6610f2026 100644 --- a/packages/database/tests/QueryStatements/FieldStatementTest.php +++ b/packages/database/tests/QueryStatements/FieldStatementTest.php @@ -47,12 +47,12 @@ public function test_mysql(): void public function test_postgres(): void { $this->assertSame( - '`table`.`field`', + '"table"."field"', new FieldStatement('`table`.`field`')->compile(DatabaseDialect::POSTGRESQL), ); $this->assertSame( - '`table`.`field`', + '"table"."field"', new FieldStatement('table.field')->compile(DatabaseDialect::POSTGRESQL), ); } diff --git a/packages/database/tests/QueryStatements/InsertStatementTest.php b/packages/database/tests/QueryStatements/InsertStatementTest.php index 7c8789822..1de702d4f 100644 --- a/packages/database/tests/QueryStatements/InsertStatementTest.php +++ b/packages/database/tests/QueryStatements/InsertStatementTest.php @@ -26,7 +26,7 @@ public function test_insert_statement(): void $this->assertSame($expected, $statement->compile(DatabaseDialect::MYSQL)); $this->assertSame($expected, $statement->compile(DatabaseDialect::SQLITE)); - $expectedPostgres = 'INSERT INTO `foo` AS `bar` (`foo`, `bar`) VALUES (?, ?), (?, ?) RETURNING *'; + $expectedPostgres = 'INSERT INTO `foo` AS `bar` ("foo", "bar") VALUES (?, ?), (?, ?) RETURNING *'; $this->assertSame($expectedPostgres, $statement->compile(DatabaseDialect::POSTGRESQL)); } diff --git a/packages/database/tests/QueryStatements/UpdateStatementTest.php b/packages/database/tests/QueryStatements/UpdateStatementTest.php index 266772573..afee68d32 100644 --- a/packages/database/tests/QueryStatements/UpdateStatementTest.php +++ b/packages/database/tests/QueryStatements/UpdateStatementTest.php @@ -28,7 +28,9 @@ public function test_update(): void $this->assertSame($expected, $statement->compile(DatabaseDialect::MYSQL)); $this->assertSame($expected, $statement->compile(DatabaseDialect::SQLITE)); - $this->assertSame($expected, $statement->compile(DatabaseDialect::POSTGRESQL)); + $expectedPostgres = 'UPDATE "foo" SET "bar" = ?, "baz" = ? WHERE `bar` = ?'; + + $this->assertSame($expectedPostgres, $statement->compile(DatabaseDialect::POSTGRESQL)); } public function test_exception_when_no_values(): void diff --git a/packages/database/tests/QueryStatements/UuidPrimaryKeyStatementTest.php b/packages/database/tests/QueryStatements/UuidPrimaryKeyStatementTest.php index 70ed1b044..bcaa91986 100644 --- a/packages/database/tests/QueryStatements/UuidPrimaryKeyStatementTest.php +++ b/packages/database/tests/QueryStatements/UuidPrimaryKeyStatementTest.php @@ -29,7 +29,7 @@ public function postgresql_compilation(): void $statement = new UuidPrimaryKeyStatement('uuid'); $compiled = $statement->compile(DatabaseDialect::POSTGRESQL); - $this->assertSame('`uuid` UUID PRIMARY KEY', $compiled); + $this->assertSame('"uuid" UUID PRIMARY KEY', $compiled); } #[Test] diff --git a/tests/Integration/Database/Builder/InsertQueryBuilderTest.php b/tests/Integration/Database/Builder/InsertQueryBuilderTest.php index 1ea96334d..fcfba7b85 100644 --- a/tests/Integration/Database/Builder/InsertQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/InsertQueryBuilderTest.php @@ -192,7 +192,7 @@ public function test_insert_mapping(): void $expected = match ($dialect) { DatabaseDialect::POSTGRESQL => <<<'SQL' - INSERT INTO authors (name) VALUES (?) RETURNING * + INSERT INTO "authors" ("name") VALUES (?) RETURNING * SQL, default => <<<'SQL' INSERT INTO `authors` (`name`) VALUES (?) diff --git a/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php b/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php index d34fb0ee1..94600f57f 100644 --- a/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php @@ -314,7 +314,7 @@ public function test_update_mapping(): void $expected = match ($dialect) { DatabaseDialect::POSTGRESQL => <<<'SQL' - UPDATE authors SET name = ? WHERE authors.id = ? + UPDATE "authors" SET "name" = ? WHERE "authors"."id" = ? SQL, default => <<<'SQL' UPDATE `authors` SET `name` = ? WHERE `authors`.`id` = ? diff --git a/tests/Integration/FrameworkIntegrationTestCase.php b/tests/Integration/FrameworkIntegrationTestCase.php index 6a1c91959..66efa7148 100644 --- a/tests/Integration/FrameworkIntegrationTestCase.php +++ b/tests/Integration/FrameworkIntegrationTestCase.php @@ -62,7 +62,7 @@ protected function assertSameWithoutBackticks(Stringable|string $expected, Strin { $clean = fn (string $string): string => str($string) ->replace('`', '') - ->replaceRegex('/AS \"(?.*?)\"/', fn (array $matches) => "AS {$matches['alias']}") + ->replace('"', '') ->toString(); $this->assertSame(