From b869066036e8cb6541e3edd09ea6e41bdbe79464 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Wed, 4 Mar 2026 15:34:29 -0600 Subject: [PATCH] fix: support for explicit schemas in update/merge/delete query builders --- .../QueryBuilder/Delete/DeleteBuilder.php | 23 +++++- .../QueryBuilder/Merge/MergeBuilder.php | 15 +++- .../QueryBuilder/Update/UpdateBuilder.php | 25 +++++- .../Database/DeleteDatabaseTest.php | 45 +++++++++++ .../Database/InsertDatabaseTest.php | 38 +++++++++ .../Database/MergeDatabaseTest.php | 81 +++++++++++++++++++ .../Database/SelectDatabaseTest.php | 38 +++++++++ .../Database/TruncateDatabaseTest.php | 47 +++++++++++ .../Database/UpdateDatabaseTest.php | 46 +++++++++++ .../QueryBuilder/Delete/DeleteBuilderTest.php | 45 +++++++++++ .../QueryBuilder/Merge/MergeBuilderTest.php | 24 ++++++ .../QueryBuilder/Update/UpdateBuilderTest.php | 51 ++++++++++++ 12 files changed, 472 insertions(+), 6 deletions(-) diff --git a/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Delete/DeleteBuilder.php b/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Delete/DeleteBuilder.php index 3db5938bf..26aaf52bc 100644 --- a/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Delete/DeleteBuilder.php +++ b/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Delete/DeleteBuilder.php @@ -5,7 +5,7 @@ namespace Flow\PostgreSql\QueryBuilder\Delete; use Flow\PostgreSql\Protobuf\AST\{Alias, DeleteStmt, Node, RangeVar, ResTarget}; -use Flow\PostgreSql\QueryBuilder\AstToSql; +use Flow\PostgreSql\QueryBuilder\{AstToSql, QualifiedIdentifier}; use Flow\PostgreSql\QueryBuilder\Clause\WithClause; use Flow\PostgreSql\QueryBuilder\Condition\{Condition, ConditionFactory}; use Flow\PostgreSql\QueryBuilder\Exception\InvalidAstException; @@ -33,6 +33,7 @@ private function __construct( private ?WithClause $with = null, private ?string $table = null, + private ?string $schema = null, private ?string $alias = null, private array $using = [], private ?Condition $where = null, @@ -59,6 +60,12 @@ public static function fromAst(DeleteStmt $deleteStmt) : static throw InvalidAstException::missingRequiredField('relname', 'RangeVar'); } + $schema = $relation->getSchemaname(); + + if ($schema === '') { + $schema = null; + } + $alias = $relation->getAlias(); $aliasName = $alias !== null ? $alias->getAliasname() : null; @@ -125,6 +132,7 @@ public static function fromAst(DeleteStmt $deleteStmt) : static return new self( with: $withClause, table: $tableName, + schema: $schema, alias: $aliasName, using: $using, where: $whereCondition, @@ -139,9 +147,12 @@ public static function with(WithClause $with) : DeleteFromStep public function from(string $table, ?string $alias = null) : DeleteUsingStep { + $identifier = QualifiedIdentifier::parse($table); + return new self( with: $this->with, - table: $table, + table: $identifier->name(), + schema: $identifier->schema(), alias: $alias, using: $this->using, where: $this->where, @@ -154,6 +165,7 @@ public function returning(Expression ...$expressions) : DeleteFinalStep return new self( with: $this->with, table: $this->table, + schema: $this->schema, alias: $this->alias, using: $this->using, where: $this->where, @@ -166,6 +178,7 @@ public function returningAll() : DeleteFinalStep return new self( with: $this->with, table: $this->table, + schema: $this->schema, alias: $this->alias, using: $this->using, where: $this->where, @@ -186,6 +199,10 @@ public function toAst() : DeleteStmt 'inh' => true, ]); + if ($this->schema !== null) { + $rangeVar->setSchemaname($this->schema); + } + if ($this->alias !== null) { $alias = new Alias([ 'aliasname' => $this->alias, @@ -242,6 +259,7 @@ public function using(TableReference ...$tables) : DeleteWhereStep return new self( with: $this->with, table: $this->table, + schema: $this->schema, alias: $this->alias, using: $tables, where: $this->where, @@ -254,6 +272,7 @@ public function where(Condition $condition) : DeleteReturningStep return new self( with: $this->with, table: $this->table, + schema: $this->schema, alias: $this->alias, using: $this->using, where: $condition, diff --git a/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Merge/MergeBuilder.php b/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Merge/MergeBuilder.php index 63f72e406..0fd21ae95 100644 --- a/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Merge/MergeBuilder.php +++ b/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Merge/MergeBuilder.php @@ -27,6 +27,7 @@ private function __construct( private ?string $schema = null, private ?string $tableAlias = null, private ?string $sourceTable = null, + private ?string $sourceSchema = null, private ?SelectFinalStep $sourceSelect = null, private ?string $sourceAlias = null, private ?Condition $joinCondition = null, @@ -52,6 +53,7 @@ public function addWhenClause(MergeWhenClauseData $clause) : MergeWhenStep $this->schema, $this->tableAlias, $this->sourceTable, + $this->sourceSchema, $this->sourceSelect, $this->sourceAlias, $this->joinCondition, @@ -69,6 +71,7 @@ public function into(string $table, ?string $alias = null) : MergeUsingStep $identifier->schema(), $alias, $this->sourceTable, + $this->sourceSchema, $this->sourceSelect, $this->sourceAlias, $this->joinCondition, @@ -84,6 +87,7 @@ public function on(Condition $condition) : MergeWhenStep $this->schema, $this->tableAlias, $this->sourceTable, + $this->sourceSchema, $this->sourceSelect, $this->sourceAlias, $condition, @@ -148,6 +152,11 @@ public function toAst() : MergeStmt 'relname' => $this->sourceTable ?? '', 'inh' => true, ]); + + if ($this->sourceSchema !== null) { + $sourceRangeVar->setSchemaname($this->sourceSchema); + } + $alias = new Alias(); $alias->setAliasname($this->sourceAlias); $sourceRangeVar->setAlias($alias); @@ -187,6 +196,7 @@ public function using(string|SelectFinalStep $source, string $alias) : MergeOnSt $this->schema, $this->tableAlias, null, + null, $source, $alias, $this->joinCondition, @@ -194,12 +204,15 @@ public function using(string|SelectFinalStep $source, string $alias) : MergeOnSt ); } + $identifier = QualifiedIdentifier::parse($source); + return new self( $this->with, $this->table, $this->schema, $this->tableAlias, - $source, + $identifier->name(), + $identifier->schema(), null, $alias, $this->joinCondition, diff --git a/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Update/UpdateBuilder.php b/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Update/UpdateBuilder.php index d8cb3e56d..a5c3a2fb9 100644 --- a/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Update/UpdateBuilder.php +++ b/src/lib/postgresql/src/Flow/PostgreSql/QueryBuilder/Update/UpdateBuilder.php @@ -5,7 +5,7 @@ namespace Flow\PostgreSql\QueryBuilder\Update; use Flow\PostgreSql\Protobuf\AST\{Alias, Node, RangeVar, ResTarget, UpdateStmt}; -use Flow\PostgreSql\QueryBuilder\AstToSql; +use Flow\PostgreSql\QueryBuilder\{AstToSql, QualifiedIdentifier}; use Flow\PostgreSql\QueryBuilder\Clause\WithClause; use Flow\PostgreSql\QueryBuilder\Condition\{Condition, ConditionFactory}; use Flow\PostgreSql\QueryBuilder\Exception\{InvalidAstException, InvalidExpressionException}; @@ -27,6 +27,7 @@ private function __construct( private ?WithClause $with = null, private ?string $table = null, + private ?string $schema = null, private ?string $alias = null, private array $assignments = [], private array $from = [], @@ -62,6 +63,12 @@ public static function fromAst(UpdateStmt $updateStmt) : static throw InvalidAstException::missingRequiredField('relname', 'RangeVar'); } + $schema = $relation->getSchemaname(); + + if ($schema === '') { + $schema = null; + } + $alias = null; if ($relation->hasAlias()) { @@ -127,7 +134,7 @@ public static function fromAst(UpdateStmt $updateStmt) : static } } - return new self($with, $table, $alias, $assignments, $from, $where, $returning); + return new self($with, $table, $schema, $alias, $assignments, $from, $where, $returning); } public static function with(WithClause $with) : UpdateTableStep @@ -140,6 +147,7 @@ public function from(TableReference ...$tables) : UpdateWhereStep return new self( with: $this->with, table: $this->table, + schema: $this->schema, alias: $this->alias, assignments: $this->assignments, from: $tables, @@ -153,6 +161,7 @@ public function returning(Expression ...$expressions) : UpdateFinalStep return new self( with: $this->with, table: $this->table, + schema: $this->schema, alias: $this->alias, assignments: $this->assignments, from: $this->from, @@ -171,6 +180,7 @@ public function set(string $column, Expression $value) : UpdateSetStep return new self( with: $this->with, table: $this->table, + schema: $this->schema, alias: $this->alias, assignments: [...$this->assignments, $column => $value], from: $this->from, @@ -184,6 +194,7 @@ public function setAll(array $assignments) : UpdateFromStep return new self( with: $this->with, table: $this->table, + schema: $this->schema, alias: $this->alias, assignments: [...$this->assignments, ...$assignments], from: $this->from, @@ -206,6 +217,10 @@ public function toAst() : UpdateStmt $rangeVar = new RangeVar(['relname' => $this->table, 'inh' => true]); + if ($this->schema !== null) { + $rangeVar->setSchemaname($this->schema); + } + if ($this->alias !== null) { $aliasProto = new Alias(['aliasname' => $this->alias]); $rangeVar->setAlias($aliasProto); @@ -271,9 +286,12 @@ public function toAst() : UpdateStmt public function update(string $table, ?string $alias = null) : UpdateSetStep { + $identifier = QualifiedIdentifier::parse($table); + return new self( with: $this->with, - table: $table, + table: $identifier->name(), + schema: $identifier->schema(), alias: $alias, assignments: $this->assignments, from: $this->from, @@ -287,6 +305,7 @@ public function where(Condition $condition) : UpdateReturningStep return new self( with: $this->with, table: $this->table, + schema: $this->schema, alias: $this->alias, assignments: $this->assignments, from: $this->from, diff --git a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/DeleteDatabaseTest.php b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/DeleteDatabaseTest.php index ac2b1b306..ae80cd376 100644 --- a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/DeleteDatabaseTest.php +++ b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/DeleteDatabaseTest.php @@ -27,6 +27,10 @@ final class DeleteDatabaseTest extends DatabaseTestCase { + private const SCHEMA_NAME = 'flow_postgres_test_delete_schema'; + + private const SCHEMA_TABLE = 'flow_postgres_schema_logs'; + private const TABLE_ARCHIVE = 'flow_postgres_log_archive'; private const TABLE_LOGS = 'flow_postgres_logs'; @@ -79,6 +83,7 @@ protected function tearDown() : void { $this->dropTableIfExists(self::TABLE_ARCHIVE); $this->dropTableIfExists(self::TABLE_LOGS); + $this->execute('DROP SCHEMA IF EXISTS ' . self::SCHEMA_NAME . ' CASCADE'); parent::tearDown(); } @@ -159,6 +164,46 @@ public function test_delete_with_returning_all() : void self::assertArrayHasKey('created_at', $row); } + public function test_delete_with_schema_qualified_table() : void + { + $this->execute('CREATE SCHEMA IF NOT EXISTS ' . self::SCHEMA_NAME); + + $this->execute( + create()->table(self::SCHEMA_TABLE, self::SCHEMA_NAME) + ->column(column('id', data_type_serial())) + ->column(column('level', data_type_varchar(20))->notNull()) + ->column(column('message', data_type_text())) + ->constraint(primary_key('id')) + ->toSql() + ); + + $this->execute( + insert() + ->into(self::SCHEMA_NAME . '.' . self::SCHEMA_TABLE) + ->columns('level', 'message') + ->values(literal('DEBUG'), literal('Test message')) + ->values(literal('INFO'), literal('Info message')) + ->toSql() + ); + + $query = delete() + ->from(self::SCHEMA_NAME . '.' . self::SCHEMA_TABLE) + ->where(eq(col('level'), literal('DEBUG'))); + + $result = $this->execute($query->toSql()); + + self::assertNotFalse($result); + self::assertSame(1, $this->affectedRows($result)); + + $check = $this->execute( + select(agg_count(star())->as('cnt')) + ->from(table(self::SCHEMA_TABLE, self::SCHEMA_NAME)) + ->toSql() + ); + $row = $this->fetchOne($check); + self::assertSame('1', $row['cnt']); + } + public function test_delete_with_using() : void { $query = delete() diff --git a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/InsertDatabaseTest.php b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/InsertDatabaseTest.php index c486b3fa6..8ed439f80 100644 --- a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/InsertDatabaseTest.php +++ b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/InsertDatabaseTest.php @@ -26,6 +26,10 @@ final class InsertDatabaseTest extends DatabaseTestCase { + private const SCHEMA_NAME = 'flow_postgres_test_insert_schema'; + + private const SCHEMA_TABLE = 'flow_postgres_schema_products'; + private const TABLE_PRODUCTS = 'flow_postgres_products'; protected function setUp() : void @@ -48,6 +52,7 @@ protected function setUp() : void protected function tearDown() : void { $this->dropTableIfExists(self::TABLE_PRODUCTS); + $this->execute('DROP SCHEMA IF EXISTS ' . self::SCHEMA_NAME . ' CASCADE'); parent::tearDown(); } @@ -189,4 +194,37 @@ public function test_insert_with_returning_all() : void self::assertArrayHasKey('price', $row); self::assertArrayHasKey('stock', $row); } + + public function test_insert_with_schema_qualified_table() : void + { + $this->execute('CREATE SCHEMA IF NOT EXISTS ' . self::SCHEMA_NAME); + + $this->execute( + create()->table(self::SCHEMA_TABLE, self::SCHEMA_NAME) + ->column(column('id', data_type_serial())) + ->column(column('sku', data_type_varchar(50))->notNull()) + ->column(column('name', data_type_varchar(100))->notNull()) + ->constraint(primary_key('id')) + ->toSql() + ); + + $query = insert() + ->into(self::SCHEMA_NAME . '.' . self::SCHEMA_TABLE) + ->columns('sku', 'name') + ->values(literal('SCHEMA-SKU'), literal('Schema Product')); + + $result = $this->execute($query->toSql()); + + self::assertNotFalse($result); + self::assertSame(1, $this->affectedRows($result)); + + $check = $this->execute( + select(col('name')) + ->from(table(self::SCHEMA_TABLE, self::SCHEMA_NAME)) + ->where(eq(col('sku'), literal('SCHEMA-SKU'))) + ->toSql() + ); + $row = $this->fetchOne($check); + self::assertSame('Schema Product', $row['name']); + } } diff --git a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/MergeDatabaseTest.php b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/MergeDatabaseTest.php index 805348009..f86e05ecd 100644 --- a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/MergeDatabaseTest.php +++ b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/MergeDatabaseTest.php @@ -25,6 +25,12 @@ final class MergeDatabaseTest extends DatabaseTestCase { + private const SCHEMA_NAME = 'flow_postgres_test_merge_schema'; + + private const SCHEMA_SOURCE = 'flow_postgres_schema_merge_source'; + + private const SCHEMA_TARGET = 'flow_postgres_schema_merge_target'; + private const TABLE_SOURCE = 'flow_postgres_merge_source'; private const TABLE_TARGET = 'flow_postgres_merge_target'; @@ -75,6 +81,7 @@ protected function tearDown() : void { $this->dropTableIfExists(self::TABLE_SOURCE); $this->dropTableIfExists(self::TABLE_TARGET); + $this->execute('DROP SCHEMA IF EXISTS ' . self::SCHEMA_NAME . ' CASCADE'); parent::tearDown(); } @@ -272,4 +279,78 @@ public function test_merge_with_condition() : void self::assertSame('100', $rows[0]['value']); self::assertSame('250', $rows[1]['value']); } + + public function test_merge_with_schema_qualified_tables() : void + { + $this->execute('CREATE SCHEMA IF NOT EXISTS ' . self::SCHEMA_NAME); + + $this->execute( + create()->table(self::SCHEMA_TARGET, self::SCHEMA_NAME) + ->column(column('id', data_type_serial())) + ->column(column('name', data_type_varchar(100))->notNull()) + ->column(column('value', data_type_integer())->default(0)) + ->constraint(primary_key('id')) + ->toSql() + ); + + $this->execute( + create()->table(self::SCHEMA_SOURCE, self::SCHEMA_NAME) + ->column(column('id', data_type_integer())->notNull()) + ->column(column('name', data_type_varchar(100))->notNull()) + ->column(column('value', data_type_integer())->default(0)) + ->constraint(primary_key('id')) + ->toSql() + ); + + $this->execute( + insert() + ->into(self::SCHEMA_NAME . '.' . self::SCHEMA_TARGET) + ->columns('name', 'value') + ->values(literal('Target Row'), literal(100)) + ->toSql() + ); + + $this->execute( + insert() + ->into(self::SCHEMA_NAME . '.' . self::SCHEMA_SOURCE) + ->columns('id', 'name', 'value') + ->values(literal(1), literal('Updated Row'), literal(200)) + ->values(literal(2), literal('New Row'), literal(300)) + ->toSql() + ); + + $query = merge(self::SCHEMA_NAME . '.' . self::SCHEMA_TARGET, 't') + ->using(self::SCHEMA_NAME . '.' . self::SCHEMA_SOURCE, 's') + ->on(eq(col('t.id'), col('s.id'))) + ->whenMatched() + ->thenUpdate([ + 'name' => col('s.name'), + 'value' => col('s.value'), + ]) + ->whenNotMatched() + ->thenInsertValues([ + 'name' => col('s.name'), + 'value' => col('s.value'), + ]); + + $result = $this->execute($query->toSql()); + + self::assertNotFalse($result); + self::assertSame(2, $this->affectedRows($result)); + + $rows = $this->fetchAll( + $this->execute( + select(star()) + ->from(table(self::SCHEMA_TARGET, self::SCHEMA_NAME)) + ->orderBy(order_by(col('id'))) + ->toSql() + ) + ); + + self::assertCount(2, $rows); + self::assertSame('Updated Row', $rows[0]['name']); + self::assertSame('200', $rows[0]['value']); + self::assertSame('New Row', $rows[1]['name']); + self::assertSame('300', $rows[1]['value']); + } } diff --git a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/SelectDatabaseTest.php b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/SelectDatabaseTest.php index f32e5e8c1..127b838be 100644 --- a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/SelectDatabaseTest.php +++ b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/SelectDatabaseTest.php @@ -29,6 +29,10 @@ final class SelectDatabaseTest extends DatabaseTestCase { + private const SCHEMA_NAME = 'flow_postgres_test_select_schema'; + + private const SCHEMA_TABLE = 'flow_postgres_schema_users'; + private const TABLE_ORDERS = 'flow_postgres_orders'; private const TABLE_USERS = 'flow_postgres_users'; @@ -83,6 +87,7 @@ protected function tearDown() : void { $this->dropTableIfExists(self::TABLE_ORDERS); $this->dropTableIfExists(self::TABLE_USERS); + $this->execute('DROP SCHEMA IF EXISTS ' . self::SCHEMA_NAME . ' CASCADE'); parent::tearDown(); } @@ -208,6 +213,39 @@ public function test_select_with_order_by_and_limit() : void self::assertSame('John Doe', $rows[1]['name']); } + public function test_select_with_schema_qualified_table() : void + { + $this->execute('CREATE SCHEMA IF NOT EXISTS ' . self::SCHEMA_NAME); + + $this->execute( + create()->table(self::SCHEMA_TABLE, self::SCHEMA_NAME) + ->column(column('id', data_type_serial())) + ->column(column('name', data_type_varchar(100))->notNull()) + ->column(column('email', data_type_varchar(255))) + ->constraint(primary_key('id')) + ->toSql() + ); + + $this->execute( + insert() + ->into(self::SCHEMA_NAME . '.' . self::SCHEMA_TABLE) + ->columns('name', 'email') + ->values(literal('Schema User'), literal('schema@example.com')) + ->toSql() + ); + + $query = select(star()) + ->from(table(self::SCHEMA_TABLE, self::SCHEMA_NAME)); + + $result = $this->execute($query->toSql()); + + self::assertNotFalse($result); + $rows = $this->fetchAll($result); + self::assertCount(1, $rows); + self::assertSame('Schema User', $rows[0]['name']); + self::assertSame('schema@example.com', $rows[0]['email']); + } + public function test_select_with_where() : void { $query = select(star()) diff --git a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/TruncateDatabaseTest.php b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/TruncateDatabaseTest.php index 743ddf2b4..545d603fc 100644 --- a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/TruncateDatabaseTest.php +++ b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/TruncateDatabaseTest.php @@ -22,6 +22,10 @@ final class TruncateDatabaseTest extends DatabaseTestCase { + private const SCHEMA_NAME = 'flow_postgres_test_truncate_schema'; + + private const SCHEMA_TABLE = 'flow_postgres_schema_truncate'; + private const TABLE_ONE = 'flow_postgres_truncate_one'; private const TABLE_TWO = 'flow_postgres_truncate_two'; @@ -70,6 +74,7 @@ protected function tearDown() : void { $this->dropTableIfExists(self::TABLE_TWO); $this->dropTableIfExists(self::TABLE_ONE); + $this->execute('DROP SCHEMA IF EXISTS ' . self::SCHEMA_NAME . ' CASCADE'); parent::tearDown(); } @@ -240,4 +245,46 @@ public function test_truncate_with_restart_identity_and_cascade() : void self::assertSame('1', $row['id']); } + + public function test_truncate_with_schema_qualified_table() : void + { + $this->execute('CREATE SCHEMA IF NOT EXISTS ' . self::SCHEMA_NAME); + + $this->execute( + create()->table(self::SCHEMA_TABLE, self::SCHEMA_NAME) + ->column(column('id', data_type_serial())) + ->column(column('name', data_type_varchar(100))->notNull()) + ->constraint(primary_key('id')) + ->toSql() + ); + + $this->execute( + insert() + ->into(self::SCHEMA_NAME . '.' . self::SCHEMA_TABLE) + ->columns('name') + ->values(literal('Row 1')) + ->values(literal('Row 2')) + ->toSql() + ); + + $rows = $this->fetchAll( + $this->execute( + select(star())->from(table(self::SCHEMA_TABLE, self::SCHEMA_NAME))->toSql() + ) + ); + self::assertCount(2, $rows); + + $result = $this->execute( + truncate_table(self::SCHEMA_NAME . '.' . self::SCHEMA_TABLE)->toSql() + ); + + self::assertNotFalse($result); + + $rows = $this->fetchAll( + $this->execute( + select(star())->from(table(self::SCHEMA_TABLE, self::SCHEMA_NAME))->toSql() + ) + ); + self::assertCount(0, $rows); + } } diff --git a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/UpdateDatabaseTest.php b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/UpdateDatabaseTest.php index b531d9094..1e8b938c8 100644 --- a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/UpdateDatabaseTest.php +++ b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Integration/QueryBuilder/Database/UpdateDatabaseTest.php @@ -27,6 +27,10 @@ final class UpdateDatabaseTest extends DatabaseTestCase { + private const SCHEMA_NAME = 'flow_postgres_test_update_schema'; + + private const SCHEMA_TABLE = 'flow_postgres_schema_employees'; + private const TABLE_DEPARTMENTS = 'flow_postgres_departments'; private const TABLE_EMPLOYEES = 'flow_postgres_employees'; @@ -81,6 +85,7 @@ protected function tearDown() : void { $this->dropTableIfExists(self::TABLE_EMPLOYEES); $this->dropTableIfExists(self::TABLE_DEPARTMENTS); + $this->execute('DROP SCHEMA IF EXISTS ' . self::SCHEMA_NAME . ' CASCADE'); parent::tearDown(); } @@ -187,6 +192,47 @@ public function test_update_with_returning_all() : void self::assertSame('reviewed', $row['status']); } + public function test_update_with_schema_qualified_table() : void + { + $this->execute('CREATE SCHEMA IF NOT EXISTS ' . self::SCHEMA_NAME); + + $this->execute( + create()->table(self::SCHEMA_TABLE, self::SCHEMA_NAME) + ->column(column('id', data_type_serial())) + ->column(column('name', data_type_varchar(100))->notNull()) + ->column(column('status', data_type_varchar(50))->default('active')) + ->constraint(primary_key('id')) + ->toSql() + ); + + $this->execute( + insert() + ->into(self::SCHEMA_NAME . '.' . self::SCHEMA_TABLE) + ->columns('name', 'status') + ->values(literal('Alice'), literal('active')) + ->toSql() + ); + + $query = update() + ->update(self::SCHEMA_NAME . '.' . self::SCHEMA_TABLE) + ->set('status', literal('updated')) + ->where(eq(col('name'), literal('Alice'))); + + $result = $this->execute($query->toSql()); + + self::assertNotFalse($result); + self::assertSame(1, $this->affectedRows($result)); + + $check = $this->execute( + select(col('status')) + ->from(table(self::SCHEMA_TABLE, self::SCHEMA_NAME)) + ->where(eq(col('name'), literal('Alice'))) + ->toSql() + ); + $row = $this->fetchOne($check); + self::assertSame('updated', $row['status']); + } + public function test_update_with_where() : void { $query = update() diff --git a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Delete/DeleteBuilderTest.php b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Delete/DeleteBuilderTest.php index e22744fea..c438b584d 100644 --- a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Delete/DeleteBuilderTest.php +++ b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Delete/DeleteBuilderTest.php @@ -284,6 +284,51 @@ public function test_delete_with_returning_deparsed_output() : void self::assertSame('DELETE FROM sessions WHERE expired = true RETURNING id', $deparsed); } + public function test_delete_with_schema() : void + { + $query = DeleteBuilder::create() + ->from('myschema.users'); + + $ast = $query->toAst(); + + $relation = $ast->getRelation(); + self::assertNotNull($relation); + self::assertSame('users', $relation->getRelname()); + self::assertSame('myschema', $relation->getSchemaname()); + } + + public function test_delete_with_schema_deparsed_output() : void + { + if (!\function_exists('pg_query_deparse')) { + self::markTestSkipped('pg_query_deparse function not available.'); + } + + $query = DeleteBuilder::create() + ->from('public.users'); + + $deparsed = $this->deparse($query->toAst()); + self::assertSame('DELETE FROM public.users', $deparsed); + } + + public function test_delete_with_schema_round_trip() : void + { + $original = DeleteBuilder::create() + ->from('myschema.users'); + + $ast = $original->toAst(); + $restored = DeleteBuilder::fromAst($ast); + $restoredAst = $restored->toAst(); + + $originalRelation = $ast->getRelation(); + self::assertNotNull($originalRelation); + + $restoredRelation = $restoredAst->getRelation(); + self::assertNotNull($restoredRelation); + + self::assertSame($originalRelation->getRelname(), $restoredRelation->getRelname()); + self::assertSame($originalRelation->getSchemaname(), $restoredRelation->getSchemaname()); + } + public function test_delete_with_using() : void { $query = DeleteBuilder::create() diff --git a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Merge/MergeBuilderTest.php b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Merge/MergeBuilderTest.php index ffd67f240..043480e40 100644 --- a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Merge/MergeBuilderTest.php +++ b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Merge/MergeBuilderTest.php @@ -144,6 +144,30 @@ public function test_merge_match_kind_enum_values() : void self::assertSame(3, MergeMatchKind::NOT_MATCHED_BY_TARGET->value); } + public function test_merge_using_parses_schema_and_table() : void + { + $query = MergeBuilder::create() + ->into('target', 't') + ->using('myschema.source', 's') + ->on(new Comparison(Column::tableColumn('t', 'id'), ComparisonOperator::EQ, Column::tableColumn('s', 'id'))) + ->whenMatched() + ->thenDoNothing(); + + $ast = $query->toAst(); + + $sourceRelation = $ast->getSourceRelation(); + self::assertNotNull($sourceRelation); + + $rangeVar = $sourceRelation->getRangeVar(); + self::assertNotNull($rangeVar); + self::assertSame('source', $rangeVar->getRelname()); + self::assertSame('myschema', $rangeVar->getSchemaname()); + + $alias = $rangeVar->getAlias(); + self::assertNotNull($alias); + self::assertSame('s', $alias->getAliasname()); + } + public function test_merge_when_clause_data_structure() : void { $condition = new Comparison(Column::name('id'), ComparisonOperator::EQ, Literal::int(1)); diff --git a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Update/UpdateBuilderTest.php b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Update/UpdateBuilderTest.php index 88432656d..2dfb9d673 100644 --- a/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Update/UpdateBuilderTest.php +++ b/src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/QueryBuilder/Update/UpdateBuilderTest.php @@ -588,6 +588,57 @@ public function test_update_with_returning_deparsed_output() : void self::assertSame('UPDATE users SET last_login = now() WHERE id = 1 RETURNING id, last_login', $deparsed); } + public function test_update_with_schema() : void + { + $query = UpdateBuilder::create() + ->update('myschema.users') + ->set('name', Literal::string('John')) + ->setAll([]); + + $ast = $query->toAst(); + + $relation = $ast->getRelation(); + self::assertNotNull($relation); + self::assertSame('users', $relation->getRelname()); + self::assertSame('myschema', $relation->getSchemaname()); + } + + public function test_update_with_schema_deparsed_output() : void + { + if (!\function_exists('pg_query_deparse')) { + self::markTestSkipped('pg_query_deparse function not available.'); + } + + $query = UpdateBuilder::create() + ->update('public.users') + ->set('name', Literal::string('John')) + ->setAll([]); + + $deparsed = $this->deparse($query->toAst()); + self::assertSame("UPDATE public.users SET name = 'John'", $deparsed); + } + + public function test_update_with_schema_round_trip() : void + { + $original = UpdateBuilder::create() + ->update('myschema.users') + ->set('name', Literal::string('John')) + ->setAll([]); + + $ast = $original->toAst(); + $restored = UpdateBuilder::fromAst($ast); + $restoredAst = $restored->toAst(); + + $originalRelation = $ast->getRelation(); + self::assertNotNull($originalRelation); + + $restoredRelation = $restoredAst->getRelation(); + self::assertNotNull($restoredRelation); + + self::assertSame($originalRelation->getRelname(), $restoredRelation->getRelname()); + self::assertSame($originalRelation->getSchemaname(), $restoredRelation->getSchemaname()); + } + public function test_update_with_where() : void { $query = UpdateBuilder::create()