From 975a7ada424627448212ba83242292629c26c1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Vavrus=CC=8Ca?= Date: Sat, 21 Oct 2017 13:08:49 -0700 Subject: [PATCH 1/3] StorageMaterializedView: allow CREATE MATERIALIZED VIEW x TO y This allows creation of materialized views without inner tables, using an existing table for materialized rows instead. This is useful for cases when you want to detach the materializing view, but keep the already materialized data readable, especially when the inner table is replicated. --- dbms/src/Databases/DatabasesCommon.cpp | 15 ++++++- .../Interpreters/InterpreterCreateQuery.cpp | 12 ++--- dbms/src/Parsers/ASTCreateQuery.h | 3 +- dbms/src/Parsers/ParserCreateQuery.cpp | 20 ++++++++- dbms/src/Parsers/ParserCreateQuery.h | 2 +- dbms/src/Storages/StorageMaterializedView.cpp | 44 ++++++++++++------- dbms/src/Storages/StorageMaterializedView.h | 16 ++++--- .../00508_materialized_view_to.reference | 2 + .../00508_materialized_view_to.sql | 16 +++++++ docs/en/query_language/queries.rst | 5 ++- 10 files changed, 98 insertions(+), 37 deletions(-) create mode 100644 dbms/tests/queries/0_stateless/00508_materialized_view_to.reference create mode 100644 dbms/tests/queries/0_stateless/00508_materialized_view_to.sql diff --git a/dbms/src/Databases/DatabasesCommon.cpp b/dbms/src/Databases/DatabasesCommon.cpp index 6e5945554db..3eae0fec6cd 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; @@ -33,6 +31,13 @@ String getTableDefinitionFromCreateQuery(const ASTPtr & query) if (engine != "View" && engine != "MaterializedView") 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'; @@ -58,6 +63,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"); InterpreterCreateQuery::ColumnsInfo columns_info = InterpreterCreateQuery::getColumnsInfo(ast_create_query.columns, context); @@ -68,7 +75,11 @@ std::pair createTableFromDefinition( else if (ast_create_query.is_materialized_view) storage_name = "MaterializedView"; else + { + if (!ast_create_query.storage) + throw Exception("Missing ENGINE definition"); storage_name = typeid_cast(*ast_create_query.storage).name; + } return { diff --git a/dbms/src/Interpreters/InterpreterCreateQuery.cpp b/dbms/src/Interpreters/InterpreterCreateQuery.cpp index 6d570ad09d4..176ac41eedb 100644 --- a/dbms/src/Interpreters/InterpreterCreateQuery.cpp +++ b/dbms/src/Interpreters/InterpreterCreateQuery.cpp @@ -438,6 +438,12 @@ String InterpreterCreateQuery::setEngine( { storage_name = typeid_cast(*create.storage).name; } + 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. @@ -460,12 +466,6 @@ String InterpreterCreateQuery::setEngine( else storage_name = as_storage->getName(); } - 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 throw Exception("Incorrect CREATE query: required ENGINE.", ErrorCodes::ENGINE_REQUIRED); diff --git a/dbms/src/Parsers/ASTCreateQuery.h b/dbms/src/Parsers/ASTCreateQuery.h index 209c2fb9b0f..65116067a3d 100644 --- a/dbms/src/Parsers/ASTCreateQuery.h +++ b/dbms/src/Parsers/ASTCreateQuery.h @@ -105,7 +105,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 25037bd67b9..a467b83a1e3 100644 --- a/dbms/src/Parsers/ParserCreateQuery.cpp +++ b/dbms/src/Parsers/ParserCreateQuery.cpp @@ -162,6 +162,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)) { @@ -304,6 +305,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)) { @@ -315,7 +332,8 @@ bool ParserCreateQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) } /// Optional - internal ENGINE for MATERIALIZED VIEW can be specified - engine_p.parse(pos, inner_storage, expected); + if (!to_table) + engine_p.parse(pos, inner_storage, expected); if (s_populate.ignore(pos, expected)) is_populate = true; diff --git a/dbms/src/Parsers/ParserCreateQuery.h b/dbms/src/Parsers/ParserCreateQuery.h index 3aa78f9cc5c..6478bd37abe 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 c59b84125f8..b701e7dfdf4 100644 --- a/dbms/src/Storages/StorageMaterializedView.cpp +++ b/dbms/src/Storages/StorageMaterializedView.cpp @@ -71,7 +71,7 @@ StorageMaterializedView::StorageMaterializedView( if (!create.select) throw Exception("SELECT query is not specified for " + getName(), ErrorCodes::INCORRECT_QUERY); - if (!create.inner_storage) + if (!create.inner_storage && create.as_table.empty()) throw Exception("ENGINE of MaterializedView should be specified explicitly", ErrorCodes::INCORRECT_QUERY); ASTSelectQuery & select = typeid_cast(*create.select); @@ -86,16 +86,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 = create.select; /// 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->columns = create.columns; manual_create_query->children.push_back(manual_create_query->columns); manual_create_query->storage = create.inner_storage; @@ -122,12 +134,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( @@ -138,12 +150,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() @@ -152,14 +164,12 @@ void StorageMaterializedView::drop() DatabaseAndTableName(select_database_name, select_table_name), DatabaseAndTableName(database_name, table_name)); - auto inner_table_name = getInnerTableName(); - - if (context.tryGetTable(database_name, inner_table_name)) + if (has_inner_table && 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, context); drop_interpreter.execute(); @@ -168,12 +178,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 context.getTable(database_name, getInnerTableName()); + return context.getTable(target_database_name, target_table_name); } diff --git a/dbms/src/Storages/StorageMaterializedView.h b/dbms/src/Storages/StorageMaterializedView.h index efc6927e26e..7ba9dfe7f71 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 & 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..1191247b6d9 --- /dev/null +++ b/dbms/tests/queries/0_stateless/00508_materialized_view_to.reference @@ -0,0 +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..70f8b98d32b --- /dev/null +++ b/dbms/tests/queries/0_stateless/00508_materialized_view_to.sql @@ -0,0 +1,16 @@ +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); + +-- 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 25894a0c81c..d0bded7c604 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 can specify ENGINE - the table engine for storing data. By default, it uses the same engine as for the table that the SELECT query is made from. +When creating a materialized view, you can 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. From 5f53df7dbe2778b5dc71ac938973e8b1c1a605ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Vavrus=CC=8Ca?= Date: Sat, 21 Oct 2017 13:38:39 -0700 Subject: [PATCH 2/3] ParserCreateQuery: allow ATTACH TABLE x shorthand statement Allow `ATTACH TABLE [db.]name` if the table was previously detached, an the table structure can be read from disk. This makes reattaching tables less cumbersome: ``` CREATE TABLE test.t (x UInt8) ENGINE = Null; DETACH TABLE test.t; ATTACH TABLE test.t; ``` --- .../src/Interpreters/InterpreterCreateQuery.cpp | 9 +++++++++ dbms/src/Parsers/ParserCreateQuery.cpp | 17 +++++++++++++++++ .../00508_materialized_view_to.reference | 2 ++ .../0_stateless/00508_materialized_view_to.sql | 7 +++++++ docs/en/query_language/queries.rst | 6 ++++++ 5 files changed, 41 insertions(+) diff --git a/dbms/src/Interpreters/InterpreterCreateQuery.cpp b/dbms/src/Interpreters/InterpreterCreateQuery.cpp index 176ac41eedb..9469506f9d4 100644 --- a/dbms/src/Interpreters/InterpreterCreateQuery.cpp +++ b/dbms/src/Interpreters/InterpreterCreateQuery.cpp @@ -489,6 +489,15 @@ 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) + { + 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/ParserCreateQuery.cpp b/dbms/src/Parsers/ParserCreateQuery.cpp index a467b83a1e3..bdd72198213 100644 --- a/dbms/src/Parsers/ParserCreateQuery.cpp +++ b/dbms/src/Parsers/ParserCreateQuery.cpp @@ -214,6 +214,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)) { diff --git a/dbms/tests/queries/0_stateless/00508_materialized_view_to.reference b/dbms/tests/queries/0_stateless/00508_materialized_view_to.reference index 1191247b6d9..099b7d91c92 100644 --- a/dbms/tests/queries/0_stateless/00508_materialized_view_to.reference +++ b/dbms/tests/queries/0_stateless/00508_materialized_view_to.reference @@ -1,2 +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 index 70f8b98d32b..d2a819ec623 100644 --- a/dbms/tests/queries/0_stateless/00508_materialized_view_to.sql +++ b/dbms/tests/queries/0_stateless/00508_materialized_view_to.sql @@ -8,6 +8,13 @@ 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; diff --git a/docs/en/query_language/queries.rst b/docs/en/query_language/queries.rst index d0bded7c604..8c07866db1c 100644 --- a/docs/en/query_language/queries.rst +++ b/docs/en/query_language/queries.rst @@ -155,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 From d942ca0c0b2f206ea80860f973271a065ecdac47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Vavru=C5=A1a?= Date: Tue, 24 Oct 2017 17:55:31 -0700 Subject: [PATCH 3/3] Updated exceptions and added comments. --- dbms/src/Databases/DatabasesCommon.cpp | 4 ++-- dbms/src/Interpreters/InterpreterCreateQuery.cpp | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dbms/src/Databases/DatabasesCommon.cpp b/dbms/src/Databases/DatabasesCommon.cpp index d7ab80a3a91..7012b0c170c 100644 --- a/dbms/src/Databases/DatabasesCommon.cpp +++ b/dbms/src/Databases/DatabasesCommon.cpp @@ -64,7 +64,7 @@ std::pair createTableFromDefinition( /// - 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"); + throw Exception("Missing definition of columns.", ErrorCodes::EMPTY_LIST_OF_COLUMNS_PASSED); InterpreterCreateQuery::ColumnsInfo columns_info = InterpreterCreateQuery::getColumnsInfo(*ast_create_query.columns, context); @@ -77,7 +77,7 @@ std::pair createTableFromDefinition( else { if (!ast_create_query.storage) - throw Exception("Missing ENGINE definition"); + throw Exception("Missing ENGINE definition.", ErrorCodes::ENGINE_REQUIRED); storage_name = ast_create_query.storage->engine->name; } diff --git a/dbms/src/Interpreters/InterpreterCreateQuery.cpp b/dbms/src/Interpreters/InterpreterCreateQuery.cpp index 59d90b8f24e..0931c12837e 100644 --- a/dbms/src/Interpreters/InterpreterCreateQuery.cpp +++ b/dbms/src/Interpreters/InterpreterCreateQuery.cpp @@ -486,6 +486,7 @@ BlockIO InterpreterCreateQuery::createTable(ASTCreateQuery & create) // 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