diff --git a/src/include/postgres_utils.hpp b/src/include/postgres_utils.hpp index 9941d988a..2c8321bd2 100644 --- a/src/include/postgres_utils.hpp +++ b/src/include/postgres_utils.hpp @@ -57,6 +57,8 @@ struct PostgresCopyState { enum class PostgresIsolationLevel { READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE }; +enum class PostgresUseCtidScan : uint8_t { DEFAULT, ENABLED, DISABLED }; + class PostgresUtils { public: static PGconn *PGConnect(const string &dsn, const string &attach_path); diff --git a/src/include/storage/postgres_catalog.hpp b/src/include/storage/postgres_catalog.hpp index 19c3af2b5..c5cb01f27 100644 --- a/src/include/storage/postgres_catalog.hpp +++ b/src/include/storage/postgres_catalog.hpp @@ -33,6 +33,7 @@ class PostgresCatalog : public Catalog { string attach_path; AccessMode access_mode; PostgresIsolationLevel isolation_level; + PostgresUseCtidScan use_ctid_scan = PostgresUseCtidScan::DEFAULT; public: void Initialize(bool load_builtin) override; diff --git a/src/postgres_scanner.cpp b/src/postgres_scanner.cpp index c0ea75e18..91f830e8c 100644 --- a/src/postgres_scanner.cpp +++ b/src/postgres_scanner.cpp @@ -121,6 +121,10 @@ void PostgresScanFunction::PrepareBind(PostgresVersion version, ClientContext &c if (context.TryGetCurrentSetting("pg_use_ctid_scan", pg_use_ctid_scan)) { use_ctid_scan = BooleanValue::Get(pg_use_ctid_scan); } + auto pg_catalog = bind_data.GetCatalog(); + if (pg_catalog && pg_catalog->use_ctid_scan != PostgresUseCtidScan::DEFAULT) { + use_ctid_scan = pg_catalog->use_ctid_scan == PostgresUseCtidScan::ENABLED; + } if (bind_data.use_text_protocol) { // ctid scan is only supported for binary copy use_ctid_scan = false; diff --git a/src/postgres_storage.cpp b/src/postgres_storage.cpp index 23a0cf1c6..dea2fcb8a 100644 --- a/src/postgres_storage.cpp +++ b/src/postgres_storage.cpp @@ -22,6 +22,7 @@ static unique_ptr PostgresAttach(optional_ptr sto PostgresIsolationLevel isolation_level = PostgresIsolationLevel::REPEATABLE_READ; string secret_storage_table_name; bool secret_storage_table_specified_explicitly = false; + PostgresUseCtidScan use_ctid_scan = PostgresUseCtidScan::DEFAULT; for (auto &entry : attach_options.options) { auto lower_name = StringUtil::Lower(entry.first); if (lower_name == "secret") { @@ -45,15 +46,21 @@ static unique_ptr PostgresAttach(optional_ptr sto } else if (lower_name == "secret_storage_table") { secret_storage_table_name = entry.second.ToString(); secret_storage_table_specified_explicitly = true; + } else if (lower_name == "use_ctid_scan") { + use_ctid_scan = BooleanValue::Get(entry.second.DefaultCastAs(LogicalType::BOOLEAN)) + ? PostgresUseCtidScan::ENABLED + : PostgresUseCtidScan::DISABLED; } else { throw BinderException("Unrecognized option for Postgres attach: %s", entry.first); } } SecretStorageTable secret_storage_table(std::move(secret_storage_table_name), secret_storage_table_specified_explicitly); - return make_uniq(context, db, std::move(attach_path), attach_options.access_mode, - std::move(schema_to_load), isolation_level, secret_name, - std::move(secret_storage_table)); + auto catalog = make_uniq(context, db, std::move(attach_path), attach_options.access_mode, + std::move(schema_to_load), isolation_level, secret_name, + std::move(secret_storage_table)); + catalog->use_ctid_scan = use_ctid_scan; + return catalog; } static unique_ptr PostgresCreateTransactionManager(optional_ptr storage_info, diff --git a/test/sql/storage/attach_use_ctid_scan.test b/test/sql/storage/attach_use_ctid_scan.test new file mode 100644 index 000000000..86d1c7125 --- /dev/null +++ b/test/sql/storage/attach_use_ctid_scan.test @@ -0,0 +1,73 @@ +# name: test/sql/storage/attach_use_ctid_scan.test +# description: Test the per-attach use_ctid_scan option overriding the global pg_use_ctid_scan setting +# group: [storage] + +require postgres_scanner + +require-env POSTGRES_TEST_DATABASE_AVAILABLE + +statement ok +PRAGMA enable_verification + +# per-attach option disables ctid scan even though the global setting defaults to true +statement ok +SET pg_use_ctid_scan=true; + +statement ok +ATTACH 'dbname=postgresscanner' AS s (TYPE POSTGRES, USE_CTID_SCAN false) + +query I +SELECT * FROM s.pg_giant_numeric +---- +0.0 +1.0000000000000002e+32 +-1.234567891234568e+26 + +statement ok +DETACH s + +# per-attach option enables ctid scan even though the global setting is false (override works both ways) +statement ok +SET pg_use_ctid_scan=false; + +statement ok +ATTACH 'dbname=postgresscanner' AS s (TYPE POSTGRES, USE_CTID_SCAN true) + +query I +SELECT * FROM s.pg_giant_numeric +---- +0.0 +1.0000000000000002e+32 +-1.234567891234568e+26 + +statement ok +DETACH s + +# when omitted, behavior follows the global setting (here: false) and still produces correct results +statement ok +ATTACH 'dbname=postgresscanner' AS s (TYPE POSTGRES) + +query I +SELECT * FROM s.pg_giant_numeric +---- +0.0 +1.0000000000000002e+32 +-1.234567891234568e+26 + +statement ok +DETACH s + +statement ok +SET pg_use_ctid_scan=true; + +# boolean-typed values are accepted +statement ok +ATTACH 'dbname=postgresscanner' AS s (TYPE POSTGRES, USE_CTID_SCAN 1) + +statement ok +DETACH s + +# a non-boolean value raises a clear error +statement error +ATTACH 'dbname=postgresscanner' AS s (TYPE POSTGRES, USE_CTID_SCAN 'not_a_bool') +----