From aaff2850809ae47fe7149db72b8296366f793de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 19 Jan 2024 16:46:52 +0300 Subject: [PATCH] Add refreshMaterializedView command --- src/Command.php | 56 +++++++++++++++++++++++ tests/CommandTest.php | 79 ++++++++++++++++++++++++++++++++- tests/Support/Fixture/pgsql.sql | 7 +++ 3 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/Command.php b/src/Command.php index c686f212..f24dd5e8 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): void + { + 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'); + } + + $this->setSql($sql)->execute(); + } } diff --git a/tests/CommandTest.php b/tests/CommandTest.php index d924a550..e1ca3ee1 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,78 @@ 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(); + + $command->refreshMaterializedView('mat_view_without_unique'); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('CONCURRENTLY refresh is not allowed without unique index.'); + $command->refreshMaterializedView('mat_view_without_unique', 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/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index 6b786a9d..1e479f86 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");