diff --git a/src/Command/AbstractCommand.php b/src/Command/AbstractCommand.php index 10ba47fb6..1178442e3 100644 --- a/src/Command/AbstractCommand.php +++ b/src/Command/AbstractCommand.php @@ -7,6 +7,7 @@ use Closure; use Throwable; use Yiisoft\Db\Exception\Exception; +use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Query\Data\DataReaderInterface; use Yiisoft\Db\Query\QueryInterface; @@ -21,6 +22,7 @@ use function is_scalar; use function is_string; use function preg_replace_callback; +use function sprintf; use function str_starts_with; use function stream_get_contents; @@ -526,6 +528,13 @@ public function upsert( return $this->setSql($sql)->bindValues($params); } + public function refreshMaterializedView(string $viewName, ?bool $concurrently = null, ?bool $withData = null): bool + { + $sql = $this->getQueryBuilder()->refreshMaterializedView($viewName, $concurrently ?? false, $withData); + + throw new NotSupportedException(sprintf('"%s" command not supported', $sql)); + } + /** * @return QueryBuilderInterface The query builder instance. */ diff --git a/src/Command/CommandInterface.php b/src/Command/CommandInterface.php index 3caad60d2..0838e6416 100644 --- a/src/Command/CommandInterface.php +++ b/src/Command/CommandInterface.php @@ -833,4 +833,14 @@ public function upsert( bool|array $updateColumns = true, array $params = [] ): static; + + /** + * + * Execute `REFRESH MATERIALIZED VIEW` command + * @param string $viewName + * @param bool|null $concurrently If `null` then auto choice from depends on DBMS + * @param bool|null $withData + * @return bool + */ + public function refreshMaterializedView(string $viewName, ?bool $concurrently = null, ?bool $withData = null): bool; } diff --git a/src/QueryBuilder/AbstractDDLQueryBuilder.php b/src/QueryBuilder/AbstractDDLQueryBuilder.php index d73f57853..13660ad5b 100644 --- a/src/QueryBuilder/AbstractDDLQueryBuilder.php +++ b/src/QueryBuilder/AbstractDDLQueryBuilder.php @@ -11,6 +11,7 @@ use Yiisoft\Db\Schema\SchemaInterface; use function implode; +use function is_bool; use function is_string; use function preg_split; @@ -302,4 +303,21 @@ public function truncateTable(string $table): string { return 'TRUNCATE TABLE ' . $this->quoter->quoteTableName($table); } + + public function refreshMaterializedView(string $viewName, bool $concurrently = false, ?bool $withData = null): string + { + $sql = 'REFRESH MATERIALIZED VIEW '; + + if ($concurrently) { + $sql .= 'CONCURRENTLY '; + } + + $sql .= $this->quoter->quoteTableName($viewName); + + if (is_bool($withData)) { + $sql .= ' WITH ' . ($withData ? 'DATA' : 'NO DATA'); + } + + return $sql; + } } diff --git a/src/QueryBuilder/AbstractQueryBuilder.php b/src/QueryBuilder/AbstractQueryBuilder.php index 53971f27b..e89171e8d 100644 --- a/src/QueryBuilder/AbstractQueryBuilder.php +++ b/src/QueryBuilder/AbstractQueryBuilder.php @@ -405,4 +405,9 @@ public function upsert( ): string { return $this->dmlBuilder->upsert($table, $insertColumns, $updateColumns, $params); } + + public function refreshMaterializedView(string $viewName, bool $concurrently = false, ?bool $withData = null): string + { + return $this->ddlBuilder->refreshMaterializedView($viewName, $concurrently, $withData); + } } diff --git a/src/QueryBuilder/DDLQueryBuilderInterface.php b/src/QueryBuilder/DDLQueryBuilderInterface.php index 3018b5964..801b65c63 100644 --- a/src/QueryBuilder/DDLQueryBuilderInterface.php +++ b/src/QueryBuilder/DDLQueryBuilderInterface.php @@ -424,4 +424,15 @@ public function renameTable(string $oldName, string $newName): string; * Note: The method will quote the `table` parameter before using it in the generated SQL. */ public function truncateTable(string $table): string; + + /** + * Refresh materialized view + * + * @param string $viewName The name of the view to refresh. + * @param bool $concurrently Refresh the materialized view without locking out concurrent selects on the materialized view. + * @param bool|null $withData When `true` then the backing query is executed to provide the new data. Otherwise if `false` then no new data is generated and the materialized view is left in an unscannable state + * + * @return string The `REFRESH MATERIALIZED VIEW` SQL statement + */ + public function refreshMaterializedView(string $viewName, bool $concurrently = false, ?bool $withData = null): string; } diff --git a/tests/Db/Command/CommandTest.php b/tests/Db/Command/CommandTest.php index d361a9ab8..3d8d89c4b 100644 --- a/tests/Db/Command/CommandTest.php +++ b/tests/Db/Command/CommandTest.php @@ -727,4 +727,39 @@ public function testProfilerData(string $sql = null): void ); parent::testProfilerData(); } + + public static function refreshMaterializedViewDataProvider(): array + { + return [ + [ + 'default_mt', + null, + null, + ], + [ + 'concurrently_mt', + true, + null, + ], + [ + 'concurrently_with_data_mt', + true, + true, + ], + [ + 'concurrently_without_data_mt', + true, + false, + ], + ]; + } + + + public function testRefreshMaterializedView(string $viewName, ?bool $concurrently, ?bool $withData): void + { + $db = $this->getConnection(); + $this->expectException(NotSupportedException::class); + + $db->createCommand()->refreshMaterializedView($viewName, $concurrently, $withData); + } } diff --git a/tests/Db/QueryBuilder/QueryBuilderTest.php b/tests/Db/QueryBuilder/QueryBuilderTest.php index c549aec3d..adaaf08d0 100644 --- a/tests/Db/QueryBuilder/QueryBuilderTest.php +++ b/tests/Db/QueryBuilder/QueryBuilderTest.php @@ -178,7 +178,7 @@ public function testGetExpressionBuilderException(): void $this->expectException(Exception::class); - $expression = new class () implements ExpressionInterface { + $expression = new class() implements ExpressionInterface { }; $qb = $db->getQueryBuilder(); $qb->getExpressionBuilder($expression); @@ -310,4 +310,64 @@ public function testUpsertExecute( $actualParams = []; $actualSQL = $db->getQueryBuilder()->upsert($table, $insertColumns, $updateColumns, $actualParams); } + + + public static function refreshMaterializedViewDataProvider(): array + { + return [ + [ + 'concurrently_mt', + true, + null, + 'REFRESH MATERIALIZED VIEW CONCURRENTLY [[concurrently_mt]]', + ], + [ + 'concurrently_with_data_mt', + true, + true, + 'REFRESH MATERIALIZED VIEW CONCURRENTLY [[concurrently_mt]] WITH DATA', + ], + [ + 'concurrently_without_data_mt', + true, + false, + 'REFRESH MATERIALIZED VIEW CONCURRENTLY [[concurrently_mt]] WITH NO DATA', + ], + [ + 'not_concurrently_mt', + false, + null, + 'REFRESH MATERIALIZED VIEW [[concurrently_mt]]', + ], + [ + 'not_concurrently_with_data_mt', + false, + true, + 'REFRESH MATERIALIZED VIEW [[concurrently_mt]] WITH DATA', + ], + [ + 'not_concurrently_without_data_mt', + false, + false, + 'REFRESH MATERIALIZED VIEW [[concurrently_mt]] WITH NO DATA', + ], + ]; + } + + /** + * @dataProvider refreshMaterializedViewDataProvider + * @param bool $concurrently + * @param bool|null $withData + * @return void + */ + public function testRefreshMaterializedView(string $viewName, bool $concurrently, ?bool $withData, string $expected): void + { + $db = $this->getConnection(); + $driver = $db->getDriverName(); + $sql = $db->getQueryBuilder()->refreshMaterializedView($viewName, $concurrently, $withData); + $actual = DbHelper::replaceQuotes($sql, $driver); + $expected = DbHelper::replaceQuotes($expected, $driver); + + $this->assertSame($expected, $actual); + } }