From 9b18e76a574fc6ce16365a80085745562ef2dfce Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Sat, 29 Jul 2023 19:29:16 -0700 Subject: [PATCH] wip: use sqlite extension structure that supports attach/detach --- CMakeLists.txt | 25 +- Makefile | 6 +- src/CMakeLists.txt | 18 ++ src/include/odbc_db.hpp | 46 +++ src/include/odbc_stmt.hpp | 62 ++++ src/include/odbc_storage.hpp | 12 + src/include/odbc_utils.hpp | 19 ++ src/include/storage/odbc_catalog.hpp | 73 +++++ src/include/storage/odbc_delete.hpp | 39 +++ src/include/storage/odbc_index.hpp | 25 ++ src/include/storage/odbc_index_entry.hpp | 18 ++ src/include/storage/odbc_insert.hpp | 44 +++ src/include/storage/odbc_schema_entry.hpp | 42 +++ src/include/storage/odbc_table_entry.hpp | 22 ++ src/include/storage/odbc_transaction.hpp | 34 ++ .../storage/odbc_transaction_manager.hpp | 25 ++ src/include/storage/odbc_update.hpp | 42 +++ src/odbc_db.cpp | 303 +++++++++++++++++ src/odbc_scan.cpp | 3 + src/odbc_scanner_extension.cpp | 41 ++- src/odbc_stmt.cpp | 167 ++++++++++ src/odbc_storage.cpp | 35 ++ src/odbc_utils.cpp | 109 +++++++ src/storage/CMakeLists.txt | 18 ++ src/storage/odbc_catalog.cpp | 89 +++++ src/storage/odbc_delete.cpp | 97 ++++++ src/storage/odbc_index.cpp | 56 ++++ src/storage/odbc_index_entry.cpp | 14 + src/storage/odbc_insert.cpp | 204 ++++++++++++ src/storage/odbc_schema_entry.cpp | 304 ++++++++++++++++++ src/storage/odbc_table_entry.cpp | 98 ++++++ src/storage/odbc_transaction.cpp | 119 +++++++ src/storage/odbc_transaction_manager.cpp | 40 +++ src/storage/odbc_update.cpp | 119 +++++++ test/sql/odbc_attach_db2.test | 11 + test/sql/odbc_scan_postgres.test | 62 ++-- 36 files changed, 2380 insertions(+), 61 deletions(-) create mode 100644 src/CMakeLists.txt create mode 100644 src/include/odbc_db.hpp create mode 100644 src/include/odbc_stmt.hpp create mode 100644 src/include/odbc_storage.hpp create mode 100644 src/include/odbc_utils.hpp create mode 100644 src/include/storage/odbc_catalog.hpp create mode 100644 src/include/storage/odbc_delete.hpp create mode 100644 src/include/storage/odbc_index.hpp create mode 100644 src/include/storage/odbc_index_entry.hpp create mode 100644 src/include/storage/odbc_insert.hpp create mode 100644 src/include/storage/odbc_schema_entry.hpp create mode 100644 src/include/storage/odbc_table_entry.hpp create mode 100644 src/include/storage/odbc_transaction.hpp create mode 100644 src/include/storage/odbc_transaction_manager.hpp create mode 100644 src/include/storage/odbc_update.hpp create mode 100644 src/odbc_db.cpp create mode 100644 src/odbc_stmt.cpp create mode 100644 src/odbc_storage.cpp create mode 100644 src/odbc_utils.cpp create mode 100644 src/storage/CMakeLists.txt create mode 100644 src/storage/odbc_catalog.cpp create mode 100644 src/storage/odbc_delete.cpp create mode 100644 src/storage/odbc_index.cpp create mode 100644 src/storage/odbc_index_entry.cpp create mode 100644 src/storage/odbc_insert.cpp create mode 100644 src/storage/odbc_schema_entry.cpp create mode 100644 src/storage/odbc_table_entry.cpp create mode 100644 src/storage/odbc_transaction.cpp create mode 100644 src/storage/odbc_transaction_manager.cpp create mode 100644 src/storage/odbc_update.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index acf0a03..80315e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,22 +1,23 @@ cmake_minimum_required(VERSION 2.8.12) - -# Set extension name here set(TARGET_NAME odbc_scanner) - set(EXTENSION_NAME ${TARGET_NAME}_extension) project(${TARGET_NAME}) -include_directories(src/include) - -set( - EXTENSION_SOURCES - src/odbc_scan.cpp - src/odbc_scanner_extension.cpp -) -add_library(${EXTENSION_NAME} STATIC ${EXTENSION_SOURCES}) +# include_directories(src/include) +# +# set( +# EXTENSION_SOURCES +# src/odbc_scan.cpp +# src/odbc_scanner_extension.cpp +# ) +# add_library(${EXTENSION_NAME} STATIC ${EXTENSION_SOURCES}) +add_subdirectory(src) +set(EXTENSION_OBJECT_FILES ${ALL_OBJECT_FILES}) +add_library(${EXTENSION_NAME} STATIC ${EXTENSION_OBJECT_FILES}) set(PARAMETERS "-warnings") -build_loadable_extension(${TARGET_NAME} ${PARAMETERS} ${EXTENSION_SOURCES}) +# build_loadable_extension(${TARGET_NAME} ${PARAMETERS} ${EXTENSION_SOURCES}) +build_loadable_extension(${TARGET_NAME} ${PARAMETERS} ${EXTENSION_OBJECT_FILES}) # nix store location of ODBC_CONFIG is passed in as CLIENT_FLAGS to root level Makefile # set(ODBC_CONFIG /nix/store/xs4bg5404nsjarivdzxszq0z0pn2ckzv-unixODBC-2.3.11/bin/odbc_config) diff --git a/Makefile b/Makefile index 5040081..de878f0 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,11 @@ BUILD_FLAGS=-DEXTENSION_STATIC_BUILD=1 -DBUILD_TPCH_EXTENSION=1 -DBUILD_PARQUET_ CLIENT_FLAGS := # These flags will make DuckDB build the extension -EXTENSION_FLAGS=-DDUCKDB_OOT_EXTENSION_NAMES="odbc_scanner" -DDUCKDB_OOT_EXTENSION_ODBC_SCANNER_PATH="$(PROJ_DIR)" -DDUCKDB_OOT_EXTENSION_ODBC_SCANNER_SHOULD_LINK="TRUE" -DDUCKDB_OOT_EXTENSION_ODBC_SCANNER_INCLUDE_PATH="$(PROJ_DIR)src/include" +EXTENSION_FLAGS=\ + -DDUCKDB_OOT_EXTENSION_NAMES="odbc_scanner" \ + -DDUCKDB_OOT_EXTENSION_ODBC_SCANNER_PATH="$(PROJ_DIR)" \ + -DDUCKDB_OOT_EXTENSION_ODBC_SCANNER_SHOULD_LINK="TRUE" \ + -DDUCKDB_OOT_EXTENSION_ODBC_SCANNER_INCLUDE_PATH="$(PROJ_DIR)src/include" pull: git submodule init diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..8911c82 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,18 @@ +include_directories(include) + +add_subdirectory(storage) + +add_library( + odbc_ext_library OBJECT + odbc_db.cpp + odbc_scanner_extension.cpp + odbc_scan.cpp + odbc_stmt.cpp + odbc_storage.cpp + odbc_utils.cpp +) +set( + ALL_OBJECT_FILES + ${ALL_OBJECT_FILES} $ + PARENT_SCOPE +) diff --git a/src/include/odbc_db.hpp b/src/include/odbc_db.hpp new file mode 100644 index 0000000..4db1463 --- /dev/null +++ b/src/include/odbc_db.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "odbc_utils.hpp" + +namespace duckdb { +class OdbcStatement; +struct IndexInfo; + +class OdbcDB { +public: + OdbcDB(); + // OdbcDB(sqlite3 *db); + ~OdbcDB(); + // disable copy constructors + OdbcDB(const OdbcDB &other) = delete; + OdbcDB &operator=(const OdbcDB &) = delete; + //! enable move constructors + OdbcDB(OdbcDB &&other) noexcept; + OdbcDB &operator=(OdbcDB &&) noexcept; + + // sqlite3 *db; + +public: + static OdbcDB Open(const string &path, bool is_read_only = true, bool is_shared = false); + bool TryPrepare(const string &query, OdbcStatement &result); + OdbcStatement Prepare(const string &query); + void Execute(const string &query); + vector GetTables(); + + vector GetEntries(string entry_type); + CatalogType GetEntryType(const string &name); + void GetTableInfo(const string &table_name, ColumnList &columns, vector> &constraints, + bool all_varchar); + void GetViewInfo(const string &view_name, string &sql); + void GetIndexInfo(const string &index_name, string &sql, string &table_name); + idx_t RunPragma(string pragma_name); + //! Gets the max row id of a table, returns false if the table does not have a rowid column + bool GetMaxRowId(const string &table_name, idx_t &row_id); + bool ColumnExists(const string &table_name, const string &column_name); + vector GetIndexInfo(const string &table_name); + + bool IsOpen(); + void Close(); +}; + +} // namespace duckdb diff --git a/src/include/odbc_stmt.hpp b/src/include/odbc_stmt.hpp new file mode 100644 index 0000000..7e6662d --- /dev/null +++ b/src/include/odbc_stmt.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "odbc_utils.hpp" + +#include + +namespace duckdb { + +class OdbcStatement { +public: + OdbcStatement(); + // OdbcStatement(sqlite3 *db, sqlite3_stmt *stmt); + ~OdbcStatement(); + // disable copy constructors + OdbcStatement(const OdbcStatement &other) = delete; + OdbcStatement &operator=(const OdbcStatement &) = delete; + //! enable move constructors + OdbcStatement(OdbcStatement &&other) noexcept; + OdbcStatement &operator=(OdbcStatement &&) noexcept; + + // sqlite3 *db; + // sqlite3_stmt *stmt; + +public: + int Step(); + template + T GetValue(idx_t col) { + throw InternalException("Unsupported type for OdbcStatement::GetValue"); + } + template + void Bind(idx_t col, T value) { + throw InternalException("Unsupported type for OdbcStatement::Bind"); + } + void BindText(idx_t col, const string_t &value); + void BindValue(Vector &col, idx_t c, idx_t r); + int GetType(idx_t col); + bool IsOpen(); + void Close(); + // void CheckTypeMatches(sqlite3_value *val, int sqlite_column_type, int expected_type, idx_t col_idx); + // void CheckTypeIsFloatOrInteger(sqlite3_value *val, int sqlite_column_type, idx_t col_idx); + void Reset(); +}; + +template <> +string OdbcStatement::GetValue(idx_t col); +template <> +int OdbcStatement::GetValue(idx_t col); +template <> +int64_t OdbcStatement::GetValue(idx_t col); +// template <> +// sqlite3_value *OdbcStatement::GetValue(idx_t col); + +template <> +void OdbcStatement::Bind(idx_t col, int32_t value); +template <> +void OdbcStatement::Bind(idx_t col, int64_t value); +template <> +void OdbcStatement::Bind(idx_t col, double value); +template <> +void OdbcStatement::Bind(idx_t col, std::nullptr_t value); + +} // namespace duckdb diff --git a/src/include/odbc_storage.hpp b/src/include/odbc_storage.hpp new file mode 100644 index 0000000..d36d831 --- /dev/null +++ b/src/include/odbc_storage.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "duckdb/storage/storage_extension.hpp" + +namespace duckdb { + +class OdbcStorageExtension : public StorageExtension { +public: + OdbcStorageExtension(); +}; + +} // namespace duckdb diff --git a/src/include/odbc_utils.hpp b/src/include/odbc_utils.hpp new file mode 100644 index 0000000..304bd06 --- /dev/null +++ b/src/include/odbc_utils.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "duckdb.hpp" +// #include "sqlite3.h" + +namespace duckdb { + +class OdbcUtils { +public: + // static void Check(int rc, sqlite3 *db); + static string TypeToString(int sqlite_type); + static LogicalType TypeToLogicalType(const string &sqlite_type); + static string SanitizeString(const string &table_name); + static string SanitizeIdentifier(const string &table_name); + static LogicalType ToOdbcType(const LogicalType &input); + string ToOdbcTypeAlias(const LogicalType &input); +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_catalog.hpp b/src/include/storage/odbc_catalog.hpp new file mode 100644 index 0000000..c5b5290 --- /dev/null +++ b/src/include/storage/odbc_catalog.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "duckdb/catalog/catalog.hpp" +#include "duckdb/common/enums/access_mode.hpp" +#include "odbc_db.hpp" + +namespace duckdb { +class OdbcSchemaEntry; + +class OdbcCatalog : public Catalog { +public: + explicit OdbcCatalog(AttachedDatabase &db_p, const string &path, AccessMode access_mode); + ~OdbcCatalog(); + + string path; + AccessMode access_mode; + +public: + void Initialize(bool load_builtin) override; + string GetCatalogType() override { + return "odbc"; + } + + optional_ptr CreateSchema(CatalogTransaction transaction, CreateSchemaInfo &info) override; + + void ScanSchemas(ClientContext &context, std::function callback) override; + + optional_ptr GetSchema(CatalogTransaction transaction, const string &schema_name, + OnEntryNotFound if_not_found, + QueryErrorContext error_context = QueryErrorContext()) override; + + OdbcSchemaEntry &GetMainSchema() { + return *main_schema; + } + + unique_ptr PlanInsert(ClientContext &context, LogicalInsert &op, + unique_ptr plan) override; + unique_ptr PlanCreateTableAs(ClientContext &context, LogicalCreateTable &op, + unique_ptr plan) override; + unique_ptr PlanDelete(ClientContext &context, LogicalDelete &op, + unique_ptr plan) override; + unique_ptr PlanUpdate(ClientContext &context, LogicalUpdate &op, + unique_ptr plan) override; + unique_ptr BindCreateIndex(Binder &binder, CreateStatement &stmt, TableCatalogEntry &table, + unique_ptr plan) override; + + DatabaseSize GetDatabaseSize(ClientContext &context) override; + + //! Whether or not this is an in-memory Odbc database + bool InMemory() override; + string GetDBPath() override; + + //! Returns a reference to the in-memory database (if any) + OdbcDB *GetInMemoryDatabase(); + //! Release the in-memory database (if there is any) + void ReleaseInMemoryDatabase(); + +private: + void DropSchema(ClientContext &context, DropInfo &info) override; + +private: + unique_ptr main_schema; + //! Whether or not the database is in-memory + bool in_memory; + //! In-memory database - if any + OdbcDB in_memory_db; + //! The lock maintaing access to the in-memory database + mutex in_memory_lock; + //! Whether or not there is any active transaction on the in-memory database + bool active_in_memory; +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_delete.hpp b/src/include/storage/odbc_delete.hpp new file mode 100644 index 0000000..be914ca --- /dev/null +++ b/src/include/storage/odbc_delete.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "duckdb/execution/physical_operator.hpp" + +namespace duckdb { + +class OdbcDelete : public PhysicalOperator { +public: + OdbcDelete(LogicalOperator &op, TableCatalogEntry &table); + + //! The table to delete from + TableCatalogEntry &table; + +public: + // Source interface + SourceResultType GetData(ExecutionContext &context, DataChunk &chunk, OperatorSourceInput &input) const override; + + bool IsSource() const override { + return true; + } + +public: + // Sink interface + unique_ptr GetGlobalSinkState(ClientContext &context) const override; + SinkResultType Sink(ExecutionContext &context, DataChunk &chunk, OperatorSinkInput &input) const override; + + bool IsSink() const override { + return true; + } + + bool ParallelSink() const override { + return false; + } + + string GetName() const override; + string ParamsToString() const override; +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_index.hpp b/src/include/storage/odbc_index.hpp new file mode 100644 index 0000000..c944794 --- /dev/null +++ b/src/include/storage/odbc_index.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "duckdb/execution/physical_operator.hpp" +#include "duckdb/parser/parsed_data/create_index_info.hpp" + +namespace duckdb { + +//! PhysicalCreateSequence represents a CREATE SEQUENCE command +class OdbcCreateIndex : public PhysicalOperator { +public: + explicit OdbcCreateIndex(unique_ptr info, TableCatalogEntry &table); + + unique_ptr info; + TableCatalogEntry &table; + +public: + // Source interface + SourceResultType GetData(ExecutionContext &context, DataChunk &chunk, OperatorSourceInput &input) const override; + + bool IsSource() const override { + return true; + } +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_index_entry.hpp b/src/include/storage/odbc_index_entry.hpp new file mode 100644 index 0000000..00c8ab5 --- /dev/null +++ b/src/include/storage/odbc_index_entry.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "duckdb/catalog/catalog_entry/index_catalog_entry.hpp" + +namespace duckdb { + +class OdbcIndexEntry : public IndexCatalogEntry { +public: + OdbcIndexEntry(Catalog &catalog, SchemaCatalogEntry &schema, CreateIndexInfo &info, string table_name); + + string table_name; + +public: + string GetSchemaName() const override; + string GetTableName() const override; +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_insert.hpp b/src/include/storage/odbc_insert.hpp new file mode 100644 index 0000000..5f1c66d --- /dev/null +++ b/src/include/storage/odbc_insert.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "duckdb/common/index_vector.hpp" +#include "duckdb/execution/physical_operator.hpp" + +namespace duckdb { + +class OdbcInsert : public PhysicalOperator { +public: + //! INSERT INTO + OdbcInsert(LogicalOperator &op, TableCatalogEntry &table, physical_index_vector_t column_index_map); + //! CREATE TABLE AS + OdbcInsert(LogicalOperator &op, SchemaCatalogEntry &schema, unique_ptr info); + + //! The table to insert into + optional_ptr table; + //! Table schema, in case of CREATE TABLE AS + optional_ptr schema; + //! Create table info, in case of CREATE TABLE AS + unique_ptr info; + //! column_index_map + physical_index_vector_t column_index_map; + +public: + // Source interface + SourceResultType GetData(ExecutionContext &context, DataChunk &chunk, + OperatorSourceInput &input) const override; + + bool IsSource() const override { return true; } + +public: + // Sink interface + unique_ptr GetGlobalSinkState(ClientContext &context) const override; + SinkResultType Sink(ExecutionContext &context, DataChunk &chunk, OperatorSinkInput &input) const override; + + bool IsSink() const override { return true; } + + bool ParallelSink() const override { return false; } + + string GetName() const override; + string ParamsToString() const override; +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_schema_entry.hpp b/src/include/storage/odbc_schema_entry.hpp new file mode 100644 index 0000000..18bdaa5 --- /dev/null +++ b/src/include/storage/odbc_schema_entry.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "duckdb/catalog/catalog_entry/schema_catalog_entry.hpp" + +namespace duckdb { +class OdbcTransaction; + +class OdbcSchemaEntry : public SchemaCatalogEntry { +public: + OdbcSchemaEntry(Catalog &catalog); + +public: + optional_ptr CreateTable(CatalogTransaction transaction, BoundCreateTableInfo &info) override; + optional_ptr CreateFunction(CatalogTransaction transaction, CreateFunctionInfo &info) override; + optional_ptr CreateIndex(ClientContext &context, CreateIndexInfo &info, + TableCatalogEntry &table) override; + optional_ptr CreateView(CatalogTransaction transaction, CreateViewInfo &info) override; + optional_ptr CreateSequence(CatalogTransaction transaction, CreateSequenceInfo &info) override; + optional_ptr CreateTableFunction(CatalogTransaction transaction, + CreateTableFunctionInfo &info) override; + optional_ptr CreateCopyFunction(CatalogTransaction transaction, + CreateCopyFunctionInfo &info) override; + optional_ptr CreatePragmaFunction(CatalogTransaction transaction, + CreatePragmaFunctionInfo &info) override; + optional_ptr CreateCollation(CatalogTransaction transaction, CreateCollationInfo &info) override; + optional_ptr CreateType(CatalogTransaction transaction, CreateTypeInfo &info) override; + void Alter(ClientContext &context, AlterInfo &info) override; + void Scan(ClientContext &context, CatalogType type, const std::function &callback) override; + void Scan(CatalogType type, const std::function &callback) override; + void DropEntry(ClientContext &context, DropInfo &info) override; + optional_ptr GetEntry(CatalogTransaction transaction, CatalogType type, const string &name) override; + +private: + void AlterTable(OdbcTransaction &transaction, RenameTableInfo &info); + void AlterTable(OdbcTransaction &transaction, RenameColumnInfo &info); + void AlterTable(OdbcTransaction &transaction, AddColumnInfo &info); + void AlterTable(OdbcTransaction &transaction, RemoveColumnInfo &info); + + void TryDropEntry(ClientContext &context, CatalogType catalog_type, const string &name); +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_table_entry.hpp b/src/include/storage/odbc_table_entry.hpp new file mode 100644 index 0000000..2d2b3c6 --- /dev/null +++ b/src/include/storage/odbc_table_entry.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "duckdb/catalog/catalog_entry/table_catalog_entry.hpp" + +namespace duckdb { + +class OdbcTableEntry : public TableCatalogEntry { +public: + OdbcTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, CreateTableInfo &info); + +public: + unique_ptr GetStatistics(ClientContext &context, column_t column_id) override; + + TableFunction GetScanFunction(ClientContext &context, unique_ptr &bind_data) override; + + TableStorageInfo GetStorageInfo(ClientContext &context) override; + + void BindUpdateConstraints(LogicalGet &get, LogicalProjection &proj, LogicalUpdate &update, + ClientContext &context) override; +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_transaction.hpp b/src/include/storage/odbc_transaction.hpp new file mode 100644 index 0000000..6be987b --- /dev/null +++ b/src/include/storage/odbc_transaction.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "duckdb/transaction/transaction.hpp" +#include "duckdb/common/case_insensitive_map.hpp" +#include "odbc_db.hpp" + +namespace duckdb { +class OdbcCatalog; +class OdbcTableEntry; + +class OdbcTransaction : public Transaction { +public: + OdbcTransaction(OdbcCatalog &odbc_catalog, TransactionManager &manager, ClientContext &context); + ~OdbcTransaction() override; + + void Start(); + void Commit(); + void Rollback(); + + OdbcDB &GetDB(); + optional_ptr GetCatalogEntry(const string &table_name); + void DropEntry(CatalogType type, const string &table_name, bool cascade); + void ClearTableEntry(const string &table_name); + + static OdbcTransaction &Get(ClientContext &context, Catalog &catalog); + +private: + OdbcCatalog &odbc_catalog; + OdbcDB *db; + OdbcDB owned_db; + case_insensitive_map_t> catalog_entries; +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_transaction_manager.hpp b/src/include/storage/odbc_transaction_manager.hpp new file mode 100644 index 0000000..ff2e8c0 --- /dev/null +++ b/src/include/storage/odbc_transaction_manager.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "duckdb/transaction/transaction_manager.hpp" +#include "storage/odbc_catalog.hpp" +#include "storage/odbc_transaction.hpp" + +namespace duckdb { + +class OdbcTransactionManager : public TransactionManager { +public: + OdbcTransactionManager(AttachedDatabase &db_p, OdbcCatalog &sqlite_catalog); + + Transaction *StartTransaction(ClientContext &context) override; + string CommitTransaction(ClientContext &context, Transaction *transaction) override; + void RollbackTransaction(Transaction *transaction) override; + + void Checkpoint(ClientContext &context, bool force = false) override; + +private: + OdbcCatalog &sqlite_catalog; + mutex transaction_lock; + unordered_map> transactions; +}; + +} // namespace duckdb diff --git a/src/include/storage/odbc_update.hpp b/src/include/storage/odbc_update.hpp new file mode 100644 index 0000000..0d21ecd --- /dev/null +++ b/src/include/storage/odbc_update.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "duckdb/execution/physical_operator.hpp" +#include "duckdb/common/index_vector.hpp" + +namespace duckdb { + +class OdbcUpdate : public PhysicalOperator { +public: + OdbcUpdate(LogicalOperator &op, TableCatalogEntry &table, vector columns); + + //! The table to delete from + TableCatalogEntry &table; + //! The set of columns to update + vector columns; + +public: + // Source interface + SourceResultType GetData(ExecutionContext &context, DataChunk &chunk, OperatorSourceInput &input) const override; + + bool IsSource() const override { + return true; + } + +public: + // Sink interface + unique_ptr GetGlobalSinkState(ClientContext &context) const override; + SinkResultType Sink(ExecutionContext &context, DataChunk &chunk, OperatorSinkInput &input) const override; + + bool IsSink() const override { + return true; + } + + bool ParallelSink() const override { + return false; + } + + string GetName() const override; + string ParamsToString() const override; +}; + +} // namespace duckdb diff --git a/src/odbc_db.cpp b/src/odbc_db.cpp new file mode 100644 index 0000000..6f269d4 --- /dev/null +++ b/src/odbc_db.cpp @@ -0,0 +1,303 @@ +#include "odbc_db.hpp" +#include "duckdb/parser/column_list.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/parser/parser.hpp" +#include "duckdb/storage/table_storage_info.hpp" +#include "odbc_stmt.hpp" + +#include + +namespace duckdb { +// TODO: +// - need to implement the constructor below. This is a hack to get it compiling... +OdbcDB::OdbcDB() {} + +// OdbcDB::OdbcDB() : db(nullptr) { +// } + +// OdbcDB::OdbcDB(sqlite3 *db) : db(db) { +// } + +OdbcDB::~OdbcDB() { Close(); } + +OdbcDB::OdbcDB(OdbcDB &&other) noexcept { + // std::swap(db, other.db); +} + +OdbcDB &OdbcDB::operator=(OdbcDB &&other) noexcept { + // std::swap(db, other.db); + return *this; +} + +OdbcDB OdbcDB::Open(const string &path, bool is_read_only, bool is_shared) { + std::cout << "---------------------" << std::endl; + std::cout << "in OdbcDB::Open" << std::endl; + + OdbcDB result; + // int flags = SQLITE_OPEN_PRIVATECACHE; + // if (is_read_only) { + // flags |= SQLITE_OPEN_READONLY; + // } else { + // flags |= SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; + // } + // if (!is_shared) { + // // FIXME: we should just make sure we are not re-using the same `sqlite3` object across threads + // flags |= SQLITE_OPEN_NOMUTEX; + // } + // flags |= SQLITE_OPEN_EXRESCODE; + // auto rc = sqlite3_open_v2(path.c_str(), &result.db, flags, nullptr); + // if (rc != SQLITE_OK) { + // throw std::runtime_error("Unable to open database \"" + path + "\": " + string(sqlite3_errstr(rc))); + // } + return result; +} + +bool OdbcDB::TryPrepare(const string &query, OdbcStatement &stmt) { + std::cout << "---------------------" << std::endl; + std::cout << "in OdbcDB::TryPrepare" << query << std::endl; + + // stmt.db = db; + // auto rc = sqlite3_prepare_v2(db, query.c_str(), -1, &stmt.stmt, nullptr); + // if (rc != SQLITE_OK) { + // return false; + // } + return true; +} + +OdbcStatement OdbcDB::Prepare(const string &query) { + std::cout << "---------------------" << std::endl; + std::cout << "in OdbcDB::Prepare" << query << std::endl; + + OdbcStatement stmt; + // if (!TryPrepare(query, stmt)) { + // string error = "Failed to prepare query \"" + query + "\": " + string(sqlite3_errmsg(db)); + // throw std::runtime_error(error); + // } + return stmt; +} + +void OdbcDB::Execute(const string &query) { + std::cout << "---------------------" << std::endl; + std::cout << "in OdbcDB::Execute" << query << std::endl; + + // auto rc = sqlite3_exec(db, query.c_str(), nullptr, nullptr, nullptr); + // if (rc != SQLITE_OK) { + // string error = "Failed to execute query \"" + query + "\": " + string(sqlite3_errmsg(db)); + // throw std::runtime_error(error); + // } +} + +bool OdbcDB::IsOpen() { + std::cout << "---------------------" << std::endl; + std::cout << "in OdbcDB::IsOpen" << std::endl; + + // return db; + return false; +} + +void OdbcDB::Close() { + // if (!IsOpen()) { + // return; + // } + // auto rc = sqlite3_close_v2(db); + // if (rc == SQLITE_BUSY) { + // throw InternalException("Failed to close database - SQLITE_BUSY"); + // } + // db = nullptr; +} + +vector OdbcDB::GetEntries(string entry_type) { + vector result; + OdbcStatement stmt = Prepare("SELECT name FROM sqlite_master WHERE type='" + entry_type + "'"); + while (stmt.Step()) { + auto table_name = stmt.GetValue(0); + result.push_back(std::move(table_name)); + } + return result; +} + +vector OdbcDB::GetTables() { return GetEntries("table"); } + +CatalogType OdbcDB::GetEntryType(const string &name) { + OdbcStatement stmt; + stmt = Prepare(StringUtil::Format("SELECT type FROM sqlite_master WHERE lower(name)=lower('%s');", + OdbcUtils::SanitizeString(name))); + while (stmt.Step()) { + auto type = stmt.GetValue(0); + if (type == "table") { + return CatalogType::TABLE_ENTRY; + } else if (type == "view") { + return CatalogType::VIEW_ENTRY; + } else if (type == "index") { + return CatalogType::INDEX_ENTRY; + } else { + throw InternalException("Unrecognized Odbc type \"%s\"", name); + } + } + return CatalogType::INVALID; +} + +void OdbcDB::GetIndexInfo(const string &index_name, string &sql, string &table_name) { + OdbcStatement stmt; + stmt = Prepare(StringUtil::Format("SELECT tbl_name, sql FROM sqlite_master WHERE lower(name)=lower('%s');", + OdbcUtils::SanitizeString(index_name))); + while (stmt.Step()) { + table_name = stmt.GetValue(0); + sql = stmt.GetValue(1); + return; + } + throw InternalException("GetViewInfo - index \"%s\" not found", index_name); +} + +void OdbcDB::GetViewInfo(const string &view_name, string &sql) { + OdbcStatement stmt; + stmt = Prepare(StringUtil::Format("SELECT sql FROM sqlite_master WHERE lower(name)=lower('%s');", + OdbcUtils::SanitizeString(view_name))); + while (stmt.Step()) { + sql = stmt.GetValue(0); + return; + } + throw InternalException("GetViewInfo - view \"%s\" not found", view_name); +} + +void OdbcDB::GetTableInfo(const string &table_name, ColumnList &columns, + vector> &constraints, bool all_varchar) { + OdbcStatement stmt; + + idx_t primary_key_index = idx_t(-1); + vector primary_keys; + + bool found = false; + + stmt = Prepare(StringUtil::Format("PRAGMA table_info('%s')", OdbcUtils::SanitizeString(table_name))); + while (stmt.Step()) { + auto cid = stmt.GetValue(0); + auto sqlite_colname = stmt.GetValue(1); + auto sqlite_type = StringUtil::Lower(stmt.GetValue(2)); + auto not_null = stmt.GetValue(3); + auto default_value = stmt.GetValue(4); + auto pk = stmt.GetValue(5); + StringUtil::Trim(sqlite_type); + auto column_type = all_varchar ? LogicalType::VARCHAR : OdbcUtils::TypeToLogicalType(sqlite_type); + + ColumnDefinition column(std::move(sqlite_colname), std::move(column_type)); + if (!default_value.empty()) { + auto expressions = Parser::ParseExpressionList(default_value); + if (expressions.empty()) { + throw InternalException("Expression list is empty"); + } + column.SetDefaultValue(std::move(expressions[0])); + } + columns.AddColumn(std::move(column)); + if (not_null) { + constraints.push_back(make_uniq(LogicalIndex(cid))); + } + if (pk) { + primary_key_index = cid; + primary_keys.push_back(sqlite_colname); + } + found = true; + } + if (!found) { + throw InternalException("GetTableInfo - table \"%s\" not found", table_name); + } + if (!primary_keys.empty()) { + if (primary_keys.size() == 1) { + constraints.push_back(make_uniq(LogicalIndex(primary_key_index), true)); + } else { + constraints.push_back(make_uniq(std::move(primary_keys), true)); + } + } +} + +bool OdbcDB::ColumnExists(const string &table_name, const string &column_name) { + OdbcStatement stmt; + + stmt = Prepare(StringUtil::Format("PRAGMA table_info(\"%s\")", OdbcUtils::SanitizeIdentifier(table_name))); + while (stmt.Step()) { + auto sqlite_colname = stmt.GetValue(1); + if (sqlite_colname == column_name) { + return true; + } + } + return false; +} + +bool OdbcDB::GetMaxRowId(const string &table_name, idx_t &max_row_id) { + OdbcStatement stmt; + if (!TryPrepare( + StringUtil::Format("SELECT MAX(ROWID) FROM \"%s\"", OdbcUtils::SanitizeIdentifier(table_name)), + stmt)) { + return false; + } + if (!stmt.Step()) { + return false; + } + int64_t val = stmt.GetValue(0); + ; + if (val <= 0) { + return false; + } + max_row_id = idx_t(val); + return true; +} + +vector OdbcDB::GetIndexInfo(const string &table_name) { + vector info; + // fetch the primary key + OdbcStatement stmt; + stmt = Prepare(StringUtil::Format("SELECT cid FROM pragma_table_info('%s') WHERE pk", + OdbcUtils::SanitizeString(table_name))); + IndexInfo pk_index; + while (stmt.Step()) { + auto cid = stmt.GetValue(0); + pk_index.column_set.insert(cid); + } + if (!pk_index.column_set.empty()) { + // we have a pk - add it + pk_index.is_primary = true; + pk_index.is_unique = true; + pk_index.is_foreign = false; + info.push_back(std::move(pk_index)); + } + + // now query the set of unique constraints for the table + stmt = + Prepare(StringUtil::Format("SELECT name FROM pragma_index_list('%s') WHERE \"unique\" AND origin='u'", + OdbcUtils::SanitizeString(table_name))); + vector unique_indexes; + while (stmt.Step()) { + auto index_name = stmt.GetValue(0); + unique_indexes.push_back(index_name); + } + for (auto &index_name : unique_indexes) { + stmt = Prepare( + StringUtil::Format("SELECT cid FROM pragma_index_info('%s')", OdbcUtils::SanitizeString(index_name))); + IndexInfo unique_index; + while (stmt.Step()) { + auto cid = stmt.GetValue(0); + unique_index.column_set.insert(cid); + } + if (!unique_index.column_set.empty()) { + // we have a pk - add it + unique_index.is_primary = false; + unique_index.is_unique = true; + unique_index.is_foreign = false; + info.push_back(std::move(unique_index)); + } + } + return info; +} + +idx_t OdbcDB::RunPragma(string pragma_name) { + OdbcStatement stmt; + stmt = Prepare("PRAGMA " + pragma_name); + while (stmt.Step()) { + return idx_t(stmt.GetValue(0)); + } + throw InternalException("No result returned from pragma " + pragma_name); +} + +} // namespace duckdb diff --git a/src/odbc_scan.cpp b/src/odbc_scan.cpp index a492f3a..c3f0e1e 100644 --- a/src/odbc_scan.cpp +++ b/src/odbc_scan.cpp @@ -82,6 +82,9 @@ static void OdbcScan(ClientContext &context, TableFunctionInput &data, DataChunk static unique_ptr OdbcScanBind(ClientContext &context, TableFunctionBindInput &input, vector &return_types, vector &names) { + std::cout << "---------------------" << std::endl; + std::cout << "in OdbcScanBind" << std::endl; + auto bind_data = make_uniq(); bind_data->connection_string = input.inputs[0].GetValue(); bind_data->schema_name = input.inputs[1].GetValue(); diff --git a/src/odbc_scanner_extension.cpp b/src/odbc_scanner_extension.cpp index 5c84227..25dc21b 100644 --- a/src/odbc_scanner_extension.cpp +++ b/src/odbc_scanner_extension.cpp @@ -1,8 +1,10 @@ #define DUCKDB_EXTENSION_MAIN -#include "odbc_scanner_extension.hpp" -#include "odbc_attach.hpp" +#include "odbc_storage.hpp" +// #include "odbc_scanner_extension.hpp" +// #include "odbc_attach.hpp" #include "odbc_scan.hpp" +#include "odbc_scanner_extension.hpp" #include "duckdb.hpp" #include "duckdb/common/exception.hpp" @@ -15,9 +17,9 @@ #include "duckdb/parser/parsed_data/create_table_function_info.hpp" namespace duckdb { -static void LoadInternal(DatabaseInstance &instance) { +static void LoadInternal(DatabaseInstance &db) { // table functions - Connection con(instance); + Connection con(db); con.BeginTransaction(); auto &context = *con.context; auto &catalog = Catalog::GetSystemCatalog(context); @@ -26,19 +28,24 @@ static void LoadInternal(DatabaseInstance &instance) { CreateTableFunctionInfo scan_info(scan_fun); catalog.CreateTableFunction(context, scan_info); - OdbcScanFunctionFilterPushdown scan_fun_filter_pushdown; - CreateTableFunctionInfo scan_filter_pushdown_info(scan_fun_filter_pushdown); - catalog.CreateTableFunction(context, scan_filter_pushdown_info); - - TableFunction attach_fun("odbc_attach", {LogicalType::VARCHAR}, AttachFunction, AttachBind); - attach_fun.named_parameters["overwrite"] = LogicalType::BOOLEAN; - attach_fun.named_parameters["filter_pushdown"] = LogicalType::BOOLEAN; - attach_fun.named_parameters["source_schema"] = LogicalType::VARCHAR; - // attach_fun.named_parameters["sink_schema_prefix"] = LogicalType::VARCHAR; - attach_fun.named_parameters["sink_schema"] = LogicalType::VARCHAR; - attach_fun.named_parameters["suffix"] = LogicalType::VARCHAR; - CreateTableFunctionInfo attach_info(attach_fun); - catalog.CreateTableFunction(context, attach_info); + // OdbcScanFunctionFilterPushdown scan_fun_filter_pushdown; + // CreateTableFunctionInfo scan_filter_pushdown_info(scan_fun_filter_pushdown); + // catalog.CreateTableFunction(context, scan_filter_pushdown_info); + + // TableFunction attach_fun("odbc_attach", {LogicalType::VARCHAR}, AttachFunction, AttachBind); + // attach_fun.named_parameters["overwrite"] = LogicalType::BOOLEAN; + // attach_fun.named_parameters["filter_pushdown"] = LogicalType::BOOLEAN; + // attach_fun.named_parameters["source_schema"] = LogicalType::VARCHAR; + // // attach_fun.named_parameters["sink_schema_prefix"] = LogicalType::VARCHAR; + // attach_fun.named_parameters["sink_schema"] = LogicalType::VARCHAR; + // attach_fun.named_parameters["suffix"] = LogicalType::VARCHAR; + // CreateTableFunctionInfo attach_info(attach_fun); + // catalog.CreateTableFunction(context, attach_info); + + auto &config = DBConfig::GetConfig(db); + // config.AddExtensionOption("sqlite_all_varchar", "Load all SQLite columns as VARCHAR columns", LogicalType::BOOLEAN); + + config.storage_extensions["odbc_scanner"] = make_uniq(); con.Commit(); } diff --git a/src/odbc_stmt.cpp b/src/odbc_stmt.cpp new file mode 100644 index 0000000..5dc3c20 --- /dev/null +++ b/src/odbc_stmt.cpp @@ -0,0 +1,167 @@ +#include "odbc_stmt.hpp" +#include "odbc_db.hpp" + +namespace duckdb { +// TODO: +// - this was not a real constructor. Just here to get things to compile +OdbcStatement::OdbcStatement() {} + +// OdbcStatement::OdbcStatement() : db(nullptr), stmt(nullptr) { +// } + +// OdbcStatement::OdbcStatement(sqlite3 *db, sqlite3_stmt *stmt) : db(db), stmt(stmt) { +// D_ASSERT(db); +// } + +OdbcStatement::~OdbcStatement() { Close(); } + +OdbcStatement::OdbcStatement(OdbcStatement &&other) noexcept { + // std::swap(db, other.db); + // std::swap(stmt, other.stmt); +} + +OdbcStatement &OdbcStatement::operator=(OdbcStatement &&other) noexcept { + // std::swap(db, other.db); + // std::swap(stmt, other.stmt); + return *this; +} + +int OdbcStatement::Step() { + // D_ASSERT(db); + // D_ASSERT(stmt); + // auto rc = sqlite3_step(stmt); + // if (rc == SQLITE_ROW) { + // return true; + // } + // if (rc == SQLITE_DONE) { + // return false; + // } + // throw std::runtime_error(string(sqlite3_errmsg(db))); + + return false; +} +int OdbcStatement::GetType(idx_t col) { + // D_ASSERT(stmt); + // return sqlite3_column_type(stmt, col); + + return false; +} + +bool OdbcStatement::IsOpen() { + // return stmt; + + return false; +} + +void OdbcStatement::Close() { + // if (!IsOpen()) { + // return; + // } + // sqlite3_finalize(stmt); + // db = nullptr; + // stmt = nullptr; +} + +// void OdbcStatement::CheckTypeMatches(sqlite3_value *val, int sqlite_column_type, int expected_type, idx_t +// col_idx) { D_ASSERT(stmt); if (sqlite_column_type != expected_type) { auto column_name +// = string(sqlite3_column_name(stmt, int(col_idx))); auto value_as_text = string((char +// *)sqlite3_value_text(val)); auto message = "Invalid type in column \"" + column_name + "\": column +// was +// declared as " + OdbcUtils::TypeToString(expected_type) + ", found \"" + +// value_as_text + "\" of type \"" + OdbcUtils::TypeToString(sqlite_column_type) + "\" instead."; throw +// Exception(ExceptionType::MISMATCH_TYPE, message); +// } +// } +// +// void OdbcStatement::CheckTypeIsFloatOrInteger(sqlite3_value *val, int sqlite_column_type, idx_t col_idx) { +// if (sqlite_column_type != SQLITE_FLOAT && sqlite_column_type != SQLITE_INTEGER) { +// auto column_name = string(sqlite3_column_name(stmt, int(col_idx))); +// auto value_as_text = string((const char *)sqlite3_value_text(val)); +// auto message = "Invalid type in column \"" + column_name + "\": expected float or integer, +// found +// \"" + value_as_text + "\" of type \"" + +// OdbcUtils::TypeToString(sqlite_column_type) + "\" instead."; throw +// Exception(ExceptionType::MISMATCH_TYPE, message); +// } +// } + +void OdbcStatement::Reset() { + // OdbcUtils::Check(sqlite3_reset(stmt), db); +} + +template <> string OdbcStatement::GetValue(idx_t col) { + // D_ASSERT(stmt); + // auto ptr = sqlite3_column_text(stmt, col); + // if (!ptr) { + // return string(); + // } + // return string((char *)ptr); + + return "todo"; +} + +template <> int OdbcStatement::GetValue(idx_t col) { + // D_ASSERT(stmt); + // return sqlite3_column_int(stmt, col); + + return false; +} + +template <> int64_t OdbcStatement::GetValue(idx_t col) { + // D_ASSERT(stmt); + // return sqlite3_column_int64(stmt, col); + + return false; +} + +// template <> +// sqlite3_value *OdbcStatement::GetValue(idx_t col) { +// // D_ASSERT(stmt); +// // return sqlite3_column_value(stmt, col); +// +// return false; +// } + +template <> void OdbcStatement::Bind(idx_t col, int32_t value) { + // OdbcUtils::Check(sqlite3_bind_int(stmt, col + 1, value), db); +} + +template <> void OdbcStatement::Bind(idx_t col, int64_t value) { + // OdbcUtils::Check(sqlite3_bind_int64(stmt, col + 1, value), db); +} + +template <> void OdbcStatement::Bind(idx_t col, double value) { + // OdbcUtils::Check(sqlite3_bind_double(stmt, col + 1, value), db); +} + +void OdbcStatement::BindText(idx_t col, const string_t &value) { + // OdbcUtils::Check(sqlite3_bind_text(stmt, col + 1, value.GetDataUnsafe(), value.GetSize(), nullptr), db); +} + +template <> void OdbcStatement::Bind(idx_t col, std::nullptr_t value) { + // OdbcUtils::Check(sqlite3_bind_null(stmt, col + 1), db); +} + +void OdbcStatement::BindValue(Vector &col, idx_t c, idx_t r) { + // auto &mask = FlatVector::Validity(col); + // if (!mask.RowIsValid(r)) { + // Bind(c, nullptr); + // } else { + // switch (col.GetType().id()) { + // case LogicalTypeId::BIGINT: + // Bind(c, FlatVector::GetData(col)[r]); + // break; + // case LogicalTypeId::DOUBLE: + // Bind(c, FlatVector::GetData(col)[r]); + // break; + // case LogicalTypeId::BLOB: + // case LogicalTypeId::VARCHAR: + // BindText(c, FlatVector::GetData(col)[r]); + // break; + // default: + // throw InternalException("Unsupported type \"%s\" for Odbc::BindValue", col.GetType()); + // } + // } +} + +} // namespace duckdb diff --git a/src/odbc_storage.cpp b/src/odbc_storage.cpp new file mode 100644 index 0000000..658b7c2 --- /dev/null +++ b/src/odbc_storage.cpp @@ -0,0 +1,35 @@ +#include "duckdb.hpp" + +// #include "sqlite3.h" +#include "duckdb/catalog/catalog_entry/schema_catalog_entry.hpp" +#include "duckdb/catalog/catalog_entry/table_catalog_entry.hpp" +#include "duckdb/parser/parsed_data/attach_info.hpp" +#include "duckdb/transaction/transaction_manager.hpp" +#include "odbc_storage.hpp" +#include "odbc_utils.hpp" +#include "storage/odbc_catalog.hpp" +#include "storage/odbc_transaction_manager.hpp" + +#include + +namespace duckdb { + +static unique_ptr OdbcAttach(StorageExtensionInfo *storage_info, AttachedDatabase &db, + const string &name, AttachInfo &info, AccessMode access_mode) { + std::cout << "---------------------" << std::endl; + std::cout << "in OdbcStorage::OdbcAttach name=" << info.name << ", path=" << info.path << std::endl; + return make_uniq(db, info.path, access_mode); +} + +static unique_ptr OdbcCreateTransactionManager(StorageExtensionInfo *storage_info, + AttachedDatabase &db, Catalog &catalog) { + auto &odbc_catalog = catalog.Cast(); + return make_uniq(db, odbc_catalog); +} + +OdbcStorageExtension::OdbcStorageExtension() { + attach = OdbcAttach; + create_transaction_manager = OdbcCreateTransactionManager; +} + +} // namespace duckdb diff --git a/src/odbc_utils.cpp b/src/odbc_utils.cpp new file mode 100644 index 0000000..7fd1e9c --- /dev/null +++ b/src/odbc_utils.cpp @@ -0,0 +1,109 @@ +#include "odbc_utils.hpp" + +namespace duckdb { + +// void OdbcUtils::Check(int rc, sqlite3 *db) { +// // if (rc != SQLITE_OK) { +// // throw std::runtime_error(string(sqlite3_errmsg(db))); +// // } +// } + +string OdbcUtils::TypeToString(int sqlite_type) { + // switch (sqlite_type) { + // case SQLITE_ANY: + // return "any"; + // case SQLITE_INTEGER: + // return "integer"; + // case SQLITE_TEXT: + // return "text"; + // case SQLITE_BLOB: + // return "blob"; + // case SQLITE_FLOAT: + // return "float"; + // default: + // return "unknown"; + // } + + return "todo"; +} + +string OdbcUtils::SanitizeString(const string &table_name) { + return StringUtil::Replace(table_name, "'", "''"); +} + +string OdbcUtils::SanitizeIdentifier(const string &table_name) { + return StringUtil::Replace(table_name, "\"", "\"\""); +} + +LogicalType OdbcUtils::ToOdbcType(const LogicalType &input) { + switch (input.id()) { + case LogicalTypeId::BOOLEAN: + case LogicalTypeId::TINYINT: + case LogicalTypeId::SMALLINT: + case LogicalTypeId::INTEGER: + case LogicalTypeId::BIGINT: + case LogicalTypeId::UTINYINT: + case LogicalTypeId::USMALLINT: + case LogicalTypeId::UINTEGER: + return LogicalType::BIGINT; + case LogicalTypeId::FLOAT: + case LogicalTypeId::DOUBLE: + return LogicalType::DOUBLE; + case LogicalTypeId::BLOB: + return LogicalType::BLOB; + default: + return LogicalType::VARCHAR; + } +} + +LogicalType OdbcUtils::TypeToLogicalType(const string &sqlite_type) { + // type affinity rules are taken from here: https://www.sqlite.org/datatype3.html + + // If the declared type contains the string "INT" then it is assigned INTEGER affinity. + if (StringUtil::Contains(sqlite_type, "int")) { + return LogicalType::BIGINT; + } + // If the declared type of the column contains any of the strings "CHAR", "CLOB", or "TEXT" then that column has + // TEXT affinity. Notice that the type VARCHAR contains the string "CHAR" and is thus assigned TEXT affinity. + if (StringUtil::Contains(sqlite_type, "char") || StringUtil::Contains(sqlite_type, "clob") || + StringUtil::Contains(sqlite_type, "text")) { + return LogicalType::VARCHAR; + } + + // If the declared type for a column contains the string "BLOB" or if no type is specified then the column has + // affinity BLOB. + if (StringUtil::Contains(sqlite_type, "blob") || sqlite_type.empty()) { + return LogicalType::BLOB; + } + + // If the declared type for a column contains any of the strings "REAL", "FLOA", or "DOUB" then the column has REAL + // affinity. + if (StringUtil::Contains(sqlite_type, "real") || StringUtil::Contains(sqlite_type, "floa") || + StringUtil::Contains(sqlite_type, "doub")) { + return LogicalType::DOUBLE; + } + // Otherwise, the affinity is NUMERIC. + // now numeric sounds simple, but it is rather complex: + // A column with NUMERIC affinity may contain values using all five storage classes. + // ... + // we add some more extra rules to try to be somewhat sane + + if (sqlite_type == "date") { + return LogicalType::DATE; + } + + // datetime, timestamp + if (StringUtil::Contains(sqlite_type, "time")) { + return LogicalType::TIMESTAMP; + } + + // decimal, numeric + if (StringUtil::Contains(sqlite_type, "dec") || StringUtil::Contains(sqlite_type, "num")) { + return LogicalType::DOUBLE; + } + + // alright, give up and fallback to varchar + return LogicalType::VARCHAR; +} + +} // namespace duckdb diff --git a/src/storage/CMakeLists.txt b/src/storage/CMakeLists.txt new file mode 100644 index 0000000..9d5fb6f --- /dev/null +++ b/src/storage/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library( + odbc_ext_storage OBJECT + odbc_catalog.cpp + odbc_delete.cpp + odbc_index.cpp + odbc_index_entry.cpp + odbc_insert.cpp + odbc_table_entry.cpp + odbc_schema_entry.cpp + odbc_transaction.cpp + odbc_transaction_manager.cpp + odbc_update.cpp +) +set( + ALL_OBJECT_FILES + ${ALL_OBJECT_FILES} $ + PARENT_SCOPE +) diff --git a/src/storage/odbc_catalog.cpp b/src/storage/odbc_catalog.cpp new file mode 100644 index 0000000..b22050f --- /dev/null +++ b/src/storage/odbc_catalog.cpp @@ -0,0 +1,89 @@ +#include "storage/odbc_catalog.hpp" +#include "duckdb/storage/database_size.hpp" +#include "odbc_db.hpp" +#include "storage/odbc_schema_entry.hpp" +#include "storage/odbc_transaction.hpp" + +namespace duckdb { + +OdbcCatalog::OdbcCatalog(AttachedDatabase &db_p, const string &path, AccessMode access_mode) + : Catalog(db_p), path(path), access_mode(access_mode), in_memory(path == ":memory:"), + active_in_memory(false) { + if (InMemory()) { + in_memory_db = OdbcDB::Open(path, false, true); + } +} + +OdbcCatalog::~OdbcCatalog() {} + +void OdbcCatalog::Initialize(bool load_builtin) { main_schema = make_uniq(*this); } + +optional_ptr OdbcCatalog::CreateSchema(CatalogTransaction transaction, CreateSchemaInfo &info) { + throw BinderException("Odbc databases do not support creating new schemas"); +} + +void OdbcCatalog::ScanSchemas(ClientContext &context, std::function callback) { + callback(*main_schema); +} + +optional_ptr OdbcCatalog::GetSchema(CatalogTransaction transaction, + const string &schema_name, + OnEntryNotFound if_not_found, + QueryErrorContext error_context) { + if (schema_name == DEFAULT_SCHEMA || schema_name == INVALID_SCHEMA) { + return main_schema.get(); + } + if (if_not_found == OnEntryNotFound::RETURN_NULL) { + return nullptr; + } + throw BinderException("Odbc databases only have a single schema - \"%s\"", DEFAULT_SCHEMA); +} + +bool OdbcCatalog::InMemory() { return in_memory; } + +string OdbcCatalog::GetDBPath() { return path; } + +OdbcDB *OdbcCatalog::GetInMemoryDatabase() { + if (!InMemory()) { + throw InternalException("GetInMemoryDatabase() called on a non-in-memory database"); + } + lock_guard l(in_memory_lock); + if (active_in_memory) { + throw TransactionException( + "Only a single transaction can be active on an in-memory Odbc database at a time"); + } + active_in_memory = true; + return &in_memory_db; +} + +void OdbcCatalog::ReleaseInMemoryDatabase() { + if (!InMemory()) { + return; + } + lock_guard l(in_memory_lock); + if (!active_in_memory) { + throw InternalException( + "ReleaseInMemoryDatabase called but there is no active transaction on an in-memory database"); + } + active_in_memory = false; +} + +void OdbcCatalog::DropSchema(ClientContext &context, DropInfo &info) { + throw BinderException("Odbc databases do not support dropping schemas"); +} + +DatabaseSize OdbcCatalog::GetDatabaseSize(ClientContext &context) { + DatabaseSize result; + + auto &transaction = OdbcTransaction::Get(context, *this); + auto &db = transaction.GetDB(); + result.total_blocks = db.RunPragma("page_count"); + result.block_size = db.RunPragma("page_size"); + result.free_blocks = db.RunPragma("freelist_count"); + result.used_blocks = result.total_blocks - result.free_blocks; + result.bytes = result.total_blocks * result.block_size; + result.wal_size = idx_t(-1); + return result; +} + +} // namespace duckdb diff --git a/src/storage/odbc_delete.cpp b/src/storage/odbc_delete.cpp new file mode 100644 index 0000000..a93489f --- /dev/null +++ b/src/storage/odbc_delete.cpp @@ -0,0 +1,97 @@ +#include "storage/odbc_delete.hpp" +#include "storage/odbc_table_entry.hpp" +#include "duckdb/planner/operator/logical_delete.hpp" +#include "storage/odbc_catalog.hpp" +#include "storage/odbc_transaction.hpp" +#include "odbc_db.hpp" +#include "odbc_stmt.hpp" + +namespace duckdb { + +OdbcDelete::OdbcDelete(LogicalOperator &op, TableCatalogEntry &table) + : PhysicalOperator(PhysicalOperatorType::EXTENSION, op.types, 1), table(table) { +} + +//===--------------------------------------------------------------------===// +// States +//===--------------------------------------------------------------------===// +class OdbcDeleteGlobalState : public GlobalSinkState { +public: + explicit OdbcDeleteGlobalState(OdbcTableEntry &table) : table(table), delete_count(0) { + } + + OdbcTableEntry &table; + OdbcStatement statement; + idx_t delete_count; +}; + +string GetDeleteSQL(const string &table_name) { + string result; + result = "DELETE FROM " + KeywordHelper::WriteOptionallyQuoted(table_name); + result += " WHERE rowid = ?"; + return result; +} + +unique_ptr OdbcDelete::GetGlobalSinkState(ClientContext &context) const { + auto &sqlite_table = table.Cast(); + + auto &transaction = OdbcTransaction::Get(context, sqlite_table.catalog); + auto result = make_uniq(sqlite_table); + result->statement = transaction.GetDB().Prepare(GetDeleteSQL(sqlite_table.name)); + return std::move(result); +} + +//===--------------------------------------------------------------------===// +// Sink +//===--------------------------------------------------------------------===// +SinkResultType OdbcDelete::Sink(ExecutionContext &context, DataChunk &chunk, OperatorSinkInput &input) const { + auto &gstate = input.global_state.Cast(); + + chunk.Flatten(); + auto &row_identifiers = chunk.data[0]; + auto row_data = FlatVector::GetData(row_identifiers); + for (idx_t i = 0; i < chunk.size(); i++) { + gstate.statement.Bind(0, row_data[i]); + gstate.statement.Step(); + gstate.statement.Reset(); + } + gstate.delete_count += chunk.size(); + return SinkResultType::NEED_MORE_INPUT; +} + +//===--------------------------------------------------------------------===// +// GetData +//===--------------------------------------------------------------------===// +SourceResultType OdbcDelete::GetData(ExecutionContext &context, DataChunk &chunk, OperatorSourceInput &input) const { + auto &insert_gstate = sink_state->Cast(); + chunk.SetCardinality(1); + chunk.SetValue(0, 0, Value::BIGINT(insert_gstate.delete_count)); + + return SourceResultType::FINISHED; +} + +//===--------------------------------------------------------------------===// +// Helpers +//===--------------------------------------------------------------------===// +string OdbcDelete::GetName() const { + return "DELETE"; +} + +string OdbcDelete::ParamsToString() const { + return table.name; +} + +//===--------------------------------------------------------------------===// +// Plan +//===--------------------------------------------------------------------===// +unique_ptr OdbcCatalog::PlanDelete(ClientContext &context, LogicalDelete &op, + unique_ptr plan) { + if (op.return_chunk) { + throw BinderException("RETURNING clause not yet supported for deletion of a Odbc table"); + } + auto insert = make_uniq(op, op.table); + insert->children.push_back(std::move(plan)); + return std::move(insert); +} + +} // namespace duckdb diff --git a/src/storage/odbc_index.cpp b/src/storage/odbc_index.cpp new file mode 100644 index 0000000..6b31da0 --- /dev/null +++ b/src/storage/odbc_index.cpp @@ -0,0 +1,56 @@ +#include "storage/odbc_catalog.hpp" +#include "storage/odbc_index.hpp" +#include "duckdb/parser/statement/create_statement.hpp" +#include "duckdb/planner/operator/logical_extension_operator.hpp" +#include "duckdb/catalog/catalog_entry/table_catalog_entry.hpp" + +namespace duckdb { + +OdbcCreateIndex::OdbcCreateIndex(unique_ptr info, TableCatalogEntry &table) + : PhysicalOperator(PhysicalOperatorType::EXTENSION, {LogicalType::BIGINT}, 1), info(std::move(info)), table(table) { +} + +//===--------------------------------------------------------------------===// +// Source +//===--------------------------------------------------------------------===// +SourceResultType OdbcCreateIndex::GetData(ExecutionContext &context, DataChunk &chunk, + OperatorSourceInput &input) const { + auto &catalog = table.catalog; + auto &schema = catalog.GetSchema(context.client, info->schema); + schema.CreateIndex(context.client, *info, table); + + return SourceResultType::FINISHED; +} + +//===--------------------------------------------------------------------===// +// Logical Operator +//===--------------------------------------------------------------------===// +class LogicalOdbcCreateIndex : public LogicalExtensionOperator { +public: + LogicalOdbcCreateIndex(unique_ptr info_p, TableCatalogEntry &table) + : info(std::move(info_p)), table(table) { + } + + unique_ptr info; + TableCatalogEntry &table; + + unique_ptr CreatePlan(ClientContext &context, PhysicalPlanGenerator &generator) override { + return make_uniq(std::move(info), table); + } + + void Serialize(FieldWriter &writer) const override { + throw InternalException("Cannot serialize Odbc Create index"); + } + + void ResolveTypes() override { + types = {LogicalType::BIGINT}; + } +}; + +unique_ptr OdbcCatalog::BindCreateIndex(Binder &binder, CreateStatement &stmt, + TableCatalogEntry &table, unique_ptr plan) { + return make_uniq(unique_ptr_cast(std::move(stmt.info)), + table); +} + +} // namespace duckdb diff --git a/src/storage/odbc_index_entry.cpp b/src/storage/odbc_index_entry.cpp new file mode 100644 index 0000000..3c9cf91 --- /dev/null +++ b/src/storage/odbc_index_entry.cpp @@ -0,0 +1,14 @@ +#include "storage/odbc_index_entry.hpp" +#include "duckdb/catalog/catalog_entry/schema_catalog_entry.hpp" + +namespace duckdb { + +OdbcIndexEntry::OdbcIndexEntry(Catalog &catalog, SchemaCatalogEntry &schema, CreateIndexInfo &info, + string table_name_p) + : IndexCatalogEntry(catalog, schema, info), table_name(std::move(table_name_p)) {} + +string OdbcIndexEntry::GetSchemaName() const { return schema.name; } + +string OdbcIndexEntry::GetTableName() const { return table_name; } + +} // namespace duckdb diff --git a/src/storage/odbc_insert.cpp b/src/storage/odbc_insert.cpp new file mode 100644 index 0000000..85dc8e4 --- /dev/null +++ b/src/storage/odbc_insert.cpp @@ -0,0 +1,204 @@ +#include "storage/odbc_insert.hpp" +#include "storage/odbc_catalog.hpp" +#include "storage/odbc_transaction.hpp" +#include "duckdb/planner/operator/logical_insert.hpp" +#include "duckdb/planner/operator/logical_create_table.hpp" +#include "storage/odbc_table_entry.hpp" +#include "duckdb/planner/parsed_data/bound_create_table_info.hpp" +#include "duckdb/execution/operator/projection/physical_projection.hpp" +#include "duckdb/planner/expression/bound_cast_expression.hpp" +#include "duckdb/planner/expression/bound_reference_expression.hpp" +#include "odbc_db.hpp" +#include "odbc_stmt.hpp" + +namespace duckdb { + +OdbcInsert::OdbcInsert(LogicalOperator &op, TableCatalogEntry &table, + physical_index_vector_t column_index_map_p) + : PhysicalOperator(PhysicalOperatorType::EXTENSION, op.types, 1), table(&table), schema(nullptr), + column_index_map(std::move(column_index_map_p)) { +} + +OdbcInsert::OdbcInsert(LogicalOperator &op, SchemaCatalogEntry &schema, unique_ptr info) + : PhysicalOperator(PhysicalOperatorType::EXTENSION, op.types, 1), table(nullptr), schema(&schema), + info(std::move(info)) { +} + +//===--------------------------------------------------------------------===// +// States +//===--------------------------------------------------------------------===// +class OdbcInsertGlobalState : public GlobalSinkState { +public: + explicit OdbcInsertGlobalState(ClientContext &context, OdbcTableEntry *table) : insert_count(0) { + } + + OdbcTableEntry *table; + OdbcStatement statement; + idx_t insert_count; +}; + +string GetInsertSQL(const OdbcInsert &insert, OdbcTableEntry *entry) { + string result; + result = "INSERT INTO " + KeywordHelper::WriteOptionallyQuoted(entry->name); + auto &columns = entry->GetColumns(); + idx_t column_count; + if (!insert.column_index_map.empty()) { + column_count = 0; + result += " ("; + vector column_indexes; + column_indexes.resize(columns.LogicalColumnCount(), PhysicalIndex(DConstants::INVALID_INDEX)); + for (idx_t c = 0; c < insert.column_index_map.size(); c++) { + auto column_index = PhysicalIndex(c); + auto mapped_index = insert.column_index_map[column_index]; + if (mapped_index == DConstants::INVALID_INDEX) { + // column not specified + continue; + } + column_indexes[mapped_index] = column_index; + column_count++; + } + for (idx_t c = 0; c < column_count; c++) { + if (c > 0) { + result += ", "; + } + auto &col = columns.GetColumn(column_indexes[c]); + result += KeywordHelper::WriteOptionallyQuoted(col.GetName()); + } + result += ")"; + } else { + column_count = columns.LogicalColumnCount(); + } + result += " VALUES ("; + for (idx_t i = 0; i < column_count; i++) { + if (i > 0) { + result += ", "; + } + result += "?"; + } + result += ");"; + return result; +} + +unique_ptr OdbcInsert::GetGlobalSinkState(ClientContext &context) const { + OdbcTableEntry *insert_table; + if (!table) { + auto &schema_ref = *schema.get_mutable(); + insert_table = + &schema_ref.CreateTable(schema_ref.GetCatalogTransaction(context), *info)->Cast(); + } else { + insert_table = &table.get_mutable()->Cast(); + } + auto &transaction = OdbcTransaction::Get(context, insert_table->catalog); + auto result = make_uniq(context, insert_table); + result->statement = transaction.GetDB().Prepare(GetInsertSQL(*this, insert_table)); + return std::move(result); +} + +//===--------------------------------------------------------------------===// +// Sink +//===--------------------------------------------------------------------===// +SinkResultType OdbcInsert::Sink(ExecutionContext &context, DataChunk &chunk, OperatorSinkInput &input) const { + auto &gstate = sink_state->Cast(); + chunk.Flatten(); + auto &stmt = gstate.statement; + for (idx_t r = 0; r < chunk.size(); r++) { + for (idx_t c = 0; c < chunk.ColumnCount(); c++) { + auto &col = chunk.data[c]; + stmt.BindValue(col, c, r); + } + // execute and clear bindings + stmt.Step(); + stmt.Reset(); + } + gstate.insert_count += chunk.size(); + return SinkResultType::NEED_MORE_INPUT; +} + +//===--------------------------------------------------------------------===// +// GetData +//===--------------------------------------------------------------------===// +SourceResultType OdbcInsert::GetData(ExecutionContext &context, DataChunk &chunk, OperatorSourceInput &input) const { + auto &insert_gstate = sink_state->Cast(); + chunk.SetCardinality(1); + chunk.SetValue(0, 0, Value::BIGINT(insert_gstate.insert_count)); + + return SourceResultType::FINISHED; +} + +//===--------------------------------------------------------------------===// +// Helpers +//===--------------------------------------------------------------------===// +string OdbcInsert::GetName() const { + return table ? "INSERT" : "CREATE_TABLE_AS"; +} + +string OdbcInsert::ParamsToString() const { + return table ? table->name : info->Base().table; +} + +//===--------------------------------------------------------------------===// +// Plan +//===--------------------------------------------------------------------===// +unique_ptr AddCastToOdbcTypes(ClientContext &context, unique_ptr plan) { + // check if we need to cast anything + bool require_cast = false; + auto &child_types = plan->GetTypes(); + for (auto &type : child_types) { + auto sqlite_type = OdbcUtils::ToOdbcType(type); + if (sqlite_type != type) { + require_cast = true; + break; + } + } + if (require_cast) { + vector sqlite_types; + vector> select_list; + for (idx_t i = 0; i < child_types.size(); i++) { + auto &type = child_types[i]; + unique_ptr expr; + expr = make_uniq(type, i); + + auto sqlite_type = OdbcUtils::ToOdbcType(type); + if (sqlite_type != type) { + // add a cast + expr = BoundCastExpression::AddCastToType(context, std::move(expr), sqlite_type); + } + sqlite_types.push_back(std::move(sqlite_type)); + select_list.push_back(std::move(expr)); + } + // we need to cast: add casts + auto proj = + make_uniq(std::move(sqlite_types), std::move(select_list), plan->estimated_cardinality); + proj->children.push_back(std::move(plan)); + plan = std::move(proj); + } + + return plan; +} + +unique_ptr OdbcCatalog::PlanInsert(ClientContext &context, LogicalInsert &op, + unique_ptr plan) { + if (op.return_chunk) { + throw BinderException("RETURNING clause not yet supported for insertion into Odbc table"); + } + if (op.action_type != OnConflictAction::THROW) { + throw BinderException("ON CONFLICT clause not yet supported for insertion into Odbc table"); + } + + plan = AddCastToOdbcTypes(context, std::move(plan)); + + auto insert = make_uniq(op, op.table, op.column_index_map); + insert->children.push_back(std::move(plan)); + return std::move(insert); +} + +unique_ptr OdbcCatalog::PlanCreateTableAs(ClientContext &context, LogicalCreateTable &op, + unique_ptr plan) { + plan = AddCastToOdbcTypes(context, std::move(plan)); + + auto insert = make_uniq(op, op.schema, std::move(op.info)); + insert->children.push_back(std::move(plan)); + return std::move(insert); +} + +} // namespace duckdb diff --git a/src/storage/odbc_schema_entry.cpp b/src/storage/odbc_schema_entry.cpp new file mode 100644 index 0000000..298e487 --- /dev/null +++ b/src/storage/odbc_schema_entry.cpp @@ -0,0 +1,304 @@ +#include "storage/odbc_schema_entry.hpp" +#include "storage/odbc_table_entry.hpp" +#include "storage/odbc_transaction.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/parsed_data/create_index_info.hpp" +#include "duckdb/planner/parsed_data/bound_create_table_info.hpp" +#include "duckdb/parser/parsed_data/drop_info.hpp" +#include "duckdb/parser/constraints/list.hpp" +#include "duckdb/common/unordered_set.hpp" +#include "duckdb/parser/parsed_data/alter_info.hpp" +#include "duckdb/parser/parsed_data/alter_table_info.hpp" +#include "duckdb/parser/parsed_expression_iterator.hpp" + +namespace duckdb { + +OdbcSchemaEntry::OdbcSchemaEntry(Catalog &catalog) : SchemaCatalogEntry(catalog, DEFAULT_SCHEMA, true) { +} + +OdbcTransaction &GetOdbcTransaction(CatalogTransaction transaction) { + if (!transaction.transaction) { + throw InternalException("No transaction!?"); + } + return transaction.transaction->Cast(); +} + +string GetCreateTableSQL(CreateTableInfo &info) { + for (idx_t i = 0; i < info.columns.LogicalColumnCount(); i++) { + auto &col = info.columns.GetColumnMutable(LogicalIndex(i)); + col.SetType(OdbcUtils::ToOdbcType(col.GetType())); + } + + std::stringstream ss; + ss << "CREATE TABLE "; + if (info.on_conflict == OnCreateConflict::IGNORE_ON_CONFLICT) { + ss << "IF NOT EXISTS "; + } + ss << KeywordHelper::WriteOptionallyQuoted(info.table); + ss << TableCatalogEntry::ColumnsToSQL(info.columns, info.constraints); + ss << ";"; + return ss.str(); +} + +void OdbcSchemaEntry::TryDropEntry(ClientContext &context, CatalogType catalog_type, const string &name) { + DropInfo info; + info.type = catalog_type; + info.name = name; + info.cascade = false; + info.if_not_found = OnEntryNotFound::RETURN_NULL; + DropEntry(context, info); +} + +optional_ptr OdbcSchemaEntry::CreateTable(CatalogTransaction transaction, BoundCreateTableInfo &info) { + auto &sqlite_transaction = GetOdbcTransaction(transaction); + auto &base_info = info.Base(); + auto table_name = base_info.table; + if (base_info.on_conflict == OnCreateConflict::REPLACE_ON_CONFLICT) { + // CREATE OR REPLACE - drop any existing entries first (if any) + TryDropEntry(transaction.GetContext(), CatalogType::TABLE_ENTRY, table_name); + } + + sqlite_transaction.GetDB().Execute(GetCreateTableSQL(base_info)); + return GetEntry(transaction, CatalogType::TABLE_ENTRY, table_name); +} + +optional_ptr OdbcSchemaEntry::CreateFunction(CatalogTransaction transaction, CreateFunctionInfo &info) { + throw BinderException("Odbc databases do not support creating functions"); +} + +void UnqualifyColumnReferences(ParsedExpression &expr) { + if (expr.type == ExpressionType::COLUMN_REF) { + auto &colref = expr.Cast(); + auto name = std::move(colref.column_names.back()); + colref.column_names = {std::move(name)}; + return; + } + ParsedExpressionIterator::EnumerateChildren(expr, UnqualifyColumnReferences); +} + +string GetCreateIndexSQL(CreateIndexInfo &info, TableCatalogEntry &tbl) { + string sql; + sql = "CREATE"; + if (info.constraint_type == IndexConstraintType::UNIQUE) { + sql += " UNIQUE"; + } + sql += " INDEX "; + sql += KeywordHelper::WriteOptionallyQuoted(info.index_name); + sql += " ON "; + sql += KeywordHelper::WriteOptionallyQuoted(tbl.name); + sql += "("; + for (idx_t i = 0; i < info.parsed_expressions.size(); i++) { + if (i > 0) { + sql += ", "; + } + UnqualifyColumnReferences(*info.parsed_expressions[i]); + sql += info.parsed_expressions[i]->ToString(); + } + sql += ")"; + return sql; +} + +optional_ptr OdbcSchemaEntry::CreateIndex(ClientContext &context, CreateIndexInfo &info, + TableCatalogEntry &table) { + auto &sqlite_transaction = OdbcTransaction::Get(context, table.catalog); + sqlite_transaction.GetDB().Execute(GetCreateIndexSQL(info, table)); + return nullptr; +} + +string GetCreateViewSQL(CreateViewInfo &info) { + string sql; + sql = "CREATE VIEW "; + if (info.on_conflict == OnCreateConflict::IGNORE_ON_CONFLICT) { + sql += "IF NOT EXISTS "; + } + sql += KeywordHelper::WriteOptionallyQuoted(info.view_name); + sql += " "; + if (!info.aliases.empty()) { + sql += "("; + for (idx_t i = 0; i < info.aliases.size(); i++) { + if (i > 0) { + sql += ", "; + } + auto &alias = info.aliases[i]; + sql += KeywordHelper::WriteOptionallyQuoted(alias); + } + sql += ") "; + } + sql += "AS "; + sql += info.query->ToString(); + return sql; +} + +optional_ptr OdbcSchemaEntry::CreateView(CatalogTransaction transaction, CreateViewInfo &info) { + if (info.sql.empty()) { + throw BinderException("Cannot create view in Odbc that originated from an empty SQL statement"); + } + if (info.on_conflict == OnCreateConflict::REPLACE_ON_CONFLICT) { + // CREATE OR REPLACE - drop any existing entries first (if any) + TryDropEntry(transaction.GetContext(), CatalogType::VIEW_ENTRY, info.view_name); + } + auto &sqlite_transaction = GetOdbcTransaction(transaction); + sqlite_transaction.GetDB().Execute(GetCreateViewSQL(info)); + return GetEntry(transaction, CatalogType::VIEW_ENTRY, info.view_name); +} + +optional_ptr OdbcSchemaEntry::CreateSequence(CatalogTransaction transaction, CreateSequenceInfo &info) { + throw BinderException("Odbc databases do not support creating sequences"); +} + +optional_ptr OdbcSchemaEntry::CreateTableFunction(CatalogTransaction transaction, + CreateTableFunctionInfo &info) { + throw BinderException("Odbc databases do not support creating table functions"); +} + +optional_ptr OdbcSchemaEntry::CreateCopyFunction(CatalogTransaction transaction, + CreateCopyFunctionInfo &info) { + throw BinderException("Odbc databases do not support creating copy functions"); +} + +optional_ptr OdbcSchemaEntry::CreatePragmaFunction(CatalogTransaction transaction, + CreatePragmaFunctionInfo &info) { + throw BinderException("Odbc databases do not support creating pragma functions"); +} + +optional_ptr OdbcSchemaEntry::CreateCollation(CatalogTransaction transaction, + CreateCollationInfo &info) { + throw BinderException("Odbc databases do not support creating collations"); +} + +optional_ptr OdbcSchemaEntry::CreateType(CatalogTransaction transaction, CreateTypeInfo &info) { + throw BinderException("Odbc databases do not support creating types"); +} + +void OdbcSchemaEntry::AlterTable(OdbcTransaction &sqlite_transaction, RenameTableInfo &info) { + string sql = "ALTER TABLE "; + sql += KeywordHelper::WriteOptionallyQuoted(info.name); + sql += " RENAME TO "; + sql += KeywordHelper::WriteOptionallyQuoted(info.new_table_name); + sqlite_transaction.GetDB().Execute(sql); +} + +void OdbcSchemaEntry::AlterTable(OdbcTransaction &sqlite_transaction, RenameColumnInfo &info) { + string sql = "ALTER TABLE "; + sql += KeywordHelper::WriteOptionallyQuoted(info.name); + sql += " RENAME COLUMN "; + sql += KeywordHelper::WriteOptionallyQuoted(info.old_name); + sql += " TO "; + sql += KeywordHelper::WriteOptionallyQuoted(info.new_name); + sqlite_transaction.GetDB().Execute(sql); +} + +void OdbcSchemaEntry::AlterTable(OdbcTransaction &sqlite_transaction, AddColumnInfo &info) { + if (info.if_column_not_exists) { + if (sqlite_transaction.GetDB().ColumnExists(info.name, info.new_column.GetName())) { + return; + } + } + string sql = "ALTER TABLE "; + sql += KeywordHelper::WriteOptionallyQuoted(info.name); + sql += " ADD COLUMN "; + sql += KeywordHelper::WriteOptionallyQuoted(info.new_column.Name()); + sql += " "; + sql += info.new_column.Type().ToString(); + sqlite_transaction.GetDB().Execute(sql); +} + +void OdbcSchemaEntry::AlterTable(OdbcTransaction &sqlite_transaction, RemoveColumnInfo &info) { + if (info.if_column_exists) { + if (!sqlite_transaction.GetDB().ColumnExists(info.name, info.removed_column)) { + return; + } + } + string sql = "ALTER TABLE "; + sql += KeywordHelper::WriteOptionallyQuoted(info.name); + sql += " DROP COLUMN "; + sql += KeywordHelper::WriteOptionallyQuoted(info.removed_column); + sqlite_transaction.GetDB().Execute(sql); +} + +void OdbcSchemaEntry::Alter(ClientContext &context, AlterInfo &info) { + if (info.type != AlterType::ALTER_TABLE) { + throw BinderException("Only altering tables is supported for now"); + } + auto &alter = info.Cast(); + auto &transaction = OdbcTransaction::Get(context, catalog); + switch (alter.alter_table_type) { + case AlterTableType::RENAME_TABLE: + AlterTable(transaction, alter.Cast()); + break; + case AlterTableType::RENAME_COLUMN: + AlterTable(transaction, alter.Cast()); + break; + case AlterTableType::ADD_COLUMN: + AlterTable(transaction, alter.Cast()); + break; + case AlterTableType::REMOVE_COLUMN: + AlterTable(transaction, alter.Cast()); + break; + default: + throw BinderException("Unsupported ALTER TABLE type - Odbc tables only support RENAME TABLE, RENAME COLUMN, " + "ADD COLUMN and DROP COLUMN"); + } + transaction.ClearTableEntry(info.name); +} + +void OdbcSchemaEntry::Scan(ClientContext &context, CatalogType type, + const std::function &callback) { + auto &transaction = OdbcTransaction::Get(context, catalog); + vector entries; + switch (type) { + case CatalogType::TABLE_ENTRY: + entries = transaction.GetDB().GetTables(); + break; + case CatalogType::VIEW_ENTRY: + entries = transaction.GetDB().GetEntries("view"); + break; + case CatalogType::INDEX_ENTRY: + entries = transaction.GetDB().GetEntries("index"); + break; + default: + // no entries of this catalog type + return; + } + for (auto &entry_name : entries) { + callback(*GetEntry(GetCatalogTransaction(context), type, entry_name)); + } +} +void OdbcSchemaEntry::Scan(CatalogType type, const std::function &callback) { + throw InternalException("Scan"); +} + +void OdbcSchemaEntry::DropEntry(ClientContext &context, DropInfo &info) { + switch (info.type) { + case CatalogType::TABLE_ENTRY: + case CatalogType::VIEW_ENTRY: + case CatalogType::INDEX_ENTRY: + break; + default: + throw BinderException("Odbc databases do not support dropping entries of type \"%s\"", + CatalogTypeToString(type)); + } + auto table = GetEntry(GetCatalogTransaction(context), info.type, info.name); + if (!table) { + throw InternalException("Failed to drop entry \"%s\" - could not find entry", info.name); + } + auto &transaction = OdbcTransaction::Get(context, catalog); + transaction.DropEntry(info.type, info.name, info.cascade); +} + +optional_ptr OdbcSchemaEntry::GetEntry(CatalogTransaction transaction, CatalogType type, + const string &name) { + auto &odbc_transaction = GetOdbcTransaction(transaction); + switch (type) { + case CatalogType::INDEX_ENTRY: + case CatalogType::TABLE_ENTRY: + case CatalogType::VIEW_ENTRY: + return odbc_transaction.GetCatalogEntry(name); + default: + return nullptr; + } +} + +} // namespace duckdb diff --git a/src/storage/odbc_table_entry.cpp b/src/storage/odbc_table_entry.cpp new file mode 100644 index 0000000..69c6763 --- /dev/null +++ b/src/storage/odbc_table_entry.cpp @@ -0,0 +1,98 @@ +#include "storage/odbc_table_entry.hpp" +#include "duckdb/storage/statistics/base_statistics.hpp" +#include "duckdb/storage/table_storage_info.hpp" +#include "odbc_scan.hpp" +#include "storage/odbc_catalog.hpp" +#include "storage/odbc_transaction.hpp" + +namespace duckdb { + +OdbcTableEntry::OdbcTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, CreateTableInfo &info) + : TableCatalogEntry(catalog, schema, info) {} + +unique_ptr OdbcTableEntry::GetStatistics(ClientContext &context, column_t column_id) { + return nullptr; +} + +void OdbcTableEntry::BindUpdateConstraints(LogicalGet &, LogicalProjection &, LogicalUpdate &, + ClientContext &) {} + +TableFunction OdbcTableEntry::GetScanFunction(ClientContext &context, unique_ptr &bind_data) { + // auto result = make_uniq(); + // for (auto &col : columns.Logical()) { + // result->names.push_back(col.GetName()); + // result->types.push_back(col.GetType()); + // } + // auto &odbc_catalog = catalog.Cast(); + // result->file_name = odbc_catalog.path; + // result->table_name = name; + // + // auto &transaction = Transaction::Get(context, catalog).Cast(); + // auto &db = transaction.GetDB(); + // + // if (!db.GetMaxRowId(name, result->max_rowid)) { + // result->max_rowid = idx_t(-1); + // result->rows_per_group = idx_t(-1); + // } + // if (!transaction.IsReadOnly() || odbc_catalog.InMemory()) { + // // for in-memory databases or if we have transaction-local changes we can only do a single-threaded + // scan + // // set up the transaction's connection object as the global db + // result->global_db = &db; + // result->rows_per_group = idx_t(-1); + // } + // + // bind_data = std::move(result); + // return SqliteScanFunction(); + + // ---------------------- + // + + // return OdbcScanFunction(); + + // ---------------------- + // + std::cout << "---------------------" << std::endl; + std::cout << "in OdbcTableEntry::GetScanFunction" << std::endl; + + auto result = make_uniq(); + for (auto &col : columns.Logical()) { + result->names.push_back(col.GetName()); + result->types.push_back(col.GetType()); + } + // auto &odbc_catalog = catalog.Cast(); + // result->file_name = odbc_catalog.path; + // result->table_name = name; + // + // auto &transaction = Transaction::Get(context, catalog).Cast(); + // auto &db = transaction.GetDB(); + // + // if (!db.GetMaxRowId(name, result->max_rowid)) { + // result->max_rowid = idx_t(-1); + // result->rows_per_group = idx_t(-1); + // } + // if (!transaction.IsReadOnly() || odbc_catalog.InMemory()) { + // // for in-memory databases or if we have transaction-local changes we can only do a single-threaded + // scan + // // set up the transaction's connection object as the global db + // result->global_db = &db; + // result->rows_per_group = idx_t(-1); + // } + + bind_data = std::move(result); + return OdbcScanFunction(); +} + +TableStorageInfo OdbcTableEntry::GetStorageInfo(ClientContext &context) { + auto &transaction = Transaction::Get(context, catalog).Cast(); + auto &db = transaction.GetDB(); + TableStorageInfo result; + if (!db.GetMaxRowId(name, result.cardinality)) { + // probably + result.cardinality = 10000; + } + result.index_info = db.GetIndexInfo(name); + return result; +} + +} // namespace duckdb diff --git a/src/storage/odbc_transaction.cpp b/src/storage/odbc_transaction.cpp new file mode 100644 index 0000000..f4a5f1a --- /dev/null +++ b/src/storage/odbc_transaction.cpp @@ -0,0 +1,119 @@ +#include "storage/odbc_transaction.hpp" +#include "duckdb/catalog/catalog_entry/index_catalog_entry.hpp" +#include "duckdb/catalog/catalog_entry/view_catalog_entry.hpp" +#include "duckdb/parser/parsed_data/create_table_info.hpp" +#include "duckdb/parser/parsed_data/create_view_info.hpp" +#include "storage/odbc_catalog.hpp" +#include "storage/odbc_index_entry.hpp" +#include "storage/odbc_schema_entry.hpp" +#include "storage/odbc_table_entry.hpp" + +namespace duckdb { + +OdbcTransaction::OdbcTransaction(OdbcCatalog &odbc_catalog, TransactionManager &manager, + ClientContext &context) + : Transaction(manager, context), odbc_catalog(odbc_catalog) { + if (odbc_catalog.InMemory()) { + // in-memory database - get a reference to the in-memory connection + db = odbc_catalog.GetInMemoryDatabase(); + } else { + // on-disk database - open a new database connection + owned_db = OdbcDB::Open(odbc_catalog.path, + odbc_catalog.access_mode == AccessMode::READ_ONLY ? true : false, true); + db = &owned_db; + } +} + +OdbcTransaction::~OdbcTransaction() { odbc_catalog.ReleaseInMemoryDatabase(); } + +void OdbcTransaction::Start() { db->Execute("BEGIN TRANSACTION"); } +void OdbcTransaction::Commit() { db->Execute("COMMIT"); } +void OdbcTransaction::Rollback() { db->Execute("ROLLBACK"); } + +OdbcDB &OdbcTransaction::GetDB() { return *db; } + +OdbcTransaction &OdbcTransaction::Get(ClientContext &context, Catalog &catalog) { + return Transaction::Get(context, catalog).Cast(); +} + +optional_ptr OdbcTransaction::GetCatalogEntry(const string &entry_name) { + auto entry = catalog_entries.find(entry_name); + if (entry != catalog_entries.end()) { + return entry->second.get(); + } + // catalog entry not found - look up table in main Odbc database + auto type = db->GetEntryType(entry_name); + if (type == CatalogType::INVALID) { + // no table or view found + return nullptr; + } + unique_ptr result; + switch (type) { + case CatalogType::TABLE_ENTRY: { + CreateTableInfo info(odbc_catalog.GetMainSchema(), entry_name); + // FIXME: all_varchar from config + db->GetTableInfo(entry_name, info.columns, info.constraints, false); + D_ASSERT(!info.columns.empty()); + + result = make_uniq(odbc_catalog, odbc_catalog.GetMainSchema(), info); + break; + } + case CatalogType::VIEW_ENTRY: { + string sql; + db->GetViewInfo(entry_name, sql); + + auto view_info = CreateViewInfo::FromCreateView(*context.lock(), sql); + view_info->internal = false; + result = make_uniq(odbc_catalog, odbc_catalog.GetMainSchema(), *view_info); + break; + } + case CatalogType::INDEX_ENTRY: { + CreateIndexInfo info; + info.index_name = entry_name; + + string table_name; + string sql; + db->GetIndexInfo(entry_name, sql, table_name); + + auto index_entry = + make_uniq(odbc_catalog, odbc_catalog.GetMainSchema(), info, std::move(table_name)); + index_entry->sql = std::move(sql); + result = std::move(index_entry); + break; + } + default: + throw InternalException("Unrecognized catalog entry type"); + } + auto result_ptr = result.get(); + catalog_entries[entry_name] = std::move(result); + return result_ptr; +} + +void OdbcTransaction::ClearTableEntry(const string &table_name) { catalog_entries.erase(table_name); } + +string GetDropSQL(CatalogType type, const string &table_name, bool cascade) { + string result; + result = "DROP "; + switch (type) { + case CatalogType::TABLE_ENTRY: + result += "TABLE "; + break; + case CatalogType::VIEW_ENTRY: + result += "VIEW "; + break; + case CatalogType::INDEX_ENTRY: + result += "INDEX "; + break; + default: + throw InternalException("Unsupported type for drop"); + } + result += KeywordHelper::WriteOptionallyQuoted(table_name); + return result; +} + +void OdbcTransaction::DropEntry(CatalogType type, const string &table_name, bool cascade) { + catalog_entries.erase(table_name); + db->Execute(GetDropSQL(type, table_name, cascade)); +} + +} // namespace duckdb diff --git a/src/storage/odbc_transaction_manager.cpp b/src/storage/odbc_transaction_manager.cpp new file mode 100644 index 0000000..66af71f --- /dev/null +++ b/src/storage/odbc_transaction_manager.cpp @@ -0,0 +1,40 @@ +#include "storage/odbc_transaction_manager.hpp" +#include "duckdb/main/attached_database.hpp" + +namespace duckdb { + +OdbcTransactionManager::OdbcTransactionManager(AttachedDatabase &db_p, OdbcCatalog &sqlite_catalog) + : TransactionManager(db_p), sqlite_catalog(sqlite_catalog) { +} + +Transaction *OdbcTransactionManager::StartTransaction(ClientContext &context) { + auto transaction = make_uniq(sqlite_catalog, *this, context); + transaction->Start(); + auto result = transaction.get(); + lock_guard l(transaction_lock); + transactions[result] = std::move(transaction); + return result; +} + +string OdbcTransactionManager::CommitTransaction(ClientContext &context, Transaction *transaction) { + auto sqlite_transaction = (OdbcTransaction *)transaction; + sqlite_transaction->Commit(); + lock_guard l(transaction_lock); + transactions.erase(transaction); + return string(); +} + +void OdbcTransactionManager::RollbackTransaction(Transaction *transaction) { + auto sqlite_transaction = (OdbcTransaction *)transaction; + sqlite_transaction->Rollback(); + lock_guard l(transaction_lock); + transactions.erase(transaction); +} + +void OdbcTransactionManager::Checkpoint(ClientContext &context, bool force) { + auto &transaction = OdbcTransaction::Get(context, db.GetCatalog()); + auto &db = transaction.GetDB(); + db.Execute("PRAGMA wal_checkpoint"); +} + +} // namespace duckdb diff --git a/src/storage/odbc_update.cpp b/src/storage/odbc_update.cpp new file mode 100644 index 0000000..b5b2621 --- /dev/null +++ b/src/storage/odbc_update.cpp @@ -0,0 +1,119 @@ +#include "storage/odbc_update.hpp" +#include "storage/odbc_table_entry.hpp" +#include "duckdb/planner/operator/logical_update.hpp" +#include "storage/odbc_catalog.hpp" +#include "storage/odbc_transaction.hpp" +#include "odbc_db.hpp" +#include "odbc_stmt.hpp" + +namespace duckdb { + +OdbcUpdate::OdbcUpdate(LogicalOperator &op, TableCatalogEntry &table, vector columns_p) + : PhysicalOperator(PhysicalOperatorType::EXTENSION, op.types, 1), table(table), columns(std::move(columns_p)) { +} + +//===--------------------------------------------------------------------===// +// States +//===--------------------------------------------------------------------===// +class OdbcUpdateGlobalState : public GlobalSinkState { +public: + explicit OdbcUpdateGlobalState(OdbcTableEntry &table) : table(table), update_count(0) { + } + + OdbcTableEntry &table; + OdbcStatement statement; + idx_t update_count; +}; + +string GetUpdateSQL(OdbcTableEntry &table, const vector &index) { + string result; + result = "UPDATE " + KeywordHelper::WriteOptionallyQuoted(table.name); + result += " SET "; + for (idx_t i = 0; i < index.size(); i++) { + if (i > 0) { + result += ", "; + } + auto &col = table.GetColumn(LogicalIndex(index[i].index)); + result += KeywordHelper::WriteOptionallyQuoted(col.GetName()); + result += " = ?"; + } + result += " WHERE rowid = ?"; + return result; +} + +unique_ptr OdbcUpdate::GetGlobalSinkState(ClientContext &context) const { + auto &sqlite_table = table.Cast(); + + auto &transaction = OdbcTransaction::Get(context, sqlite_table.catalog); + auto result = make_uniq(sqlite_table); + result->statement = transaction.GetDB().Prepare(GetUpdateSQL(sqlite_table, columns)); + return std::move(result); +} + +//===--------------------------------------------------------------------===// +// Sink +//===--------------------------------------------------------------------===// +SinkResultType OdbcUpdate::Sink(ExecutionContext &context, DataChunk &chunk, OperatorSinkInput &input) const { + auto &gstate = input.global_state.Cast(); + + chunk.Flatten(); + auto &row_identifiers = chunk.data[chunk.ColumnCount() - 1]; + auto row_data = FlatVector::GetData(row_identifiers); + auto &stmt = gstate.statement; + auto update_columns = chunk.ColumnCount() - 1; + for (idx_t r = 0; r < chunk.size(); r++) { + // bind the SET values + for (idx_t c = 0; c < update_columns; c++) { + auto &col = chunk.data[c]; + stmt.BindValue(col, c, r); + } + // bind the row identifier + stmt.Bind(update_columns, row_data[r]); + stmt.Step(); + stmt.Reset(); + } + gstate.update_count += chunk.size(); + return SinkResultType::NEED_MORE_INPUT; +} + +//===--------------------------------------------------------------------===// +// GetData +//===--------------------------------------------------------------------===// +SourceResultType OdbcUpdate::GetData(ExecutionContext &context, DataChunk &chunk, OperatorSourceInput &input) const { + auto &insert_gstate = sink_state->Cast(); + chunk.SetCardinality(1); + chunk.SetValue(0, 0, Value::BIGINT(insert_gstate.update_count)); + + return SourceResultType::FINISHED; +} + +//===--------------------------------------------------------------------===// +// Helpers +//===--------------------------------------------------------------------===// +string OdbcUpdate::GetName() const { + return "UPDATE"; +} + +string OdbcUpdate::ParamsToString() const { + return table.name; +} + +//===--------------------------------------------------------------------===// +// Plan +//===--------------------------------------------------------------------===// +unique_ptr OdbcCatalog::PlanUpdate(ClientContext &context, LogicalUpdate &op, + unique_ptr plan) { + if (op.return_chunk) { + throw BinderException("RETURNING clause not yet supported for updates of a Odbc table"); + } + for (auto &expr : op.expressions) { + if (expr->type == ExpressionType::VALUE_DEFAULT) { + throw BinderException("SET DEFAULT is not yet supported for updates of a Odbc table"); + } + } + auto insert = make_uniq(op, op.table, std::move(op.columns)); + insert->children.push_back(std::move(plan)); + return std::move(insert); +} + +} // namespace duckdb diff --git a/test/sql/odbc_attach_db2.test b/test/sql/odbc_attach_db2.test index e1dc8a1..2db9610 100644 --- a/test/sql/odbc_attach_db2.test +++ b/test/sql/odbc_attach_db2.test @@ -12,6 +12,17 @@ Catalog Error: Table Function with name odbc_attach does not exist! require odbc_scanner # Confirm the extension works +statement ok +ATTACH 'DSN={db2 odbctest};Hostname=localhost;Database=odbctest;Uid=db2inst1;Pwd=password;Port=50000' AS db2_odbctest (TYPE odbc_scanner); + +query III +SELECT * FROM db2_odbctest.people; +---- +Lebron James 37 100.1 +Spiderman 25 200.2 +Wonder Woman 21 300.3 +David Bowie 69 400.4 + statement ok CALL odbc_attach('DSN={db2 odbctest};Hostname=localhost;Database=odbctest;Uid=db2inst1;Pwd=password;Port=50000'); diff --git a/test/sql/odbc_scan_postgres.test b/test/sql/odbc_scan_postgres.test index def98f2..84bcaa2 100644 --- a/test/sql/odbc_scan_postgres.test +++ b/test/sql/odbc_scan_postgres.test @@ -1,31 +1,31 @@ -# name: test/sql/odbc_scan_postgres.test -# description: test odbc_scanner extension -# group: [odbc_scan] - -# Before we load the extension, this will fail -statement error -SELECT * FROM odbc_scan( - 'DSN={postgres odbc_test};Server=localhost;Database=odbc_test;Uid=postgres;Pwd=password;Port=5432', - '', - 'people' -) -ORDER BY salary ASC; ----- -Catalog Error: Table Function with name odbc_scan does not exist! - -# Require statement will ensure this test is run with this extension loaded -require odbc_scanner - -# Confirm the extension works -query III -SELECT * FROM odbc_scan( - 'DSN={postgres odbc_test};Server=localhost;Database=odbc_test;Uid=postgres;Pwd=password;Port=5432', - '', - 'people' -) -ORDER BY salary ASC; ----- -Lebron James 37 100.1 -Spiderman 25 200.2 -Wonder Woman 21 300.3 -David Bowie 69 400.4 +# # name: test/sql/odbc_scan_postgres.test +# # description: test odbc_scanner extension +# # group: [odbc_scan] +# +# # Before we load the extension, this will fail +# statement error +# SELECT * FROM odbc_scan( +# 'DSN={postgres odbc_test};Server=localhost;Database=odbc_test;Uid=postgres;Pwd=password;Port=5432', +# '', +# 'people' +# ) +# ORDER BY salary ASC; +# ---- +# Catalog Error: Table Function with name odbc_scan does not exist! +# +# # Require statement will ensure this test is run with this extension loaded +# require odbc_scanner +# +# # Confirm the extension works +# query III +# SELECT * FROM odbc_scan( +# 'DSN={postgres odbc_test};Server=localhost;Database=odbc_test;Uid=postgres;Pwd=password;Port=5432', +# '', +# 'people' +# ) +# ORDER BY salary ASC; +# ---- +# Lebron James 37 100.1 +# Spiderman 25 200.2 +# Wonder Woman 21 300.3 +# David Bowie 69 400.4