diff --git a/src/Command.php b/src/Command.php index c686f2122..230c10ac6 100644 --- a/src/Command.php +++ b/src/Command.php @@ -4,8 +4,12 @@ namespace Yiisoft\Db\Pgsql; +use InvalidArgumentException; +use LogicException; use Yiisoft\Db\Driver\Pdo\AbstractPdoCommand; +use function sprintf; + /** * Implements a database command that can be executed with a PDO (PHP Data Object) database connection for PostgreSQL * Server. @@ -20,4 +24,56 @@ public function showDatabases(): array return $this->setSql($sql)->queryColumn(); } + + /** + * @see {https://www.postgresql.org/docs/current/sql-refreshmaterializedview.html} + * + * @param string $viewName + * @param bool|null $concurrently Add [ CONCURRENTLY ] to refresh command + * @param bool|null $withData Add [ WITH [ NO ] DATA ] to refresh command + * @return void + * @throws \Throwable + * @throws \Yiisoft\Db\Exception\Exception + * @throws \Yiisoft\Db\Exception\InvalidConfigException + */ + public function refreshMaterializedView(string $viewName, ?bool $concurrently = null, ?bool $withData = null): bool + { + if ($concurrently || ($concurrently === null || $withData === null)) { + + $tableSchema = $this->db->getTableSchema($viewName); + + if ($tableSchema) { + $hasUnique = count($this->db->getSchema()->findUniqueIndexes($tableSchema)) > 0; + } else { + throw new InvalidArgumentException( + sprintf('"%s" not found in DB', $viewName) + ); + } + + if ($concurrently && !$hasUnique) { + throw new LogicException('CONCURRENTLY refresh is not allowed without unique index.'); + } + + $concurrently = $hasUnique; + } + + $sql = 'REFRESH MATERIALIZED VIEW'; + + if ($concurrently) { + + if ($withData === false) { + throw new LogicException('CONCURRENTLY and WITH NO DATA may not be specified together.'); + } + + $sql .= ' CONCURRENTLY'; + } + + $sql .= ' ' . $this->db->getQuoter()->quoteTableName($viewName); + + if (is_bool($withData)) { + $sql .= ' WITH ' . ($withData ? 'DATA' : 'NO DATA'); + } + + return $this->setSql($sql)->execute() === 0; + } } diff --git a/tests/CommandTest.php b/tests/CommandTest.php index d924a5503..270536243 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -4,14 +4,17 @@ namespace Yiisoft\Db\Pgsql\Tests; +use InvalidArgumentException; +use LogicException; use Throwable; use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\JsonExpression; +use Yiisoft\Db\Pgsql\Command; use Yiisoft\Db\Pgsql\Connection; -use Yiisoft\Db\Pgsql\Dsn; use Yiisoft\Db\Pgsql\Driver; +use Yiisoft\Db\Pgsql\Dsn; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Tests\Common\CommonCommandTest; use Yiisoft\Db\Tests\Support\DbHelper; @@ -330,4 +333,76 @@ public function testShowDatabases(): void $this->assertSame('pgsql:host=127.0.0.1;dbname=postgres;port=5432', $db->getDriver()->getDsn()); $this->assertSame(['yiitest'], $command->showDatabases()); } + + public function testRefreshMaterializesView(): void + { + $db = $this->getConnection(true); + /** @var Command $command */ + $command = $db->createCommand(); + + $this->assertTrue($command->refreshMaterializedView('mat_view_without_unique')); + $this->assertTrue($command->refreshMaterializedView('mat_view_without_unique', false, true)); + $this->assertTrue($command->refreshMaterializedView('mat_view_without_unique', false, true)); + } + + public static function materializedViewExceptionsDataProvider(): array + { + return [ + [ + 'mat_view_without_unique', + true, + null, + LogicException::class, + 'CONCURRENTLY refresh is not allowed without unique index.' + ], + + [ + 'mat_view_with_unique', + true, + false, + LogicException::class, + 'CONCURRENTLY and WITH NO DATA may not be specified together.' + ], + + [ + 'mat_view_with_unique', + null, + false, + LogicException::class, + 'CONCURRENTLY and WITH NO DATA may not be specified together.' + ], + + [ + 'not_exists_mat_view', + null, + null, + InvalidArgumentException::class, + '"not_exists_mat_view" not found in DB' + ] + ]; + } + + /** + * @dataProvider materializedViewExceptionsDataProvider + * @param string $viewName + * @param bool|null $concurrently + * @param bool|null $withData + * @param string $exception + * @param string $message + * @return void + * @throws Exception + * @throws InvalidConfigException + */ + public function testRefreshMaterializesViewExceptions(string $viewName, ?bool $concurrently, ?bool $withData, string $exception, string $message): void + { + $db = $this->getConnection(true); + + /** @var Command $command */ + $command = $db->createCommand(); + + $this->expectException($exception); + $this->expectExceptionMessage($message); + + $command->refreshMaterializedView($viewName, $concurrently, $withData); + } } diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 2d852a14b..90fbe7990 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -574,6 +574,8 @@ public function testGetViewNames(): void 'T_constraints_3_view', 'T_constraints_4_view', 'animal_view', + 'mat_view_with_unique', + 'mat_view_without_unique', ], $views, ); diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index 6b786a9de..1e479f862 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -487,3 +487,10 @@ CREATE TABLE "test_composite_type" "price_array2" "currency_money_composite"[][], "range_price_col" "range_price_composite" DEFAULT '("(0,USD)","(100,USD)")' ); + + +DROP MATERIALIZED VIEW IF EXISTS "mat_view_without_unique"; +DROP MATERIALIZED VIEW IF EXISTS "mat_view_with_unique"; +CREATE MATERIALIZED VIEW "mat_view_without_unique" AS SELECT * FROM "test_composite_type"; +CREATE MATERIALIZED VIEW "mat_view_with_unique" AS SELECT * FROM "test_composite_type"; +CREATE UNIQUE INDEX "mat_view_with_unique_idx" ON "mat_view_with_unique" ("id");