diff --git a/create-postgres-tables.sh b/create-postgres-tables.sh index 408f292f..84e72005 100755 --- a/create-postgres-tables.sh +++ b/create-postgres-tables.sh @@ -1,4 +1,12 @@ #!/bin/bash + +# Set default value for the build type +BUILD_TYPE="release" +# If an argument is provided, use that as the build type instead +if [ $# -eq 1 ]; then + BUILD_TYPE=$1 +fi + echo " CREATE SCHEMA tpch; CREATE SCHEMA tpcds; @@ -6,7 +14,7 @@ CALL dbgen(sf=0.01, schema='tpch'); CALL dsdgen(sf=0.01, schema='tpcds'); EXPORT DATABASE '/tmp/postgresscannertmp'; " | \ -./build/release/duckdb +./build/$BUILD_TYPE/duckdb dropdb --if-exists postgresscanner createdb postgresscanner diff --git a/src/include/storage/postgres_create_info.hpp b/src/include/storage/postgres_create_info.hpp new file mode 100644 index 00000000..73793ab5 --- /dev/null +++ b/src/include/storage/postgres_create_info.hpp @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// storage/postgres_create_info.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/catalog/catalog_entry/table_catalog_entry.hpp" +#include "duckdb/parser/parsed_data/create_table_info.hpp" +#include "postgres_utils.hpp" + +namespace duckdb { + +enum class PostgresCreateInfoType : uint8_t { TABLE, VIEW }; + +struct PostgresCreateInfo { +public: + PostgresCreateInfo(PostgresCreateInfoType type) : type(type) { + } + virtual ~PostgresCreateInfo() { + } + +public: + virtual CreateInfo &GetCreateInfo() = 0; + virtual const string &GetName() const = 0; + virtual void AddColumn(ColumnDefinition def, PostgresType pg_type, const string &pg_name) = 0; + virtual void GetColumnNamesAndTypes(vector &names, vector &types) = 0; + virtual idx_t PhysicalColumnCount() const = 0; + virtual void AddConstraint(unique_ptr constraint) = 0; + PostgresCreateInfoType GetType() const { + return type; + } + +public: + template + TARGET &Cast() { + if (type != TARGET::TYPE) { + throw InternalException("Failed to cast PostgresCreateInfo to type - PostgresCreateInfoType mismatch"); + } + return reinterpret_cast(*this); + } + + template + const TARGET &Cast() const { + if (type != TARGET::TYPE) { + throw InternalException("Failed to cast PostgresCreateInfo to type - PostgresCreateInfoType mismatch"); + } + return reinterpret_cast(*this); + } + +public: + idx_t approx_num_pages = 0; + vector postgres_types; + vector postgres_names; + +protected: + PostgresCreateInfoType type; +}; + +} // namespace duckdb diff --git a/src/include/storage/postgres_table_entry.hpp b/src/include/storage/postgres_table_entry.hpp index d96dfad8..1cf0158a 100644 --- a/src/include/storage/postgres_table_entry.hpp +++ b/src/include/storage/postgres_table_entry.hpp @@ -10,38 +10,69 @@ #include "duckdb/catalog/catalog_entry/table_catalog_entry.hpp" #include "duckdb/parser/parsed_data/create_table_info.hpp" +#include "storage/postgres_create_info.hpp" #include "postgres_utils.hpp" namespace duckdb { -struct PostgresTableInfo { - PostgresTableInfo() { +struct PostgresTableInfo : public PostgresCreateInfo { +public: + static constexpr const PostgresCreateInfoType TYPE = PostgresCreateInfoType::TABLE; + +public: + PostgresTableInfo() : PostgresCreateInfo(TYPE) { create_info = make_uniq(); create_info->columns.SetAllowDuplicates(true); } - PostgresTableInfo(const string &schema, const string &table) { + PostgresTableInfo(const string &schema, const string &table) : PostgresCreateInfo(TYPE) { create_info = make_uniq(string(), schema, table); create_info->columns.SetAllowDuplicates(true); } - PostgresTableInfo(const SchemaCatalogEntry &schema, const string &table) { + PostgresTableInfo(const SchemaCatalogEntry &schema, const string &table) : PostgresCreateInfo(TYPE) { create_info = make_uniq((SchemaCatalogEntry &)schema, table); create_info->columns.SetAllowDuplicates(true); } + ~PostgresTableInfo() override { + } - const string &GetTableName() const { +public: + CreateInfo &GetCreateInfo() override { + return *create_info; + } + + const string &GetName() const override { return create_info->table; } + void AddColumn(ColumnDefinition def, PostgresType pg_type, const string &pg_name) override { + postgres_types.push_back(std::move(pg_type)); + D_ASSERT(!pg_name.empty()); + postgres_names.push_back(pg_name); + create_info->columns.AddColumn(std::move(def)); + } + void GetColumnNamesAndTypes(vector &names, vector &types) override { + for (auto &col : create_info->columns.Logical()) { + names.push_back(col.GetName()); + types.push_back(col.GetType()); + } + } + idx_t PhysicalColumnCount() const override { + return create_info->columns.PhysicalColumnCount(); + } + + void AddConstraint(unique_ptr constraint) override { + create_info->constraints.push_back(std::move(constraint)); + } + +public: unique_ptr create_info; - vector postgres_types; - vector postgres_names; - idx_t approx_num_pages = 0; }; class PostgresTableEntry : public TableCatalogEntry { public: PostgresTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, CreateTableInfo &info); PostgresTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, PostgresTableInfo &info); + ~PostgresTableEntry() override; public: unique_ptr GetStatistics(ClientContext &context, column_t column_id) override; @@ -61,7 +92,7 @@ class PostgresTableEntry : public TableCatalogEntry { vector postgres_types; //! Column names as they are within Postgres //! We track these separately because of case sensitivity - Postgres allows e.g. the columns "ID" and "id" together - //! We would in this case remap them to "ID" and "id:1", while postgres_names store the original names + //! We would in this case remap them to "ID" and "id_1", while postgres_names store the original names vector postgres_names; //! The approximate number of pages a table consumes in Postgres idx_t approx_num_pages; diff --git a/src/include/storage/postgres_table_set.hpp b/src/include/storage/postgres_table_set.hpp index 3d72efd0..de89925e 100644 --- a/src/include/storage/postgres_table_set.hpp +++ b/src/include/storage/postgres_table_set.hpp @@ -24,10 +24,10 @@ class PostgresTableSet : public PostgresInSchemaSet { public: optional_ptr CreateTable(ClientContext &context, BoundCreateTableInfo &info); - static unique_ptr GetTableInfo(PostgresTransaction &transaction, PostgresSchemaEntry &schema, - const string &table_name); - static unique_ptr GetTableInfo(PostgresConnection &connection, const string &schema_name, - const string &table_name); + static unique_ptr GetTableInfo(PostgresTransaction &transaction, PostgresSchemaEntry &schema, + const string &table_name); + static unique_ptr GetTableInfo(PostgresConnection &connection, const string &schema_name, + const string &table_name); optional_ptr ReloadEntry(ClientContext &context, const string &table_name) override; void AlterTable(ClientContext &context, AlterTableInfo &info); @@ -46,11 +46,11 @@ class PostgresTableSet : public PostgresInSchemaSet { void AlterTable(ClientContext &context, RemoveColumnInfo &info); static void AddColumn(optional_ptr transaction, optional_ptr schema, - PostgresResult &result, idx_t row, PostgresTableInfo &table_info); + PostgresResult &result, idx_t row, PostgresCreateInfo &pg_create_info); static void AddConstraint(PostgresResult &result, idx_t row, PostgresTableInfo &table_info); static void AddColumnOrConstraint(optional_ptr transaction, optional_ptr schema, PostgresResult &result, idx_t row, - PostgresTableInfo &table_info); + PostgresCreateInfo &table_info); void CreateEntries(PostgresTransaction &transaction, PostgresResult &result, idx_t start, idx_t end); diff --git a/src/include/storage/postgres_view_entry.hpp b/src/include/storage/postgres_view_entry.hpp new file mode 100644 index 00000000..9e157441 --- /dev/null +++ b/src/include/storage/postgres_view_entry.hpp @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// DuckDB +// +// storage/postgres_view_entry.hpp +// +// +//===----------------------------------------------------------------------===// + +#pragma once + +#include "duckdb/catalog/catalog_entry/view_catalog_entry.hpp" +#include "duckdb/parser/parsed_data/create_view_info.hpp" +#include "storage/postgres_create_info.hpp" + +namespace duckdb { + +struct PostgresViewInfo : public PostgresCreateInfo { +public: + static constexpr const PostgresCreateInfoType TYPE = PostgresCreateInfoType::VIEW; + +public: + PostgresViewInfo(const string &select_stmt) : PostgresCreateInfo(TYPE) { + create_info = make_uniq(); + create_info->query = CreateViewInfo::ParseSelect(select_stmt); + // create_info->columns.SetAllowDuplicates(true); + } + PostgresViewInfo(const string &schema, const string &view, const string &select_stmt) : PostgresCreateInfo(TYPE) { + create_info = make_uniq(string(), schema, view); + create_info->query = CreateViewInfo::ParseSelect(select_stmt); + // create_info->columns.SetAllowDuplicates(true); + } + PostgresViewInfo(const SchemaCatalogEntry &schema, const string &view, const string &select_stmt) + : PostgresCreateInfo(TYPE) { + create_info = make_uniq((SchemaCatalogEntry &)schema, view); + create_info->query = CreateViewInfo::ParseSelect(select_stmt); + // create_info->columns.SetAllowDuplicates(true); + } + ~PostgresViewInfo() override { + } + +public: + const string &GetName() const override { + return create_info->view_name; + } + + CreateInfo &GetCreateInfo() override { + return *create_info; + } + + void AddColumn(ColumnDefinition def, PostgresType pg_type, const string &pg_name) override { + postgres_types.push_back(std::move(pg_type)); + D_ASSERT(!pg_name.empty()); + postgres_names.push_back(pg_name); + create_info->types.push_back(def.Type()); + create_info->names.push_back(def.Name()); + } + + void GetColumnNamesAndTypes(vector &names, vector &types) override { + names = create_info->names; + types = create_info->types; + } + idx_t PhysicalColumnCount() const override { + D_ASSERT(create_info->types.size() == create_info->names.size()); + return create_info->types.size(); + } + void AddConstraint(unique_ptr constraint) override { + throw NotImplementedException("Can't create constraints on a VIEW entry"); + } + +public: + unique_ptr create_info; +}; + +class PostgresViewEntry : public ViewCatalogEntry { +public: + PostgresViewEntry(Catalog &catalog, SchemaCatalogEntry &schema, CreateViewInfo &info); + PostgresViewEntry(Catalog &catalog, SchemaCatalogEntry &schema, PostgresViewInfo &info); + ~PostgresViewEntry() override; + +public: + //! Postgres type annotations + vector postgres_types; + //! Column names as they are within Postgres + //! We track these separately because of case sensitivity - Postgres allows e.g. the columns "ID" and "id" together + //! We would in this case remap them to "ID" and "id_1", while postgres_names store the original names + vector postgres_names; + //! The approximate number of pages a table consumes in Postgres + idx_t approx_num_pages; +}; + +} // namespace duckdb diff --git a/src/postgres_scanner.cpp b/src/postgres_scanner.cpp index 449df0bc..bf0d8460 100644 --- a/src/postgres_scanner.cpp +++ b/src/postgres_scanner.cpp @@ -162,10 +162,7 @@ static unique_ptr PostgresBind(ClientContext &context, TableFuncti auto info = PostgresTableSet::GetTableInfo(con, bind_data->schema_name, bind_data->table_name); bind_data->postgres_types = info->postgres_types; - for (auto &col : info->create_info->columns.Logical()) { - names.push_back(col.GetName()); - return_types.push_back(col.GetType()); - } + info->GetColumnNamesAndTypes(names, return_types); bind_data->names = info->postgres_names; bind_data->types = return_types; bind_data->can_use_main_thread = true; diff --git a/src/storage/CMakeLists.txt b/src/storage/CMakeLists.txt index e6def351..38f354e8 100644 --- a/src/storage/CMakeLists.txt +++ b/src/storage/CMakeLists.txt @@ -18,7 +18,9 @@ add_library( postgres_transaction_manager.cpp postgres_type_entry.cpp postgres_type_set.cpp - postgres_update.cpp) + postgres_update.cpp + postgres_view_entry.cpp + ) set(ALL_OBJECT_FILES ${ALL_OBJECT_FILES} $ PARENT_SCOPE) diff --git a/src/storage/postgres_index_set.cpp b/src/storage/postgres_index_set.cpp index a886f5a1..d869c5a7 100644 --- a/src/storage/postgres_index_set.cpp +++ b/src/storage/postgres_index_set.cpp @@ -27,6 +27,7 @@ void PostgresIndexSet::LoadEntries(ClientContext &context) { auto &result = index_result->GetResult(); for (idx_t row = index_result->start; row < index_result->end; row++) { auto table_name = result.GetString(row, 1); + D_ASSERT(!table_name.empty()); auto index_name = result.GetString(row, 2); CreateIndexInfo info; info.schema = schema.name; diff --git a/src/storage/postgres_table_entry.cpp b/src/storage/postgres_table_entry.cpp index d791678b..0ea7e2e3 100644 --- a/src/storage/postgres_table_entry.cpp +++ b/src/storage/postgres_table_entry.cpp @@ -15,6 +15,7 @@ PostgresTableEntry::PostgresTableEntry(Catalog &catalog, SchemaCatalogEntry &sch col.TypeMutable() = PostgresUtils::RemoveAlias(col.GetType()); } postgres_types.push_back(PostgresUtils::CreateEmptyPostgresType(col.GetType())); + D_ASSERT(!col.GetName().empty()); postgres_names.push_back(col.GetName()); } approx_num_pages = 0; @@ -23,6 +24,11 @@ PostgresTableEntry::PostgresTableEntry(Catalog &catalog, SchemaCatalogEntry &sch PostgresTableEntry::PostgresTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, PostgresTableInfo &info) : TableCatalogEntry(catalog, schema, *info.create_info), postgres_types(std::move(info.postgres_types)), postgres_names(std::move(info.postgres_names)) { +#ifdef DEBUG + for (auto &result_name : postgres_names) { + D_ASSERT(!result_name.empty()); + } +#endif D_ASSERT(postgres_types.size() == columns.LogicalColumnCount()); approx_num_pages = info.approx_num_pages; } @@ -48,6 +54,11 @@ TableFunction PostgresTableEntry::GetScanFunction(ClientContext &context, unique result->types.push_back(col.GetType()); } result->names = postgres_names; +#ifdef DEBUG + for (auto &result_name : result->names) { + D_ASSERT(!result_name.empty()); + } +#endif result->postgres_types = postgres_types; result->read_only = transaction.IsReadOnly(); PostgresScanFunction::PrepareBind(pg_catalog.GetPostgresVersion(), context, *result, approx_num_pages); @@ -120,4 +131,7 @@ PostgresCopyFormat PostgresTableEntry::GetCopyFormat(ClientContext &context) { return PostgresCopyFormat::BINARY; } +PostgresTableEntry::~PostgresTableEntry() { +} + } // namespace duckdb diff --git a/src/storage/postgres_table_set.cpp b/src/storage/postgres_table_set.cpp index 95d6048b..d41893c6 100644 --- a/src/storage/postgres_table_set.cpp +++ b/src/storage/postgres_table_set.cpp @@ -1,14 +1,16 @@ #include "storage/postgres_table_set.hpp" #include "storage/postgres_transaction.hpp" +#include "storage/postgres_table_entry.hpp" +#include "storage/postgres_view_entry.hpp" +#include "storage/postgres_schema_entry.hpp" #include "duckdb/parser/parsed_data/create_table_info.hpp" #include "duckdb/parser/constraints/not_null_constraint.hpp" #include "duckdb/parser/constraints/unique_constraint.hpp" #include "duckdb/parser/expression/constant_expression.hpp" #include "duckdb/planner/parsed_data/bound_create_table_info.hpp" #include "duckdb/catalog/dependency_list.hpp" -#include "duckdb/parser/parsed_data/create_table_info.hpp" +#include "duckdb/parser/parsed_data/create_view_info.hpp" #include "duckdb/parser/constraints/list.hpp" -#include "storage/postgres_schema_entry.hpp" #include "duckdb/parser/parser.hpp" #include "duckdb/common/string_util.hpp" #include "postgres_conversion.hpp" @@ -19,22 +21,53 @@ PostgresTableSet::PostgresTableSet(PostgresSchemaEntry &schema, unique_ptr 0 AND relkind IN ('r', 'v', 'm', 'f', 'p') ${CONDITION} UNION ALL -SELECT pg_namespace.oid AS namespace_id, relname, NULL relpages, NULL attname, NULL type_name, - NULL type_modifier, NULL ndim, NULL attnum, NULL AS notnull, - pg_constraint.oid AS constraint_id, contype AS constraint_type, - conkey AS constraint_key +SELECT + pg_namespace.oid AS namespace_id, + relname, + NULL as relkind, + NULL as view_definition, + NULL relpages, + NULL attname, + NULL type_name, + NULL type_modifier, + NULL ndim, + NULL attnum, + NULL AS notnull, + pg_constraint.oid AS constraint_id, + contype AS constraint_type, + conkey AS constraint_key FROM pg_class JOIN pg_namespace ON relnamespace = pg_namespace.oid JOIN pg_constraint ON (pg_class.oid=pg_constraint.conrelid) @@ -42,8 +75,9 @@ WHERE contype IN ('p', 'u') ${CONDITION} ORDER BY namespace_id, relname, attnum, constraint_id; )"; string condition; + // FIXME: use StringUtil::Join(...) with 'AND ' as separator if (!schema.empty()) { - condition += "AND pg_namespace.nspname=" + KeywordHelper::WriteQuoted(schema); + condition += "AND pg_namespace.nspname=" + KeywordHelper::WriteQuoted(schema) + " "; } if (!table.empty()) { condition += "AND relname=" + KeywordHelper::WriteQuoted(table); @@ -53,21 +87,19 @@ ORDER BY namespace_id, relname, attnum, constraint_id; void PostgresTableSet::AddColumn(optional_ptr transaction, optional_ptr schema, PostgresResult &result, idx_t row, - PostgresTableInfo &table_info) { + PostgresCreateInfo &pg_create_info) { PostgresTypeData type_info; - idx_t column_index = 3; - auto column_name = result.GetString(row, column_index); - type_info.type_name = result.GetString(row, column_index + 1); - type_info.type_modifier = result.GetInt64(row, column_index + 2); - type_info.array_dimensions = result.GetInt64(row, column_index + 3); - bool is_not_null = result.GetBool(row, column_index + 5); + auto column_name = result.GetString(row, COLUMN_OFFSET); + D_ASSERT(!column_name.empty()); + type_info.type_name = result.GetString(row, COLUMN_OFFSET + 1); + type_info.type_modifier = result.GetInt64(row, COLUMN_OFFSET + 2); + type_info.array_dimensions = result.GetInt64(row, COLUMN_OFFSET + 3); + bool is_not_null = result.GetBool(row, COLUMN_OFFSET + 5); string default_value; PostgresType postgres_type; auto column_type = PostgresUtils::TypeToLogicalType(transaction, schema, type_info, postgres_type); - table_info.postgres_types.push_back(std::move(postgres_type)); - table_info.postgres_names.push_back(column_name); - ColumnDefinition column(std::move(column_name), std::move(column_type)); + ColumnDefinition column(column_name, std::move(column_type)); if (!default_value.empty()) { auto expressions = Parser::ParseExpressionList(default_value); if (expressions.empty()) { @@ -75,68 +107,121 @@ void PostgresTableSet::AddColumn(optional_ptr transaction, } column.SetDefaultValue(std::move(expressions[0])); } - auto &create_info = *table_info.create_info; if (is_not_null) { - create_info.constraints.push_back( - make_uniq(LogicalIndex(create_info.columns.PhysicalColumnCount()))); + pg_create_info.AddConstraint(make_uniq(LogicalIndex(pg_create_info.PhysicalColumnCount()))); } - create_info.columns.AddColumn(std::move(column)); + pg_create_info.AddColumn(std::move(column), std::move(postgres_type), std::move(column_name)); +} + +static CatalogType TransformRelKind(const string &relkind) { + if (relkind == "v") { + return CatalogType::VIEW_ENTRY; + } + if (relkind == "r") { + return CatalogType::TABLE_ENTRY; + } + if (relkind == "m") { + // TODO: support materialized views + return CatalogType::TABLE_ENTRY; + } + if (relkind == "f") { + // TODO: support foreign tables + return CatalogType::TABLE_ENTRY; + } + if (relkind == "p") { + // TODO: support partitioned tables + return CatalogType::TABLE_ENTRY; + } + throw InternalException("Unexpected relkind in TransformRelkind: %s", relkind); } void PostgresTableSet::AddConstraint(PostgresResult &result, idx_t row, PostgresTableInfo &table_info) { - idx_t column_index = 9; - auto constraint_type = result.GetString(row, column_index + 1); - auto constraint_key = result.GetString(row, column_index + 2); + auto constraint_type = result.GetString(row, CONSTRAINT_OFFSET + 1); + auto constraint_key = result.GetString(row, CONSTRAINT_OFFSET + 2); if (constraint_key.empty() || constraint_key.front() != '{' || constraint_key.back() != '}') { // invalid constraint key D_ASSERT(0); return; } - auto &create_info = *table_info.create_info; + auto &create_info = table_info.GetCreateInfo(); + auto &create_table_info = create_info.Cast(); auto splits = StringUtil::Split(constraint_key.substr(1, constraint_key.size() - 2), ","); vector columns; for (auto &split : splits) { auto index = std::stoull(split); - columns.push_back(create_info.columns.GetColumn(LogicalIndex(index - 1)).Name()); + columns.push_back(create_table_info.columns.GetColumn(LogicalIndex(index - 1)).Name()); } - create_info.constraints.push_back(make_uniq(std::move(columns), constraint_type == "p")); + create_table_info.constraints.push_back(make_uniq(std::move(columns), constraint_type == "p")); } void PostgresTableSet::AddColumnOrConstraint(optional_ptr transaction, optional_ptr schema, PostgresResult &result, - idx_t row, PostgresTableInfo &table_info) { - if (result.IsNull(row, 3)) { + idx_t row, PostgresCreateInfo &info) { + if (result.IsNull(row, COLUMN_OFFSET)) { // constraint + if (info.GetType() != PostgresCreateInfoType::TABLE) { + throw NotImplementedException("Can not add constraints to views!"); + } + auto &table_info = info.Cast(); AddConstraint(result, row, table_info); } else { - AddColumn(transaction, schema, result, row, table_info); + AddColumn(transaction, schema, result, row, info); } } void PostgresTableSet::CreateEntries(PostgresTransaction &transaction, PostgresResult &result, idx_t start, idx_t end) { - vector> tables; - unique_ptr info; + vector> infos; + unique_ptr info; for (idx_t row = start; row < end; row++) { - auto table_name = result.GetString(row, 1); - if (!info || info->GetTableName() != table_name) { + auto relname = result.GetString(row, REL_NAME_ID); + D_ASSERT(!relname.empty()); + auto relkind = result.GetString(row, REL_KIND_ID); + auto view_definition = result.GetString(row, VIEW_DEFINITION_ID); + auto approx_num_pages = result.GetInt64(row, APPROX_NUM_PAGES_ID); + if (!info || info->GetName() != relname) { if (info) { - tables.push_back(std::move(info)); + infos.push_back(std::move(info)); + } + auto catalog_type = TransformRelKind(relkind); + switch (catalog_type) { + case CatalogType::TABLE_ENTRY: + info = make_uniq(schema, relname); + break; + case CatalogType::VIEW_ENTRY: + info = make_uniq(schema, relname, view_definition); + break; + default: { + throw InternalException("Unexpected CatalogType in CreateEntries: %s", + CatalogTypeToString(catalog_type)); + } + auto approx_num_pages = + result.IsNull(row, APPROX_NUM_PAGES_ID) ? 0 : result.GetInt64(row, APPROX_NUM_PAGES_ID); + info->approx_num_pages = approx_num_pages; } - auto approx_num_pages = result.IsNull(row, 2) ? 0 : result.GetInt64(row, 2); - info = make_uniq(schema, table_name); - info->approx_num_pages = approx_num_pages; } AddColumnOrConstraint(&transaction, &schema, result, row, *info); } if (info) { - tables.push_back(std::move(info)); + infos.push_back(std::move(info)); } - for (auto &tbl_info : tables) { - auto table_entry = make_uniq(catalog, schema, *tbl_info); - CreateEntry(std::move(table_entry)); + for (auto &pg_create_info : infos) { + auto &create_info = pg_create_info->GetCreateInfo(); + unique_ptr catalog_entry; + switch (create_info.type) { + case CatalogType::TABLE_ENTRY: + catalog_entry = make_uniq(catalog, schema, pg_create_info->Cast()); + break; + case CatalogType::VIEW_ENTRY: + catalog_entry = make_uniq(catalog, schema, pg_create_info->Cast()); + break; + default: { + throw InternalException("Unexpected CatalogType in CreateInfo: %s", CatalogTypeToString(create_info.type)); + } + } + CreateEntry(std::move(catalog_entry)); } } @@ -155,48 +240,95 @@ void PostgresTableSet::LoadEntries(ClientContext &context) { } } -unique_ptr PostgresTableSet::GetTableInfo(PostgresTransaction &transaction, - PostgresSchemaEntry &schema, const string &table_name) { +unique_ptr PostgresTableSet::GetTableInfo(PostgresTransaction &transaction, + PostgresSchemaEntry &schema, const string &table_name) { auto query = PostgresTableSet::GetInitializeQuery(schema.name, table_name); auto result = transaction.Query(query); auto rows = result->Count(); if (rows == 0) { return nullptr; } - auto table_info = make_uniq(schema, table_name); + auto relkind = result->GetString(0, REL_KIND_ID); + auto view_definition = result->GetString(0, VIEW_DEFINITION_ID); + auto catalog_type = TransformRelKind(relkind); + unique_ptr info; + switch (catalog_type) { + case CatalogType::TABLE_ENTRY: + info = make_uniq(schema, table_name); + break; + case CatalogType::VIEW_ENTRY: + info = make_uniq(schema, table_name, view_definition); + break; + default: { + throw InternalException("Unexpected CatalogType in GetTableInfo: %s", CatalogTypeToString(catalog_type)); + } + } + for (idx_t row = 0; row < rows; row++) { - AddColumnOrConstraint(&transaction, &schema, *result, row, *table_info); + AddColumnOrConstraint(&transaction, &schema, *result, row, *info); } - table_info->approx_num_pages = result->GetInt64(0, 2); - return table_info; + info->approx_num_pages = result->GetInt64(0, APPROX_NUM_PAGES_ID); + return info; } -unique_ptr PostgresTableSet::GetTableInfo(PostgresConnection &connection, const string &schema_name, - const string &table_name) { +unique_ptr PostgresTableSet::GetTableInfo(PostgresConnection &connection, const string &schema_name, + const string &table_name) { auto query = PostgresTableSet::GetInitializeQuery(schema_name, table_name); auto result = connection.Query(query); auto rows = result->Count(); if (rows == 0) { throw InvalidInputException("Table %s does not contain any columns.", table_name); } - auto table_info = make_uniq(schema_name, table_name); + + auto relkind = result->GetString(0, REL_KIND_ID); + auto view_definition = result->GetString(0, VIEW_DEFINITION_ID); + auto catalog_type = TransformRelKind(relkind); + unique_ptr info; + switch (catalog_type) { + case CatalogType::TABLE_ENTRY: + info = make_uniq(schema_name, table_name); + break; + case CatalogType::VIEW_ENTRY: + info = make_uniq(schema_name, table_name, view_definition); + break; + default: { + throw InternalException("Unexpected CatalogType in GetTableInfo: %s", CatalogTypeToString(catalog_type)); + } + } for (idx_t row = 0; row < rows; row++) { - AddColumnOrConstraint(nullptr, nullptr, *result, row, *table_info); + AddColumnOrConstraint(nullptr, nullptr, *result, row, *info); } - table_info->approx_num_pages = result->GetInt64(0, 2); - return table_info; + info->approx_num_pages = result->GetInt64(0, APPROX_NUM_PAGES_ID); + return info; } optional_ptr PostgresTableSet::ReloadEntry(ClientContext &context, const string &table_name) { auto &transaction = PostgresTransaction::Get(context, catalog); - auto table_info = GetTableInfo(transaction, schema, table_name); - if (!table_info) { + auto pg_info = GetTableInfo(transaction, schema, table_name); + if (!pg_info) { return nullptr; } - auto table_entry = make_uniq(catalog, schema, *table_info); - auto table_ptr = table_entry.get(); - CreateEntry(std::move(table_entry)); - return table_ptr; + + unique_ptr entry; + auto &create_info = pg_info->GetCreateInfo(); + switch (create_info.type) { + case CatalogType::TABLE_ENTRY: { + auto &table_info = pg_info->Cast(); + entry = make_uniq(catalog, schema, table_info); + break; + } + case CatalogType::VIEW_ENTRY: { + auto &view_info = pg_info->Cast(); + entry = make_uniq(catalog, schema, view_info); + break; + } + default: { + throw InternalException("Unexpected CatalogType in ReloadEntry: %s", CatalogTypeToString(create_info.type)); + } + } + auto entry_ptr = entry.get(); + CreateEntry(std::move(entry)); + return entry_ptr; } // FIXME - this is almost entirely copied from TableCatalogEntry::ColumnsToSQL - should be unified diff --git a/src/storage/postgres_view_entry.cpp b/src/storage/postgres_view_entry.cpp new file mode 100644 index 00000000..21729812 --- /dev/null +++ b/src/storage/postgres_view_entry.cpp @@ -0,0 +1,63 @@ +#include "storage/postgres_catalog.hpp" +#include "storage/postgres_view_entry.hpp" +#include "storage/postgres_transaction.hpp" +#include "duckdb/storage/statistics/base_statistics.hpp" +#include "duckdb/catalog/catalog_entry/view_catalog_entry.hpp" +#include "duckdb/parser/query_node/select_node.hpp" +#include "postgres_scanner.hpp" + +namespace duckdb { + +PostgresViewEntry::PostgresViewEntry(Catalog &catalog, SchemaCatalogEntry &schema, CreateViewInfo &info) + : ViewCatalogEntry(catalog, schema, info) { + idx_t column_count = types.size(); + for (idx_t c = 0; c < column_count; c++) { + auto &type = types[c]; + auto &name = names[c]; + if (type.HasAlias()) { + type = PostgresUtils::RemoveAlias(type); + } + postgres_types.push_back(PostgresUtils::CreateEmptyPostgresType(type)); + postgres_names.push_back(name); + } + approx_num_pages = 0; +} + +static CreateViewInfo &ReplaceUnknownNames(CreateViewInfo &view_info) { + // When expressions like `select 'x'` are passed to postgres, the "?column?" name is given to them + // this clashes with DuckDB's behavior, as we assign the ToString of the expression to them instead. + // so we have to replace the "?column?" names with their DuckDB version + auto &select_node = view_info.query->node->Cast(); + auto &select_list = select_node.select_list; + // Aliases can override names, if provided + D_ASSERT(view_info.aliases.size() <= view_info.names.size()); + // Every name corresponds to an expression + D_ASSERT(view_info.names.size() == select_list.size()); + for (idx_t i = 0; i < view_info.names.size(); i++) { + if (i < view_info.aliases.size()) { + // No need to do anything, alias will be used instead of the expression name + continue; + } + if (view_info.names[i] != "?column?") { + // This expression was given an alias already + continue; + } + auto &expression = select_list[i]; + // If it had an alias the postgres name wouldn't be "?column?" + D_ASSERT(expression->alias.empty()); + view_info.names[i] = expression->ToString(); + } + return view_info; +} + +PostgresViewEntry::PostgresViewEntry(Catalog &catalog, SchemaCatalogEntry &schema, PostgresViewInfo &info) + : ViewCatalogEntry(catalog, schema, ReplaceUnknownNames(*info.create_info)), + postgres_types(std::move(info.postgres_types)), postgres_names(std::move(info.postgres_names)) { + D_ASSERT(postgres_types.size() == types.size()); + approx_num_pages = info.approx_num_pages; +} + +PostgresViewEntry::~PostgresViewEntry() { +} + +} // namespace duckdb diff --git a/test/other.sql b/test/other.sql index b1af15f4..bbf79e91 100644 --- a/test/other.sql +++ b/test/other.sql @@ -156,6 +156,8 @@ INSERT INTO chars VALUES ('hello'), ('world'), ('maxlength1'), ('hello '), ( CREATE TABLE chars_array(c CHAR(10)[]); INSERT INTO chars_array VALUES (ARRAY['hello', 'world', 'maxlength1', 'hello ', ' ', NULL]); +CREATE VIEW chars_view as select * from chars; + -- varchar with length limit CREATE TABLE varchars_fixed_len(c VARCHAR(10)); INSERT INTO varchars_fixed_len VALUES ('hello'), ('world'), ('maxlength1'), ('hello '), (' '), (NULL); diff --git a/test/sql/storage/attach_information_schema.test b/test/sql/storage/attach_information_schema.test new file mode 100644 index 00000000..69dae5f6 --- /dev/null +++ b/test/sql/storage/attach_information_schema.test @@ -0,0 +1,24 @@ +# name: test/sql/storage/attach_describe.test +# description: Test DESCRIBE +# group: [storage] + +require postgres_scanner + +require-env POSTGRES_TEST_DATABASE_AVAILABLE + +statement ok +PRAGMA enable_verification + +statement ok +ATTACH 'postgres:dbname=postgresscanner' AS s1 + +query IIII +select + table_catalog, + table_schema, + table_name, + table_type +from information_schema.tables where table_name in ('chars_view','chars'); +---- +s1 public chars BASE TABLE +s1 public chars_view VIEW diff --git a/test/sql/storage/attach_views.test b/test/sql/storage/attach_views.test index b5145f5d..31d431cc 100644 --- a/test/sql/storage/attach_views.test +++ b/test/sql/storage/attach_views.test @@ -16,74 +16,74 @@ statement ok USE s; statement ok -DROP VIEW IF EXISTS v1 +DROP VIEW IF EXISTS v5 statement ok -CREATE VIEW v1 AS SELECT 42 AS i; +CREATE VIEW v5 AS SELECT 42 AS i; query I -SELECT i FROM v1 +SELECT i FROM v5 ---- 42 statement error -CREATE VIEW v1 AS SELECT 84; +CREATE VIEW v5 AS SELECT 84; ---- already exists statement error -INSERT INTO v1 VALUES (42); +INSERT INTO v5 VALUES (42); ---- -cannot copy to view +Catalog Error: v5 is not an table # FIXME - error message here is not very descriptive statement error -UPDATE v1 SET i=84 +UPDATE v5 SET i=84 ---- statement error -DELETE FROM v1 +DELETE FROM v5 ---- statement error -INSERT INTO v1 VALUES (1, 1); +INSERT INTO v5 VALUES (1, 1); ---- -table v1 has 1 columns but 2 values were supplied +Catalog Error: v5 is not an table statement ok -CREATE VIEW IF NOT EXISTS v1 AS SELECT 84; +CREATE VIEW IF NOT EXISTS v5 AS SELECT 84; statement ok -CREATE OR REPLACE VIEW v1 AS SELECT 84; +CREATE OR REPLACE VIEW v5 AS SELECT 84; query I -SELECT * FROM v1 +SELECT * FROM v5 ---- 84 statement error -DROP TABLE v1 +DROP TABLE v5 ---- not a table statement ok -DROP VIEW v1 +DROP VIEW v5 statement error -SELECT * FROM v1 +SELECT * FROM v5 ---- -Table with name v1 does not exist +Table with name v5 does not exist statement error -DROP VIEW v1 +DROP VIEW v5 ---- -View with name v1 does not exist +View with name v5 does not exist statement ok -CREATE VIEW v1(a) AS SELECT 99; +CREATE VIEW v5(a) AS SELECT 99; query I -SELECT a FROM v1 +SELECT a FROM v5 ---- 99 diff --git a/test/sql/storage/view/test_view.test b/test/sql/storage/view/test_view.test new file mode 100644 index 00000000..84b4709f --- /dev/null +++ b/test/sql/storage/view/test_view.test @@ -0,0 +1,76 @@ +# name: test/sql/catalog/view/test_view.test +# description: Test view creation +# group: [view] + +require postgres_scanner + +require-env POSTGRES_TEST_DATABASE_AVAILABLE + +statement ok +PRAGMA enable_verification + +statement ok +ATTACH 'postgres:dbname=postgresscanner' AS s1 + +statement ok +drop view if exists s1.v1; + +statement ok +CREATE VIEW s1.v1 AS SELECT 42 as j + +statement error +CREATE VIEW s1.v1 AS SELECT 'whatever' +---- +Failed to execute query + +query I +SELECT j FROM s1.v1 WHERE j > 41 +---- +42 + +# name alias in view +query I +SELECT x FROM s1.v1 t1(x) WHERE x > 41 +---- +42 + +statement ok +DROP VIEW s1.v1 + +statement error +SELECT j FROM s1.v1 WHERE j > 41 +---- + +statement ok +CREATE VIEW s1.v1 AS SELECT 'whatever' + +query T +SELECT * FROM s1.v1 +---- +whatever + +statement ok +CREATE OR REPLACE VIEW s1.v1 AS SELECT 42 + +query I +SELECT * FROM s1.v1 +---- +42 + +statement error +INSERT INTO s1.v1 VALUES (1) +---- + +statement ok +DROP VIEW s1.v1 + +statement error +DROP VIEW s1.v1 +---- + +statement ok +DROP VIEW IF EXISTS s1.v1 + +statement error +CREATE VIEW s1.v1 AS SELECT * FROM dontexist +---- diff --git a/test/sql/storage/view/test_view_aliases.test b/test/sql/storage/view/test_view_aliases.test new file mode 100644 index 00000000..64ad693d --- /dev/null +++ b/test/sql/storage/view/test_view_aliases.test @@ -0,0 +1,69 @@ +# name: test/sql/catalog/view/test_view_aliases.test +# description: Test behavior of aliasing on view creation +# group: [view] + +require postgres_scanner + +require-env POSTGRES_TEST_DATABASE_AVAILABLE + +statement ok +PRAGMA enable_verification + +statement ok +ATTACH 'postgres:dbname=postgresscanner' AS s1 + +statement ok +drop schema if exists s1.view_aliases cascade; + +statement ok +create schema s1.view_aliases; + +# Fully aliased +statement ok +CREATE VIEW s1.view_aliases.X (a) AS SELECT 'x' + +query I +select a from s1.view_aliases.x; +---- +x + +statement ok +drop view s1.view_aliases.x; + +# Not aliased (no expression alias) +statement ok +CREATE VIEW s1.view_aliases.x AS SELECT 'x' + +query I +select x from s1.view_aliases.x +---- +{'CAST('x' AS VARCHAR)': x} + +statement ok +drop view s1.view_aliases.x; + +# Not aliased (with expression alias) +statement ok +CREATE VIEW s1.view_aliases.x AS SELECT 'x' x + +query I +select x from s1.view_aliases.x +---- +x + +statement ok +drop view s1.view_aliases.x; + +# Partially aliased +statement error +CREATE VIEW s1.view_aliases.x (x) AS SELECT 'a', 'x' x +---- +ERROR: column "x" specified more than once + +statement ok +CREATE VIEW s1.view_aliases.x (x) AS SELECT 'a', 'x' also_x + +query I +select x from s1.view_aliases.x +---- +a diff --git a/test/sql/storage/view/test_view_sql.test b/test/sql/storage/view/test_view_sql.test new file mode 100644 index 00000000..039f0d77 --- /dev/null +++ b/test/sql/storage/view/test_view_sql.test @@ -0,0 +1,64 @@ +# name: test/sql/catalog/view/test_view_sql.test +# description: Test behavior of 'sql' on various different views +# group: [view] + +require postgres_scanner + +require-env POSTGRES_TEST_DATABASE_AVAILABLE + +statement ok +PRAGMA enable_verification + +statement ok +ATTACH 'postgres:dbname=postgresscanner' AS s1 + +statement ok +drop schema if exists s1.my_schema cascade; + +statement ok +drop schema if exists s1."schema name" cascade; + +statement ok +drop view if exists s1.vw; + +statement ok +drop view if exists s1."view name"; + +statement ok +create schema s1.my_schema; + +# X contains columns `a` and `y` +statement ok +CREATE VIEW s1.my_schema.X (a) AS SELECT 'x' as x, 'y' as y; + +statement error +alter view s1.my_schema.x rename to Y; +---- +Binder Error: Only altering tables is supported for now + +statement ok +drop schema s1.my_schema cascade; + +statement ok +create view s1.vw as select * from VALUES (42, 'test') t(a, b); + +statement ok +create or replace view s1.vw (c1, c2) as select * from VALUES (42, 'test') t(a, b); + +statement ok +create or replace view s1."view name" as select * from VALUES (42, 'test') t(a, b); + +statement ok +drop view s1.vw; + +statement ok +drop view s1."view name" + +statement ok +create schema s1."schema name"; + +statement ok +CREATE VIEW s1."schema name"."view name" ( + "other name 1", + "column name 2" +) AS SELECT * FROM VALUES (42, 'test') t(a, b);