diff --git a/dbms/src/Databases/DatabasesCommon.cpp b/dbms/src/Databases/DatabasesCommon.cpp index 8fd18dc7da9..fb16331cb89 100644 --- a/dbms/src/Databases/DatabasesCommon.cpp +++ b/dbms/src/Databases/DatabasesCommon.cpp @@ -22,8 +22,6 @@ String getTableDefinitionFromCreateQuery(const ASTPtr & query) /// We remove everything that is not needed for ATTACH from the query. create.attach = true; create.database.clear(); - create.as_database.clear(); - create.as_table.clear(); create.if_not_exists = false; create.is_populate = false; @@ -31,6 +29,13 @@ String getTableDefinitionFromCreateQuery(const ASTPtr & query) if (!create.is_view && !create.is_materialized_view) create.select = nullptr; + /// For "MATERIALIZED VIEW x TO y" it's necessary to save destination table + if (engine != "MaterializedView" || create.inner_storage) + { + create.as_database.clear(); + create.as_table.clear(); + } + std::ostringstream statement_stream; formatAST(create, statement_stream, 0, false); statement_stream << '\n'; @@ -56,6 +61,8 @@ std::pair createTableFromDefinition( /// We do not directly use `InterpreterCreateQuery::execute`, because /// - the database has not been created yet; /// - the code is simpler, since the query is already brought to a suitable form. + if (!ast_create_query.columns) + throw Exception("Missing definition of columns.", ErrorCodes::EMPTY_LIST_OF_COLUMNS_PASSED); InterpreterCreateQuery::ColumnsInfo columns_info = InterpreterCreateQuery::getColumnsInfo(*ast_create_query.columns, context); diff --git a/dbms/src/Interpreters/InterpreterCreateQuery.cpp b/dbms/src/Interpreters/InterpreterCreateQuery.cpp index e29976f6621..b72f70f8318 100644 --- a/dbms/src/Interpreters/InterpreterCreateQuery.cpp +++ b/dbms/src/Interpreters/InterpreterCreateQuery.cpp @@ -434,6 +434,12 @@ void InterpreterCreateQuery::setEngine(ASTCreateQuery & create) const storage_ast->set(storage_ast->engine, engine_ast); create.set(create.storage, storage_ast); } + else if (create.is_temporary) + set_engine("Memory"); + else if (create.is_view) + set_engine("View"); + else if (create.is_materialized_view) + set_engine("MaterializedView"); else if (!create.as_table.empty()) { /// NOTE Getting the structure from the table specified in the AS is done not atomically with the creation of the table. @@ -470,6 +476,16 @@ BlockIO InterpreterCreateQuery::createTable(ASTCreateQuery & create) String data_path = path + "data/" + database_name_escaped + "/"; String metadata_path = path + "metadata/" + database_name_escaped + "/" + table_name_escaped + ".sql"; + // If this is a stub ATTACH query, read the query definition from the database + if (create.attach && !create.storage && !create.columns) + { + // Table SQL definition is available even if the table is detached + auto query = context.getCreateQuery(database_name, table_name); + auto & as_create = typeid_cast(*query); + create = as_create; // Copy the saved create query, but use ATTACH instead of CREATE + create.attach = true; + } + std::unique_ptr interpreter_select; Block as_select_sample; /// For `view` type tables, you may need `sample_block` to get the columns. diff --git a/dbms/src/Parsers/ASTCreateQuery.h b/dbms/src/Parsers/ASTCreateQuery.h index f4e9720f98e..895b109174e 100644 --- a/dbms/src/Parsers/ASTCreateQuery.h +++ b/dbms/src/Parsers/ASTCreateQuery.h @@ -168,7 +168,8 @@ protected: if (!as_table.empty()) { - settings.ostr << (settings.hilite ? hilite_keyword : "") << " AS " << (settings.hilite ? hilite_none : "") + std::string what = (!is_materialized_view ? " AS " : " TO "); + settings.ostr << (settings.hilite ? hilite_keyword : "") << what << (settings.hilite ? hilite_none : "") << (!as_database.empty() ? backQuoteIfNeed(as_database) + "." : "") << backQuoteIfNeed(as_table); } diff --git a/dbms/src/Parsers/ParserCreateQuery.cpp b/dbms/src/Parsers/ParserCreateQuery.cpp index 20862243546..ce97297258a 100644 --- a/dbms/src/Parsers/ParserCreateQuery.cpp +++ b/dbms/src/Parsers/ParserCreateQuery.cpp @@ -219,6 +219,7 @@ bool ParserCreateQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) bool is_materialized_view = false; bool is_populate = false; bool is_temporary = false; + bool to_table = false; if (!s_create.ignore(pos, expected)) { @@ -254,6 +255,23 @@ bool ParserCreateQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) return false; } + // Shortcut for ATTACH a previously detached table + if (attach && (!pos.isValid() || pos.get().type == TokenType::Semicolon)) + { + auto query = std::make_shared(StringRange(begin, pos)); + node = query; + + query->attach = attach; + query->if_not_exists = if_not_exists; + + if (database) + query->database = typeid_cast(*database).name; + if (table) + query->table = typeid_cast(*table).name; + + return true; + } + /// List of columns. if (s_lparen.ignore(pos, expected)) { @@ -341,6 +359,22 @@ bool ParserCreateQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) return false; } + // TO [db.]table + if (ParserKeyword{"TO"}.ignore(pos, expected)) + { + to_table = true; + + if (!name_p.parse(pos, as_table, expected)) + return false; + + if (s_dot.ignore(pos, expected)) + { + as_database = as_table; + if (!name_p.parse(pos, as_table, expected)) + return false; + } + } + /// Optional - a list of columns can be specified. It must fully comply with SELECT. if (s_lparen.ignore(pos, expected)) { @@ -351,7 +385,7 @@ bool ParserCreateQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) return false; } - if (is_materialized_view) + if (is_materialized_view && !to_table) { /// Internal ENGINE for MATERIALIZED VIEW must be specified. if (!storage_p.parse(pos, storage, expected)) diff --git a/dbms/src/Parsers/ParserCreateQuery.h b/dbms/src/Parsers/ParserCreateQuery.h index b8fae5d57d7..4e5261ff8b6 100644 --- a/dbms/src/Parsers/ParserCreateQuery.h +++ b/dbms/src/Parsers/ParserCreateQuery.h @@ -214,7 +214,7 @@ protected: * CREATE|ATTACH DATABASE db [ENGINE = engine] * * Or: - * CREATE|ATTACH [MATERIALIZED] VIEW [IF NOT EXISTS] [db.]name [ENGINE = engine] [POPULATE] AS SELECT ... + * CREATE|ATTACH [MATERIALIZED] VIEW [IF NOT EXISTS] [db.]name [TO [db.]name] [ENGINE = engine] [POPULATE] AS SELECT ... */ class ParserCreateQuery : public IParserBase { diff --git a/dbms/src/Storages/StorageMaterializedView.cpp b/dbms/src/Storages/StorageMaterializedView.cpp index 0e90e85e793..220485fcf8b 100644 --- a/dbms/src/Storages/StorageMaterializedView.cpp +++ b/dbms/src/Storages/StorageMaterializedView.cpp @@ -70,8 +70,8 @@ StorageMaterializedView::StorageMaterializedView( if (!query.select) throw Exception("SELECT query is not specified for " + getName(), ErrorCodes::INCORRECT_QUERY); - if (!query.storage) - throw Exception("ENGINE of MaterializedView must be specified explicitly", ErrorCodes::INCORRECT_QUERY); + if (!query.storage && query.as_table.empty()) + throw Exception("ENGINE of MaterializedView should be specified explicitly", ErrorCodes::INCORRECT_QUERY); extractDependentTable(*query.select, select_database_name, select_table_name); @@ -80,16 +80,28 @@ StorageMaterializedView::StorageMaterializedView( DatabaseAndTableName(select_database_name, select_table_name), DatabaseAndTableName(database_name, table_name)); - String inner_table_name = getInnerTableName(); + // If the destination table is not set, use inner table + if (!create.inner_storage) + { + target_database_name = create.as_database; + target_table_name = create.as_table; + } + else + { + target_database_name = database_name; + target_table_name = ".inner." + table_name; + has_inner_table = true; + } + inner_query = query.select->ptr(); /// If there is an ATTACH request, then the internal table must already be connected. - if (!attach_) + if (!attach_ && has_inner_table) { /// We will create a query to create an internal table. auto manual_create_query = std::make_shared(); - manual_create_query->database = database_name; - manual_create_query->table = inner_table_name; + manual_create_query->database = target_database_name; + manual_create_query->table = target_table_name; manual_create_query->set(manual_create_query->columns, query.columns->ptr()); manual_create_query->set(manual_create_query->storage, query.storage->ptr()); @@ -114,12 +126,12 @@ StorageMaterializedView::StorageMaterializedView( NameAndTypePair StorageMaterializedView::getColumn(const String & column_name) const { - return getInnerTable()->getColumn(column_name); + return getTargetTable()->getColumn(column_name); } bool StorageMaterializedView::hasColumn(const String & column_name) const { - return getInnerTable()->hasColumn(column_name); + return getTargetTable()->hasColumn(column_name); } BlockInputStreams StorageMaterializedView::read( @@ -130,12 +142,12 @@ BlockInputStreams StorageMaterializedView::read( const size_t max_block_size, const unsigned num_streams) { - return getInnerTable()->read(column_names, query_info, context, processed_stage, max_block_size, num_streams); + return getTargetTable()->read(column_names, query_info, context, processed_stage, max_block_size, num_streams); } BlockOutputStreamPtr StorageMaterializedView::write(const ASTPtr & query, const Settings & settings) { - return getInnerTable()->write(query, settings); + return getTargetTable()->write(query, settings); } void StorageMaterializedView::drop() @@ -144,14 +156,12 @@ void StorageMaterializedView::drop() DatabaseAndTableName(select_database_name, select_table_name), DatabaseAndTableName(database_name, table_name)); - auto inner_table_name = getInnerTableName(); - - if (global_context.tryGetTable(database_name, inner_table_name)) + if (has_inner_table && global_context.tryGetTable(target_database_name, target_table_name)) { /// We create and execute `drop` query for internal table. auto drop_query = std::make_shared(); - drop_query->database = database_name; - drop_query->table = inner_table_name; + drop_query->database = target_database_name; + drop_query->table = target_table_name; ASTPtr ast_drop_query = drop_query; InterpreterDropQuery drop_interpreter(ast_drop_query, global_context); drop_interpreter.execute(); @@ -160,12 +170,12 @@ void StorageMaterializedView::drop() bool StorageMaterializedView::optimize(const ASTPtr & query, const ASTPtr & partition, bool final, bool deduplicate, const Context & context) { - return getInnerTable()->optimize(query, partition, final, deduplicate, context); + return getTargetTable()->optimize(query, partition, final, deduplicate, context); } -StoragePtr StorageMaterializedView::getInnerTable() const +StoragePtr StorageMaterializedView::getTargetTable() const { - return global_context.getTable(database_name, getInnerTableName()); + return global_context.getTable(target_database_name, target_table_name); } } diff --git a/dbms/src/Storages/StorageMaterializedView.h b/dbms/src/Storages/StorageMaterializedView.h index def8ef9bc02..c4a47e31671 100644 --- a/dbms/src/Storages/StorageMaterializedView.h +++ b/dbms/src/Storages/StorageMaterializedView.h @@ -20,18 +20,17 @@ public: std::string getName() const override { return "MaterializedView"; } std::string getTableName() const override { return table_name; } const NamesAndTypesList & getColumnsListImpl() const override { return *columns; } - std::string getInnerTableName() const { return ".inner." + table_name; } ASTPtr getInnerQuery() const { return inner_query->clone(); }; - StoragePtr getInnerTable() const; + StoragePtr getTargetTable() const; NameAndTypePair getColumn(const String & column_name) const override; bool hasColumn(const String & column_name) const override; - bool supportsSampling() const override { return getInnerTable()->supportsSampling(); } - bool supportsPrewhere() const override { return getInnerTable()->supportsPrewhere(); } - bool supportsFinal() const override { return getInnerTable()->supportsFinal(); } - bool supportsParallelReplicas() const override { return getInnerTable()->supportsParallelReplicas(); } - bool supportsIndexForIn() const override { return getInnerTable()->supportsIndexForIn(); } + bool supportsSampling() const override { return getTargetTable()->supportsSampling(); } + bool supportsPrewhere() const override { return getTargetTable()->supportsPrewhere(); } + bool supportsFinal() const override { return getTargetTable()->supportsFinal(); } + bool supportsParallelReplicas() const override { return getTargetTable()->supportsParallelReplicas(); } + bool supportsIndexForIn() const override { return getTargetTable()->supportsIndexForIn(); } BlockOutputStreamPtr write(const ASTPtr & query, const Settings & settings) override; void drop() override; @@ -48,11 +47,14 @@ public: private: String select_database_name; String select_table_name; + String target_database_name; + String target_table_name; String table_name; String database_name; ASTPtr inner_query; Context & global_context; NamesAndTypesListPtr columns; + bool has_inner_table = false; StorageMaterializedView( const String & table_name_, diff --git a/dbms/tests/queries/0_stateless/00508_materialized_view_to.reference b/dbms/tests/queries/0_stateless/00508_materialized_view_to.reference new file mode 100644 index 00000000000..099b7d91c92 --- /dev/null +++ b/dbms/tests/queries/0_stateless/00508_materialized_view_to.reference @@ -0,0 +1,4 @@ +1 +2 +1 +2 diff --git a/dbms/tests/queries/0_stateless/00508_materialized_view_to.sql b/dbms/tests/queries/0_stateless/00508_materialized_view_to.sql new file mode 100644 index 00000000000..d2a819ec623 --- /dev/null +++ b/dbms/tests/queries/0_stateless/00508_materialized_view_to.sql @@ -0,0 +1,23 @@ +DROP TABLE IF EXISTS test.src; +DROP TABLE IF EXISTS test.dst; +DROP TABLE IF EXISTS test.mv; + +CREATE TABLE test.src (x UInt8) ENGINE = Null; +CREATE TABLE test.dst (x UInt8) ENGINE = Memory(); + +CREATE MATERIALIZED VIEW test.mv TO test.dst AS SELECT * FROM test.src; +INSERT INTO test.src VALUES (1), (2); + +-- Detach MV and see if the data is still readable +DETACH TABLE test.mv; +SELECT * FROM test.dst; + +-- Reattach MV (shortcut) +ATTACH TABLE test.mv; + +-- Drop the MV and see if the data is still readable +DROP TABLE test.mv; +SELECT * FROM test.dst; + +DROP TABLE test.src; +DROP TABLE test.dst; diff --git a/docs/en/query_language/queries.rst b/docs/en/query_language/queries.rst index 49285dccc9c..b22eefa0702 100644 --- a/docs/en/query_language/queries.rst +++ b/docs/en/query_language/queries.rst @@ -107,7 +107,7 @@ At the moment, ``ALTER`` queries for replicated tables are not supported yet. CREATE VIEW ~~~~~~~~~~~ -``CREATE [MATERIALIZED] VIEW [IF NOT EXISTS] [db.]name [ENGINE = engine] [POPULATE] AS SELECT ...`` +``CREATE [MATERIALIZED] VIEW [IF NOT EXISTS] [db.]name [TO [db.]name] [ENGINE = engine] [POPULATE] AS SELECT ...`` Creates a view. There are two types of views: normal and MATERIALIZED. @@ -133,7 +133,7 @@ This query is fully equivalent to using the subquery: Materialized views store data transformed by the corresponding SELECT query. -When creating a materialized view, you have to specify ENGINE - the table engine for storing data. +When creating a materialized view, you have to either specify ENGINE - the table engine for storing data, or target table for materialized results. By default, it uses the same engine as for the table that the SELECT query is made from. A materialized view is arranged as follows: when inserting data to the table specified in SELECT, part of the inserted data is converted by this SELECT query, and the result is inserted in the view. @@ -142,6 +142,7 @@ If you specify POPULATE, the existing table data is inserted in the view when cr The SELECT query can contain DISTINCT, GROUP BY, ORDER BY, LIMIT ... Note that the corresponding conversions are performed independently on each block of inserted data. For example, if GROUP BY is set, data is aggregated during insertion, but only within a single packet of inserted data. The data won't be further aggregated. The exception is when using an ENGINE that independently performs data aggregation, such as SummingMergeTree. The execution of ALTER queries on materialized views has not been fully developed, so they might be inconvenient. +If the materialized view uses a ``TO [db.]name`` to specify a target table, it is possible to DETACH the view, ALTER the target table, and ATTACH the view again. Views look the same as normal tables. For example, they are listed in the result of the SHOW TABLES query. @@ -154,6 +155,12 @@ The query is exactly the same as CREATE, except - The query doesn't create data on the disk, but assumes that data is already in the appropriate places, and just adds information about the table to the server. After executing an ATTACH query, the server will know about the existence of the table. +If the table has been previously detached and it's structure is known, it's possible to use shorthand form and omit structure definition: + +.. code-block:: sql + + ATTACH TABLE [IF NOT EXISTS] [db.]name + This query is used when starting the server. The server stores table metadata as files with ATTACH queries, which it simply runs at launch (with the exception of system tables, which are explicitly created on the server). DROP