diff --git a/.github/workflows/ci_pr_tests.yml b/.github/workflows/ci_pr_tests.yml index 11bbc01..75235b9 100644 --- a/.github/workflows/ci_pr_tests.yml +++ b/.github/workflows/ci_pr_tests.yml @@ -27,7 +27,7 @@ jobs: - name: ๐Ÿงช Test - PG ${{ matrix.pg }} run: pgrx-build-test - name: ๐Ÿ“Ž Clippy - PG ${{ matrix.pg }} - if: ${{ matrix.pg == '18' }} + if: ${{ matrix.pg == '15' }} run: cargo clippy --color always -- --deny warnings --allow unexpected-cfgs format: name: ๐Ÿ•ต๏ธ Format diff --git a/.gitignore b/.gitignore index 3906c33..b51a046 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store .idea/ /target +/tests/pg_regress/results +/tests/pg_regress/regression.* *.iml **/*.rs.bk Cargo.lock diff --git a/CONTRIBUTING b/CONTRIBUTING index cc28fc7..77eab4e 100644 --- a/CONTRIBUTING +++ b/CONTRIBUTING @@ -4,7 +4,7 @@ Here are some hints to prepare your environment to contribute to the project: - Install git (pre-commit)[https://pre-commit.com/] hooks with `pre-commit install` - Start a PG with the extension `cargo pgrx run` -- Run tests with `cargo test` +- Run tests with `cargo pgrx regress` (use options --resetdb to force the reset of the db, and --auto to automatically update the expected results) - Format with `cargo fmt` - Lint with `cargo clippy` diff --git a/Cargo.toml b/Cargo.toml index 1b64e36..aef86c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ name = "pgrx_embed_pg_no_seqscan" path = "./src/bin/pgrx_embed.rs" [features] -default = ["pg18"] +default = ["pg15"] pg14 = ["pgrx/pg14", "pgrx-tests/pg14" ] pg15 = ["pgrx/pg15", "pgrx-tests/pg15" ] pg16 = ["pgrx/pg16", "pgrx-tests/pg16" ] @@ -35,5 +35,8 @@ opt-level = 3 lto = "fat" codegen-units = 1 +[package.metadata.pgrx.regress] +enabled = true + [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(pgrx_embed)'] } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..982ad5f --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +PG_CONFIG ?= $(shell which pg_config) +DISTNAME = $(shell perl -nE '/^name\s*=\s*"([^"]+)/ && do { say $$1; exit }' Cargo.toml) +DISTVERSION = $(shell perl -nE '/^version\s*=\s*"([^"]+)/ && do { say $$1; exit }' Cargo.toml) +PGRXV = $(shell perl -nE '/^pgrx\s+=\s"=?([^"]+)/ && do { say $$1; exit }' Cargo.toml) +PGV = $(shell perl -E 'shift =~ /(\d+)/ && say $$1' "$(shell $(PG_CONFIG) --version)") +EXTRA_CLEAN = META.json $(DISTNAME)-$(DISTVERSION).zip target +TESTS = $(wildcard tests/pg_regress/sql/*.sql) +REGRESS = $(patsubst tests/pg_regress/sql/%.sql,%,$(TESTS)) +REGRESS_OPTS = --inputdir=tests/pg_regress --outputdir=target/installcheck + +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/src/helpers.rs b/src/helpers.rs index 921db11..e1fcbe3 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -45,7 +45,7 @@ pub unsafe fn resolve_table_name(table_oid: Oid) -> Option { pub unsafe fn current_db_name() -> String { let db_oid = pg_sys::MyDatabaseId; - string_from_ptr(pg_sys::get_database_name(db_oid)).unwrap() + string_from_ptr(pg_sys::get_database_name(db_oid)).expect("Failed to get database name") } pub unsafe fn current_username() -> String { diff --git a/src/hooks.rs b/src/hooks.rs index bcf9897..aa6c011 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -4,7 +4,6 @@ use pgrx::pg_sys::{ ProcessUtilityContext, QueryCompletion, QueryDesc, QueryEnvironment, SeqScan, }; use pgrx::{error, notice, pg_guard, pg_sys, PgBox, PgRelation}; -#[allow(deprecated)] use regex::Regex; use crate::guc::DetectionLevelEnum; @@ -45,7 +44,6 @@ impl NoSeqscanHooks { unsafe { if let Some(node) = plan.as_ref() { self.check_current_node(plan, rtables); - self.check_plan_recursively(node.lefttree, rtables); self.check_plan_recursively(node.righttree, rtables); } @@ -73,9 +71,8 @@ Query: {} fn get_query_string(&self, query_desc: &PgBox) -> String { unsafe { CStr::from_ptr(query_desc.sourceText) } .to_str() - .unwrap() + .expect("Invalid UTF-8 in query string") .to_string() - .to_lowercase() } fn is_ignored_query_for_comment(&mut self, query_string: &str) -> bool { @@ -83,58 +80,58 @@ Query: {} re.is_match(query_string) } - fn is_ignored_user(&mut self, current_user: String) -> bool { - match guc::PG_NO_SEQSCAN_IGNORE_USERS.get() { - Some(ignore_users_setting) => { + fn is_ignored_user(&self, current_user: String) -> bool { + guc::PG_NO_SEQSCAN_IGNORE_USERS + .get() + .map(|ignore_users_setting| { comma_separated_list_contains(ignore_users_setting, current_user) - } - None => unreachable!(), - } + }) + .unwrap() } - fn is_checked_database(&mut self, database: String) -> bool { - match guc::PG_NO_SEQSCAN_CHECK_DATABASES.get() { - Some(check_databases_setting) => { + fn is_checked_database(&self, database: String) -> bool { + guc::PG_NO_SEQSCAN_CHECK_DATABASES + .get() + .map(|check_databases_setting| { check_databases_setting.is_empty() || comma_separated_list_contains(check_databases_setting, database) - } - None => unreachable!(), - } + }) + .unwrap() } - fn is_checked_schema(&mut self, schema: String) -> bool { - match guc::PG_NO_SEQSCAN_CHECK_SCHEMAS.get() { - Some(check_schemas_setting) => { + fn is_checked_schema(&self, schema: String) -> bool { + guc::PG_NO_SEQSCAN_CHECK_SCHEMAS + .get() + .map(|check_schemas_setting| { check_schemas_setting.is_empty() || comma_separated_list_contains(check_schemas_setting, schema) - } - None => unreachable!(), - } + }) + .unwrap() } - fn check_tables_options_is_set(&mut self) -> bool { + fn check_tables_options_is_set(&self) -> bool { guc::PG_NO_SEQSCAN_CHECK_TABLES .get() .is_some_and(|tables| !tables.is_empty()) } - fn is_checked_table(&mut self, table_name: String) -> bool { - match guc::PG_NO_SEQSCAN_CHECK_TABLES.get() { - Some(check_tables_setting) => { + fn is_checked_table(&self, table_name: String) -> bool { + guc::PG_NO_SEQSCAN_CHECK_TABLES + .get() + .map(|check_tables_setting| { check_tables_setting.is_empty() || comma_separated_list_contains(check_tables_setting, table_name) - } - None => unreachable!(), - } + }) + .unwrap() } - fn is_ignored_table(&mut self, table_name: String) -> bool { - match guc::PG_NO_SEQSCAN_IGNORE_TABLES.get() { - Some(ignore_tables_setting) => { + fn is_ignored_table(&self, table_name: String) -> bool { + guc::PG_NO_SEQSCAN_IGNORE_TABLES + .get() + .map(|ignore_tables_setting| { comma_separated_list_contains(ignore_tables_setting, table_name) - } - None => unreachable!(), - } + }) + .unwrap() } unsafe fn check_current_node(&mut self, node: *mut Plan, rtables: *mut List) { @@ -144,9 +141,11 @@ Query: {} let seq_scan: &mut SeqScan = &mut *(node as *mut SeqScan); #[cfg(not(feature = "pg14"))] - let table_oid = scanned_table(seq_scan.scan.scanrelid, rtables).unwrap(); + let table_oid = scanned_table(seq_scan.scan.scanrelid, rtables) + .expect("Failed to get scanned table OID"); #[cfg(feature = "pg14")] - let table_oid = scanned_table(seq_scan.scanrelid, rtables).unwrap(); + let table_oid = + scanned_table(seq_scan.scanrelid, rtables).expect("Failed to get scanned table OID"); if self.is_sequence(table_oid) { return; @@ -157,13 +156,12 @@ Query: {} return; } - let schema = resolve_namespace_name(table_oid).unwrap(); + let schema = resolve_namespace_name(table_oid).expect("Failed to resolve schema name"); if !self.is_checked_schema(schema) { return; } - let table_name = resolve_table_name(table_oid); - let table_name = table_name.unwrap(); + let table_name = resolve_table_name(table_oid).expect("Failed to resolve table name"); if !self.is_checked_table(table_name.clone()) { return; diff --git a/src/lib.rs b/src/lib.rs index 4b3e30b..e3f8881 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,307 +12,6 @@ pub extern "C-unwind" fn _PG_init() { unsafe { hooks::init_hooks() }; } -#[cfg(any(test, feature = "pg_test"))] -#[allow(static_mut_refs)] -#[pg_schema] -mod tests { - use crate::{guc::DetectionLevelEnum, hooks::HOOK_OPTION}; - use pgrx::prelude::*; - use std::panic; - - #[pg_test] - fn test_check_only_schemas_in_settings() { - set_pg_no_seqscan_level(DetectionLevelEnum::Error); - - Spi::run( - " - CREATE SCHEMA test_schema1; - CREATE SCHEMA test_schema2; - CREATE TABLE test_schema1.foo AS (SELECT * FROM generate_series(1,10) as id); - CREATE TABLE test_schema2.bar AS (SELECT * FROM generate_series(1,10) as id); - CREATE TABLE public.baz AS (SELECT * FROM generate_series(1,10) as id); - ", - ) - .expect("Setup failed"); - - set_check_schemas(vec!["test_schema1", "public"]); - - // These should be ignored due to schema not in check_schemas setting - Spi::run("SELECT * FROM test_schema2.bar;").unwrap(); - - // This should error due to seqscan in checked schema - assert_seq_scan_error("SELECT * FROM test_schema1.foo;", vec!["foo".to_string()]); - assert_seq_scan_error("SELECT * FROM public.baz;", vec!["baz".to_string()]); - assert_seq_scan_error("SELECT * FROM baz;", vec!["baz".to_string()]); - } - - #[pg_test] - fn test_ignores_on_seqscan_when_level_off() { - set_pg_no_seqscan_level(DetectionLevelEnum::Off); - - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("select * from foo;").unwrap(); - - assert_no_seq_scan(); - } - - #[pg_test] - fn test_panics_on_seqscan_when_level_error() { - set_pg_no_seqscan_level(DetectionLevelEnum::Error); - - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - assert_seq_scan_error("select * from foo;", vec!["foo".to_string()]); - } - - #[pg_test] - fn test_warns_on_seqscan_when_level_warn() { - set_pg_no_seqscan_level(DetectionLevelEnum::Warn); - - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("select * from foo;").unwrap(); - assert_seq_scan(vec!["foo".to_string()]); - } - - #[pg_test] - fn test_detects_seqscan_on_multiple_selects() { - set_pg_no_seqscan_level(DetectionLevelEnum::Error); - Spi::run( - "create table foo as (select * from generate_series(1,10) as id); - create table bar as (select * from generate_series(1,10) as id);", - ) - .expect("Setup failed"); - - assert_seq_scan_error("select * from foo;", vec!["foo".to_string()]); - assert_seq_scan_error("select * from bar;", vec!["bar".to_string()]); - } - - #[pg_test] - fn test_ignores_seqscan_on_query_with_ignore_comment() { - set_pg_no_seqscan_level(DetectionLevelEnum::Error); - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("select * from foo /* pg_no_seqscan_skip */;").unwrap(); - Spi::run("select * from foo /* host_name:a-b-1.2.foo,db:my_database,git:0123456789abcdef,pg_no_seqscan_skip,path:/foo/source.java:108`(<>)' */;").unwrap(); - Spi::run("select * from foo /*pg_no_seqscan_skip*/;").unwrap(); - } - - #[pg_test] - fn test_ignores_on_seqscan_when_explain() { - set_pg_no_seqscan_level(DetectionLevelEnum::Error); - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("explain select * from foo;").unwrap(); - } - - #[pg_test] - fn test_ignores_on_seqscan_when_explain_analyze() { - set_pg_no_seqscan_level(DetectionLevelEnum::Error); - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("explain analyze select * from foo;").unwrap(); - assert_no_seq_scan(); - } - - #[pg_test] - fn test_ignores_on_ignore_users() { - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("CREATE USER test_user").expect("failed to create user"); - set_ignore_users(vec!["test_user_2", "test_user"]); - - Spi::run("GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE foo TO test_user") - .expect("failed to grant access to test_user"); - - Spi::run("SET SESSION AUTHORIZATION test_user") - .expect("failed to set session authorization"); - - Spi::run(" select * from foo;").unwrap(); - - assert_no_seq_scan(); - } - - #[pg_test] - fn test_ignores_on_ignore_tables() { - Spi::run( - "create table foo as (select * from generate_series(1,10) as id); - create table bar as (select * from generate_series(1,10) as id); - create table baz as (select * from generate_series(1,10) as id); - ", - ) - .expect("Setup failed"); - - Spi::run("SET pg_no_seqscan.ignore_tables = 'something,foo,baz'") - .expect("Unable to set ignore_tables"); - - Spi::run("select * from foo;").unwrap(); - Spi::run("select * from baz;").unwrap(); - assert_seq_scan_error("select * from bar;", vec!["bar".to_string()]); - } - - #[pg_test] - fn test_checks_on_check_tables() { - Spi::run( - "create table foo as (select * from generate_series(1,10) as id); - create table bar as (select * from generate_series(1,10) as id); - create table baz as (select * from generate_series(1,10) as id); - ", - ) - .expect("Setup failed"); - - Spi::run("SET pg_no_seqscan.check_tables = 'something,foo,baz'") - .expect("Unable to set check_tables"); - - Spi::run("select * from bar;").unwrap(); - assert_seq_scan_error("select * from foo;", vec!["foo".to_string()]); - assert_seq_scan_error("select * from baz;", vec!["baz".to_string()]); - } - - #[pg_test] - fn test_ignores_ignore_tables_option_if_check_tables_option_is_set() { - Spi::run( - "create table foo as (select * from generate_series(1,10) as id); - create table bar as (select * from generate_series(1,10) as id); - create table baz as (select * from generate_series(1,10) as id); - ", - ) - .expect("Setup failed"); - - Spi::run("SET pg_no_seqscan.check_tables = 'something,foo,baz'") - .expect("Unable to set check_tables"); - - Spi::run("SET pg_no_seqscan.ignore_tables = 'something,foo,baz'") - .expect("Unable to set ignore_tables"); - - Spi::run("select * from bar;").unwrap(); - assert_seq_scan_error("select * from foo;", vec!["foo".to_string()]); - assert_seq_scan_error("select * from baz;", vec!["baz".to_string()]); - } - - #[pg_test] - fn test_check_all_databases_when_check_database_is_not_defined() { - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("SET pg_no_seqscan.check_databases = '';").expect("Unable to set check_databases"); - assert_seq_scan_error("select * from foo;", vec!["foo".to_string()]); - } - - #[pg_test] - fn test_ignores_seqscan_on_db_not_defined_in_check_databases() { - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("SET pg_no_seqscan.check_databases = 'postgres';") - .expect("Unable to set check_databases"); - Spi::run("select * from foo;").unwrap(); - assert_no_seq_scan(); - } - - #[pg_test] - fn test_detects_seqscan_on_db_defined_in_check_databases() { - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("SET pg_no_seqscan.check_databases = 'pgrx_tests';") - .expect("Unable to set check_databases"); - assert_seq_scan_error("select * from foo;", vec!["foo".to_string()]); - - Spi::run("SET pg_no_seqscan.check_databases = 'postgres,pgrx_tests';") - .expect("Unable to set check_databases"); - assert_seq_scan_error("select * from foo;", vec!["foo".to_string()]); - - Spi::run("SET pg_no_seqscan.check_databases = 'pgrx_tests,postgres';") - .expect("Unable to set check_databases"); - assert_seq_scan_error("select * from foo;", vec!["foo".to_string()]); - } - - #[pg_test] - fn test_detects_seqscan_after_explain_analyze() { - set_pg_no_seqscan_level(DetectionLevelEnum::Error); - Spi::run("create table foo as (select * from generate_series(1,10) as id);") - .expect("Setup failed"); - - Spi::run("explain analyze select * from foo;").unwrap(); - assert_no_seq_scan(); - assert_seq_scan_error("select * from foo;", vec!["foo".to_string()]); - } - - #[pg_test] - fn test_does_nothing_when_query_by_pk() { - set_pg_no_seqscan_level(DetectionLevelEnum::Error); - Spi::run( - "create table foo (id bigint PRIMARY KEY); - insert into foo SELECT generate_series(1,10); - ", - ) - .expect("Setup failed"); - - Spi::run("select * from foo where id=1;").unwrap(); - assert_no_seq_scan(); - } - - #[pg_test] - fn test_does_nothing_when_querying_a_sequence() { - set_pg_no_seqscan_level(DetectionLevelEnum::Error); - Spi::run("CREATE SEQUENCE foo_seq;").expect("Setup failed"); - - Spi::run("select last_value from foo_seq;").unwrap(); - assert_no_seq_scan(); - } - - fn set_pg_no_seqscan_level(detection_level: DetectionLevelEnum) { - let level = match detection_level { - DetectionLevelEnum::Warn => "WARN", - DetectionLevelEnum::Error => "ERROR", - DetectionLevelEnum::Off => "OFF", - }; - - let set_level = format!("SET pg_no_seqscan.level = {}", level); - Spi::run(&set_level).expect("Unable to set settings"); - } - - fn set_ignore_users(users: Vec<&str>) { - let users_list = users.join(","); - let set_ignore_users = format!("SET pg_no_seqscan.ignore_users = '{}'", users_list); - Spi::run(&set_ignore_users).expect("Unable to set ignore_users"); - } - - fn set_check_schemas(schemas: Vec<&str>) { - let schemas_list = schemas.join(","); - let set_check_schemas = format!("SET pg_no_seqscan.check_schemas = '{}'", schemas_list); - Spi::run(&set_check_schemas).expect("Unable to set check_schemas"); - } - - fn assert_seq_scan(table_vec: Vec) { - unsafe { - assert_eq!(HOOK_OPTION.as_mut().unwrap().tables_in_seqscans, table_vec); - } - } - - fn assert_seq_scan_error(query: &str, table_vec: Vec) { - assert!(panic::catch_unwind(|| Spi::run(query)).is_err()); - assert_seq_scan(table_vec); - } - - fn assert_no_seq_scan() { - unsafe { - assert_eq!( - HOOK_OPTION.as_mut().unwrap().tables_in_seqscans, - Vec::::new() - ); - } - } -} - /// This module is required by `cargo pgrx test` invocations. /// It must be visible at the root of your extension crate. #[cfg(test)] diff --git a/tests/pg_regress/expected/bitmap_and.out b/tests/pg_regress/expected/bitmap_and.out new file mode 100644 index 0000000..69d17b6 --- /dev/null +++ b/tests/pg_regress/expected/bitmap_and.out @@ -0,0 +1,32 @@ +-- Test bitmap and +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE foo (id bigint, value text, category text); +INSERT INTO foo SELECT i, 'value' || i, CASE WHEN i % 2 = 0 THEN 'even' ELSE 'odd' END FROM generate_series(1, 10000) i; +CREATE INDEX idx_foo_value ON foo(value); +CREATE INDEX idx_foo_category ON foo(category); +-- Show the plan +EXPLAIN (COSTS OFF) +SELECT count(*) FROM foo WHERE value = 'value1' AND category = 'even'; + QUERY PLAN +-------------------------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on foo + Recheck Cond: ((category = 'even'::text) AND (value = 'value1'::text)) + -> BitmapAnd + -> Bitmap Index Scan on idx_foo_category + Index Cond: (category = 'even'::text) + -> Bitmap Index Scan on idx_foo_value + Index Cond: (value = 'value1'::text) +(8 rows) + +-- Expect standard query execution, as it uses 'BITMAP AND' +SELECT count(*) FROM foo WHERE value = 'value1' AND category = 'even'; + count +------- + 0 +(1 row) + +-- Cleanup +DROP TABLE foo; diff --git a/tests/pg_regress/expected/bitmap_or.out b/tests/pg_regress/expected/bitmap_or.out new file mode 100644 index 0000000..250fd5a --- /dev/null +++ b/tests/pg_regress/expected/bitmap_or.out @@ -0,0 +1,32 @@ +-- Test detection in bitmap or +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE foo (id bigint, value text, category text); +INSERT INTO foo SELECT i, 'value' || i, CASE WHEN i % 2 = 0 THEN 'even' ELSE 'odd' END FROM generate_series(1, 600) i; +CREATE INDEX idx_foo_value ON foo(value); +CREATE INDEX idx_foo_category ON foo(category); +-- Show query plan +EXPLAIN (COSTS OFF) +SELECT count(*) FROM foo WHERE value = 'value1' OR category = 'even'; + QUERY PLAN +------------------------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on foo + Recheck Cond: ((value = 'value1'::text) OR (category = 'even'::text)) + -> BitmapOr + -> Bitmap Index Scan on idx_foo_value + Index Cond: (value = 'value1'::text) + -> Bitmap Index Scan on idx_foo_category + Index Cond: (category = 'even'::text) +(8 rows) + +-- Expect standard query execution, as it uses 'BITMAP OR' +SELECT count(*) FROM foo WHERE value = 'value1' OR category = 'even'; + count +------- + 301 +(1 row) + +-- Cleanup +DROP TABLE foo; diff --git a/tests/pg_regress/expected/cte.out b/tests/pg_regress/expected/cte.out new file mode 100644 index 0000000..976c0a7 --- /dev/null +++ b/tests/pg_regress/expected/cte.out @@ -0,0 +1,23 @@ +-- Test CTE +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_cte AS (SELECT * FROM generate_series(1,10) as id); +-- Show plan +EXPLAIN (COSTS OFF) +WITH cte AS (SELECT * FROM test_cte) SELECT * FROM cte; + QUERY PLAN +---------------------- + Seq Scan on test_cte +(1 row) + +-- Expect standard query execution as we are not querying a real table +WITH cte AS (SELECT * FROM test_cte) SELECT * FROM cte; +ERROR: A 'Sequential Scan' on test_cte has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: WITH cte AS (SELECT * FROM test_cte) SELECT * FROM cte; + +-- Cleanup +DROP TABLE test_cte; diff --git a/tests/pg_regress/expected/explain.out b/tests/pg_regress/expected/explain.out new file mode 100644 index 0000000..188ecc1 --- /dev/null +++ b/tests/pg_regress/expected/explain.out @@ -0,0 +1,55 @@ +-- Test that EXPLAIN (COSTS OFF) queries are ignored +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +-- explain_filter is here to produce stable regression, inspired from: +-- https://github.com/postgres/postgres/blob/master/src/test/regress/sql/explain.sql +create function explain_filter(text) returns setof text +language plpgsql as +$$ +declare +ln text; +begin +for ln in execute $1 + loop + -- Replace any numeric word with just 'N' + ln := regexp_replace(ln, '-?\m\d+\M\.?\d*', 'N', 'g'); + -- In sort output, the above won't match units-suffixed numbers + ln := regexp_replace(ln, '\m\d+kB', 'NkB', 'g'); + -- Ignore text-mode buffers output because it varies depending + -- on the system state +CONTINUE WHEN (ln ~ ' +Buffers: .*'); + -- Ignore text-mode "Planning:" line because whether it's output + -- varies depending on the system state +CONTINUE WHEN (ln = 'Planning:'); + return next ln; +end loop; +end; +$$; +CREATE TABLE test_explain AS (SELECT * FROM generate_series(1,10) AS id); +-- EXPLAIN (COSTS OFF) should not trigger errors +EXPLAIN (COSTS OFF) +SELECT * FROM test_explain; + QUERY PLAN +-------------------------- + Seq Scan on test_explain +(1 row) + +-- EXPLAIN (COSTS OFF) ANALYZE should not trigger errors +select explain_filter('EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM test_explain;'); + explain_filter +-------------------------------------------------- + Seq Scan on test_explain (actual rows=N loops=N) +(1 row) + +-- But regular query should trigger error +SELECT * FROM test_explain; +ERROR: A 'Sequential Scan' on test_explain has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM test_explain; + +-- cleanup +drop table test_explain; +drop function explain_filter; diff --git a/tests/pg_regress/expected/join.out b/tests/pg_regress/expected/join.out new file mode 100644 index 0000000..503483a --- /dev/null +++ b/tests/pg_regress/expected/join.out @@ -0,0 +1,30 @@ +-- Test join query detection +-- Setup +LOAD 'pg_no_seqscan'; +CREATE TABLE complex_query_foo AS (SELECT * FROM generate_series(1,10) as id); +CREATE TABLE complex_query_bar AS (SELECT * FROM generate_series(1,10) as id); +SET pg_no_seqscan.level = ERROR; +-- Test JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM complex_query_foo JOIN complex_query_bar ON complex_query_foo.id = complex_query_bar.id; + QUERY PLAN +------------------------------------------------------------- + Merge Join + Merge Cond: (complex_query_foo.id = complex_query_bar.id) + -> Sort + Sort Key: complex_query_foo.id + -> Seq Scan on complex_query_foo + -> Sort + Sort Key: complex_query_bar.id + -> Seq Scan on complex_query_bar +(8 rows) + +SELECT * FROM complex_query_foo JOIN complex_query_bar ON complex_query_foo.id = complex_query_bar.id; +ERROR: A 'Sequential Scan' on complex_query_foo,complex_query_bar has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM complex_query_foo JOIN complex_query_bar ON complex_query_foo.id = complex_query_bar.id; + +-- Cleanup +DROP TABLE complex_query_foo, complex_query_bar; diff --git a/tests/pg_regress/expected/sequence.out b/tests/pg_regress/expected/sequence.out new file mode 100644 index 0000000..fe1869f --- /dev/null +++ b/tests/pg_regress/expected/sequence.out @@ -0,0 +1,22 @@ +-- Test that indexed queries don't trigger errors +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE SEQUENCE test_seq; +-- Show plan: +EXPLAIN (COSTS OFF) +SELECT last_value FROM test_seq; + QUERY PLAN +---------------------- + Seq Scan on test_seq +(1 row) + +-- Querying a sequence should not cause error +SELECT last_value FROM test_seq; + last_value +------------ + 1 +(1 row) + +-- cleanup +DROP SEQUENCE test_seq; diff --git a/tests/pg_regress/expected/settings_databases.out b/tests/pg_regress/expected/settings_databases.out new file mode 100644 index 0000000..28ed41f --- /dev/null +++ b/tests/pg_regress/expected/settings_databases.out @@ -0,0 +1,59 @@ +-- Test database filtering +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_db AS (SELECT * FROM generate_series(1,10) as id); +-- Show the plan +EXPLAIN (COSTS OFF) SELECT * FROM test_db; + QUERY PLAN +--------------------- + Seq Scan on test_db +(1 row) + +-- Empty check_databases should check all databases +SET pg_no_seqscan.check_databases = ''; +SELECT * FROM test_db; -- Should error +ERROR: A 'Sequential Scan' on test_db has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM test_db; + +-- Non-matching database should be ignored +-- Note: regress tests run in database named 'contrib_regression' or similar +SET pg_no_seqscan.check_databases = 'postgres'; +SELECT * FROM test_db; -- Should pass + id +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +-- Matching database should be checked +-- Note: current database depends on the test runner, that's why we use this hack to not return it. +SELECT '' +EXCEPT +SELECT set_config('pg_no_seqscan.check_databases', current_database(), false); + ?column? +---------- + +(1 row) + +SELECT * FROM test_db; -- Should error +ERROR: A 'Sequential Scan' on test_db has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM test_db; + +-- Cleanup +DROP TABLE test_db; +RESET pg_no_seqscan.check_databases; diff --git a/tests/pg_regress/expected/settings_level.out b/tests/pg_regress/expected/settings_level.out new file mode 100644 index 0000000..f7ee3bf --- /dev/null +++ b/tests/pg_regress/expected/settings_level.out @@ -0,0 +1,63 @@ +-- Test basic seqscan detection at different levels +-- Setup +LOAD 'pg_no_seqscan'; +SET client_min_messages = NOTICE; +CREATE TABLE basic_seqscan AS (SELECT * FROM generate_series(1,10) AS id); +EXPLAIN (COSTS OFF) SELECT * FROM basic_seqscan; + QUERY PLAN +--------------------------- + Seq Scan on basic_seqscan +(1 row) + +-- Level OFF should ignore seqscans +SET pg_no_seqscan.level = OFF; +SELECT * FROM basic_seqscan; + id +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +-- Level WARN should warn on seqscans +SET pg_no_seqscan.level = WARN; +SELECT * FROM basic_seqscan; +NOTICE: A 'Sequential Scan' on basic_seqscan has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM basic_seqscan; + + id +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +-- Level ERROR should error on seqscans +SET pg_no_seqscan.level = ERROR; +SELECT * FROM basic_seqscan; -- This should fail +ERROR: A 'Sequential Scan' on basic_seqscan has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM basic_seqscan; + +-- Cleanup +DROP TABLE basic_seqscan; +RESET client_min_messages; diff --git a/tests/pg_regress/expected/settings_schema.out b/tests/pg_regress/expected/settings_schema.out new file mode 100644 index 0000000..9969599 --- /dev/null +++ b/tests/pg_regress/expected/settings_schema.out @@ -0,0 +1,38 @@ +-- Test schema filtering +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE SCHEMA test_schema1; +CREATE SCHEMA test_schema2; +CREATE TABLE test_schema1.foo AS (SELECT * FROM generate_series(1,10) as id); +CREATE TABLE test_schema2.bar AS (SELECT * FROM generate_series(1,10) as id); +CREATE TABLE public.baz AS (SELECT * FROM generate_series(1,10) as id); +-- Set check_schemas to only check test_schema1 and public +SET pg_no_seqscan.check_schemas = 'test_schema1,public'; +-- This should be ignored due to schema not in check_schemas setting +SELECT * FROM test_schema2.bar; + id +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +-- These should error +SELECT * FROM test_schema1.foo; +ERROR: A 'Sequential Scan' on foo has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM test_schema1.foo; + +-- Cleanup +DROP TABLE test_schema1.foo, test_schema2.bar, public.baz; +DROP SCHEMA test_schema1, test_schema2; +RESET pg_no_seqscan.check_schemas; diff --git a/tests/pg_regress/expected/settings_table.out b/tests/pg_regress/expected/settings_table.out new file mode 100644 index 0000000..d5a6193 --- /dev/null +++ b/tests/pg_regress/expected/settings_table.out @@ -0,0 +1,71 @@ +-- Test table filtering with ignore_tables and check_tables +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE foo (id serial); +CREATE TABLE bar (id serial); +CREATE TABLE baz (id serial); +EXPLAIN (COSTS OFF) SELECT * FROM foo; + QUERY PLAN +----------------- + Seq Scan on foo +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM bar; + QUERY PLAN +----------------- + Seq Scan on bar +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM baz; + QUERY PLAN +----------------- + Seq Scan on baz +(1 row) + +-- Test ignore_tables +SET pg_no_seqscan.ignore_tables = 'something,foo,baz'; +-- Only bar should error +SELECT * FROM foo; + id +---- +(0 rows) + +SELECT * FROM baz; + id +---- +(0 rows) + +SELECT * FROM bar; +ERROR: A 'Sequential Scan' on bar has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM bar; + +-- Reset for next test +RESET pg_no_seqscan.ignore_tables; +-- Test check_tables +SET pg_no_seqscan.check_tables = 'something,foo,baz'; +-- Error expected on foo and baz only +SELECT * FROM foo; +ERROR: A 'Sequential Scan' on foo has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM foo; + +SELECT * FROM bar; + id +---- +(0 rows) + +SELECT * FROM baz; +ERROR: A 'Sequential Scan' on baz has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM baz; + +-- Cleanup +DROP TABLE foo, bar, baz; +RESET pg_no_seqscan.check_tables; diff --git a/tests/pg_regress/expected/settings_user.out b/tests/pg_regress/expected/settings_user.out new file mode 100644 index 0000000..0027b3d --- /dev/null +++ b/tests/pg_regress/expected/settings_user.out @@ -0,0 +1,19 @@ +-- Test user filtering +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_user_table (id serial); +CREATE USER test_user; +SET pg_no_seqscan.ignore_users = 'test_user_2,test_user'; +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE test_user_table TO test_user; +SET SESSION AUTHORIZATION test_user; +-- Should not error due to the user being in ignore_users +SELECT * FROM test_user_table; + id +---- +(0 rows) + +-- Reset session +RESET SESSION AUTHORIZATION; +RESET pg_no_seqscan.ignore_users; +DROP TABLE test_user_table; +DROP USER test_user; diff --git a/tests/pg_regress/expected/skip_comments.out b/tests/pg_regress/expected/skip_comments.out new file mode 100644 index 0000000..9186cbe --- /dev/null +++ b/tests/pg_regress/expected/skip_comments.out @@ -0,0 +1,68 @@ +-- Test ignoring seqscans with skip comments +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_skip AS (SELECT * FROM generate_series(1,10) AS id); +-- Show the plan +EXPLAIN (COSTS OFF) +SELECT * FROM test_skip; + QUERY PLAN +----------------------- + Seq Scan on test_skip +(1 row) + +-- This query should fail: +SELECT * FROM test_skip; +ERROR: A 'Sequential Scan' on test_skip has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM test_skip; + +-- Test with skip comment variations +SELECT * FROM test_skip /* pg_no_seqscan_skip */; + id +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +SELECT * FROM test_skip /* host_name:a-b-1.2.foo,db:my_database,git:0123456789abcdef,pg_no_seqscan_skip,path:/foo/source.java:108`(<>)' */; + id +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +SELECT * FROM test_skip /*pg_no_seqscan_skip*/; + id +---- + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +-- Cleanup +DROP TABLE test_skip; diff --git a/tests/pg_regress/expected/subquery.out b/tests/pg_regress/expected/subquery.out new file mode 100644 index 0000000..210cb5f --- /dev/null +++ b/tests/pg_regress/expected/subquery.out @@ -0,0 +1,14 @@ +-- Test subquery detection +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_subq AS (SELECT * FROM generate_series(1,10) as id); +-- Test subquery +SELECT * FROM (SELECT * FROM test_subq) as subq; +ERROR: A 'Sequential Scan' on test_subq has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM (SELECT * FROM test_subq) as subq; + +-- Cleanup +DROP TABLE test_subq; diff --git a/tests/pg_regress/expected/update_subquery.out b/tests/pg_regress/expected/update_subquery.out new file mode 100644 index 0000000..739ab2f --- /dev/null +++ b/tests/pg_regress/expected/update_subquery.out @@ -0,0 +1,17 @@ +-- Test UPDATE with subquery +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE upd_foo (id bigint, value text); +CREATE TABLE upd_bar (id bigint, value text); +INSERT INTO upd_foo SELECT i, 'foo' || i FROM generate_series(1, 10) i; +INSERT INTO upd_bar SELECT i, 'bar' || i FROM generate_series(1, 10) i; +-- Test UPDATE with subquery +UPDATE upd_foo SET value = (SELECT value FROM upd_bar WHERE upd_bar.id = upd_foo.id); +ERROR: A 'Sequential Scan' on upd_foo has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: UPDATE upd_foo SET value = (SELECT value FROM upd_bar WHERE upd_bar.id = upd_foo.id); + +-- Cleanup +DROP TABLE upd_foo, upd_bar; diff --git a/tests/pg_regress/expected/using_index.out b/tests/pg_regress/expected/using_index.out new file mode 100644 index 0000000..46cb925 --- /dev/null +++ b/tests/pg_regress/expected/using_index.out @@ -0,0 +1,22 @@ +-- Test that indexed queries don't trigger errors +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_pk (id bigint PRIMARY KEY); +INSERT INTO test_pk SELECT generate_series(1,10); +-- Query by primary key should not error +EXPLAIN (COSTS OFF) +SELECT * FROM test_pk WHERE id=1; + QUERY PLAN +----------------------------------------------- + Index Only Scan using test_pk_pkey on test_pk + Index Cond: (id = 1) +(2 rows) + +SELECT * FROM test_pk WHERE id=1; + id +---- + 1 +(1 row) + +-- Cleanup +DROP TABLE test_pk; diff --git a/tests/pg_regress/expected/view.out b/tests/pg_regress/expected/view.out new file mode 100644 index 0000000..fee19f6 --- /dev/null +++ b/tests/pg_regress/expected/view.out @@ -0,0 +1,23 @@ +-- Test view detection +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_view_table AS (SELECT * FROM generate_series(1,10) as id); +CREATE VIEW test_view AS SELECT * FROM test_view_table; +-- Test querying view +EXPLAIN (COSTS OFF) +SELECT * FROM test_view; + QUERY PLAN +----------------------------- + Seq Scan on test_view_table +(1 row) + +SELECT * FROM test_view; +ERROR: A 'Sequential Scan' on test_view_table has been detected. + - Run an EXPLAIN on your query to check the query plan. + - Make sure the query is compatible with the existing indexes. + +Query: SELECT * FROM test_view; + +-- Cleanup +DROP VIEW test_view; +DROP TABLE test_view_table; diff --git a/tests/pg_regress/sql/bitmap_and.sql b/tests/pg_regress/sql/bitmap_and.sql new file mode 100644 index 0000000..7f2ffd9 --- /dev/null +++ b/tests/pg_regress/sql/bitmap_and.sql @@ -0,0 +1,19 @@ +-- Test bitmap and +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; + +CREATE TABLE foo (id bigint, value text, category text); +INSERT INTO foo SELECT i, 'value' || i, CASE WHEN i % 2 = 0 THEN 'even' ELSE 'odd' END FROM generate_series(1, 10000) i; +CREATE INDEX idx_foo_value ON foo(value); +CREATE INDEX idx_foo_category ON foo(category); + +-- Show the plan +EXPLAIN (COSTS OFF) +SELECT count(*) FROM foo WHERE value = 'value1' AND category = 'even'; + +-- Expect standard query execution, as it uses 'BITMAP AND' +SELECT count(*) FROM foo WHERE value = 'value1' AND category = 'even'; + +-- Cleanup +DROP TABLE foo; diff --git a/tests/pg_regress/sql/bitmap_or.sql b/tests/pg_regress/sql/bitmap_or.sql new file mode 100644 index 0000000..a42cfa7 --- /dev/null +++ b/tests/pg_regress/sql/bitmap_or.sql @@ -0,0 +1,19 @@ +-- Test detection in bitmap or +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; + +CREATE TABLE foo (id bigint, value text, category text); +INSERT INTO foo SELECT i, 'value' || i, CASE WHEN i % 2 = 0 THEN 'even' ELSE 'odd' END FROM generate_series(1, 600) i; +CREATE INDEX idx_foo_value ON foo(value); +CREATE INDEX idx_foo_category ON foo(category); + +-- Show query plan +EXPLAIN (COSTS OFF) +SELECT count(*) FROM foo WHERE value = 'value1' OR category = 'even'; + +-- Expect standard query execution, as it uses 'BITMAP OR' +SELECT count(*) FROM foo WHERE value = 'value1' OR category = 'even'; + +-- Cleanup +DROP TABLE foo; diff --git a/tests/pg_regress/sql/cte.sql b/tests/pg_regress/sql/cte.sql new file mode 100644 index 0000000..e54c609 --- /dev/null +++ b/tests/pg_regress/sql/cte.sql @@ -0,0 +1,15 @@ +-- Test CTE +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_cte AS (SELECT * FROM generate_series(1,10) as id); + +-- Show plan +EXPLAIN (COSTS OFF) +WITH cte AS (SELECT * FROM test_cte) SELECT * FROM cte; + +-- Expect standard query execution as we are not querying a real table +WITH cte AS (SELECT * FROM test_cte) SELECT * FROM cte; + +-- Cleanup +DROP TABLE test_cte; diff --git a/tests/pg_regress/sql/explain.sql b/tests/pg_regress/sql/explain.sql new file mode 100644 index 0000000..d418d08 --- /dev/null +++ b/tests/pg_regress/sql/explain.sql @@ -0,0 +1,45 @@ +-- Test that EXPLAIN (COSTS OFF) queries are ignored +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; + +-- explain_filter is here to produce stable regression, inspired from: +-- https://github.com/postgres/postgres/blob/master/src/test/regress/sql/explain.sql +create function explain_filter(text) returns setof text +language plpgsql as +$$ +declare +ln text; +begin +for ln in execute $1 + loop + -- Replace any numeric word with just 'N' + ln := regexp_replace(ln, '-?\m\d+\M\.?\d*', 'N', 'g'); + -- In sort output, the above won't match units-suffixed numbers + ln := regexp_replace(ln, '\m\d+kB', 'NkB', 'g'); + -- Ignore text-mode buffers output because it varies depending + -- on the system state +CONTINUE WHEN (ln ~ ' +Buffers: .*'); + -- Ignore text-mode "Planning:" line because whether it's output + -- varies depending on the system state +CONTINUE WHEN (ln = 'Planning:'); + return next ln; +end loop; +end; +$$; + +CREATE TABLE test_explain AS (SELECT * FROM generate_series(1,10) AS id); + +-- EXPLAIN (COSTS OFF) should not trigger errors +EXPLAIN (COSTS OFF) +SELECT * FROM test_explain; + +-- EXPLAIN (COSTS OFF) ANALYZE should not trigger errors +select explain_filter('EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM test_explain;'); + +-- But regular query should trigger error +SELECT * FROM test_explain; + +-- cleanup +drop table test_explain; +drop function explain_filter; diff --git a/tests/pg_regress/sql/join.sql b/tests/pg_regress/sql/join.sql new file mode 100644 index 0000000..6bedd95 --- /dev/null +++ b/tests/pg_regress/sql/join.sql @@ -0,0 +1,15 @@ +-- Test join query detection +-- Setup +LOAD 'pg_no_seqscan'; +CREATE TABLE complex_query_foo AS (SELECT * FROM generate_series(1,10) as id); +CREATE TABLE complex_query_bar AS (SELECT * FROM generate_series(1,10) as id); +SET pg_no_seqscan.level = ERROR; + +-- Test JOIN +EXPLAIN (COSTS OFF) +SELECT * FROM complex_query_foo JOIN complex_query_bar ON complex_query_foo.id = complex_query_bar.id; + +SELECT * FROM complex_query_foo JOIN complex_query_bar ON complex_query_foo.id = complex_query_bar.id; + +-- Cleanup +DROP TABLE complex_query_foo, complex_query_bar; diff --git a/tests/pg_regress/sql/sequence.sql b/tests/pg_regress/sql/sequence.sql new file mode 100644 index 0000000..21bba24 --- /dev/null +++ b/tests/pg_regress/sql/sequence.sql @@ -0,0 +1,14 @@ +-- Test that indexed queries don't trigger errors +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; + +CREATE SEQUENCE test_seq; +-- Show plan: +EXPLAIN (COSTS OFF) +SELECT last_value FROM test_seq; +-- Querying a sequence should not cause error +SELECT last_value FROM test_seq; + +-- cleanup +DROP SEQUENCE test_seq; diff --git a/tests/pg_regress/sql/settings_databases.sql b/tests/pg_regress/sql/settings_databases.sql new file mode 100644 index 0000000..e777c4f --- /dev/null +++ b/tests/pg_regress/sql/settings_databases.sql @@ -0,0 +1,29 @@ +-- Test database filtering +-- Setup +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_db AS (SELECT * FROM generate_series(1,10) as id); + +-- Show the plan +EXPLAIN (COSTS OFF) SELECT * FROM test_db; + +-- Empty check_databases should check all databases +SET pg_no_seqscan.check_databases = ''; +SELECT * FROM test_db; -- Should error + +-- Non-matching database should be ignored +-- Note: regress tests run in database named 'contrib_regression' or similar +SET pg_no_seqscan.check_databases = 'postgres'; +SELECT * FROM test_db; -- Should pass + +-- Matching database should be checked +-- Note: current database depends on the test runner, that's why we use this hack to not return it. +SELECT '' +EXCEPT +SELECT set_config('pg_no_seqscan.check_databases', current_database(), false); + +SELECT * FROM test_db; -- Should error + +-- Cleanup +DROP TABLE test_db; +RESET pg_no_seqscan.check_databases; diff --git a/tests/pg_regress/sql/settings_level.sql b/tests/pg_regress/sql/settings_level.sql new file mode 100644 index 0000000..f0b560a --- /dev/null +++ b/tests/pg_regress/sql/settings_level.sql @@ -0,0 +1,22 @@ +-- Test basic seqscan detection at different levels +-- Setup +LOAD 'pg_no_seqscan'; +SET client_min_messages = NOTICE; +CREATE TABLE basic_seqscan AS (SELECT * FROM generate_series(1,10) AS id); +EXPLAIN (COSTS OFF) SELECT * FROM basic_seqscan; + +-- Level OFF should ignore seqscans +SET pg_no_seqscan.level = OFF; +SELECT * FROM basic_seqscan; + +-- Level WARN should warn on seqscans +SET pg_no_seqscan.level = WARN; +SELECT * FROM basic_seqscan; + +-- Level ERROR should error on seqscans +SET pg_no_seqscan.level = ERROR; +SELECT * FROM basic_seqscan; -- This should fail + +-- Cleanup +DROP TABLE basic_seqscan; +RESET client_min_messages; diff --git a/tests/pg_regress/sql/settings_schema.sql b/tests/pg_regress/sql/settings_schema.sql new file mode 100644 index 0000000..904aa1b --- /dev/null +++ b/tests/pg_regress/sql/settings_schema.sql @@ -0,0 +1,23 @@ +-- Test schema filtering +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; + +CREATE SCHEMA test_schema1; +CREATE SCHEMA test_schema2; +CREATE TABLE test_schema1.foo AS (SELECT * FROM generate_series(1,10) as id); +CREATE TABLE test_schema2.bar AS (SELECT * FROM generate_series(1,10) as id); +CREATE TABLE public.baz AS (SELECT * FROM generate_series(1,10) as id); + +-- Set check_schemas to only check test_schema1 and public +SET pg_no_seqscan.check_schemas = 'test_schema1,public'; + +-- This should be ignored due to schema not in check_schemas setting +SELECT * FROM test_schema2.bar; + +-- These should error +SELECT * FROM test_schema1.foo; + +-- Cleanup +DROP TABLE test_schema1.foo, test_schema2.bar, public.baz; +DROP SCHEMA test_schema1, test_schema2; +RESET pg_no_seqscan.check_schemas; diff --git a/tests/pg_regress/sql/settings_table.sql b/tests/pg_regress/sql/settings_table.sql new file mode 100644 index 0000000..3f14c4a --- /dev/null +++ b/tests/pg_regress/sql/settings_table.sql @@ -0,0 +1,33 @@ +-- Test table filtering with ignore_tables and check_tables +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; + +CREATE TABLE foo (id serial); +CREATE TABLE bar (id serial); +CREATE TABLE baz (id serial); + +EXPLAIN (COSTS OFF) SELECT * FROM foo; +EXPLAIN (COSTS OFF) SELECT * FROM bar; +EXPLAIN (COSTS OFF) SELECT * FROM baz; + + +-- Test ignore_tables +SET pg_no_seqscan.ignore_tables = 'something,foo,baz'; +-- Only bar should error +SELECT * FROM foo; +SELECT * FROM baz; +SELECT * FROM bar; + +-- Reset for next test +RESET pg_no_seqscan.ignore_tables; + +-- Test check_tables +SET pg_no_seqscan.check_tables = 'something,foo,baz'; +-- Error expected on foo and baz only +SELECT * FROM foo; +SELECT * FROM bar; +SELECT * FROM baz; + +-- Cleanup +DROP TABLE foo, bar, baz; +RESET pg_no_seqscan.check_tables; \ No newline at end of file diff --git a/tests/pg_regress/sql/settings_user.sql b/tests/pg_regress/sql/settings_user.sql new file mode 100644 index 0000000..5908ec1 --- /dev/null +++ b/tests/pg_regress/sql/settings_user.sql @@ -0,0 +1,19 @@ +-- Test user filtering +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_user_table (id serial); + +CREATE USER test_user; +SET pg_no_seqscan.ignore_users = 'test_user_2,test_user'; + +GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE test_user_table TO test_user; +SET SESSION AUTHORIZATION test_user; + +-- Should not error due to the user being in ignore_users +SELECT * FROM test_user_table; + +-- Reset session +RESET SESSION AUTHORIZATION; +RESET pg_no_seqscan.ignore_users; +DROP TABLE test_user_table; +DROP USER test_user; diff --git a/tests/pg_regress/sql/skip_comments.sql b/tests/pg_regress/sql/skip_comments.sql new file mode 100644 index 0000000..6161dda --- /dev/null +++ b/tests/pg_regress/sql/skip_comments.sql @@ -0,0 +1,18 @@ +-- Test ignoring seqscans with skip comments +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_skip AS (SELECT * FROM generate_series(1,10) AS id); + +-- Show the plan +EXPLAIN (COSTS OFF) +SELECT * FROM test_skip; +-- This query should fail: +SELECT * FROM test_skip; + +-- Test with skip comment variations +SELECT * FROM test_skip /* pg_no_seqscan_skip */; +SELECT * FROM test_skip /* host_name:a-b-1.2.foo,db:my_database,git:0123456789abcdef,pg_no_seqscan_skip,path:/foo/source.java:108`(<>)' */; +SELECT * FROM test_skip /*pg_no_seqscan_skip*/; + +-- Cleanup +DROP TABLE test_skip; diff --git a/tests/pg_regress/sql/subquery.sql b/tests/pg_regress/sql/subquery.sql new file mode 100644 index 0000000..3766985 --- /dev/null +++ b/tests/pg_regress/sql/subquery.sql @@ -0,0 +1,10 @@ +-- Test subquery detection +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_subq AS (SELECT * FROM generate_series(1,10) as id); + +-- Test subquery +SELECT * FROM (SELECT * FROM test_subq) as subq; + +-- Cleanup +DROP TABLE test_subq; \ No newline at end of file diff --git a/tests/pg_regress/sql/update_subquery.sql b/tests/pg_regress/sql/update_subquery.sql new file mode 100644 index 0000000..ccb47c2 --- /dev/null +++ b/tests/pg_regress/sql/update_subquery.sql @@ -0,0 +1,14 @@ +-- Test UPDATE with subquery +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; + +CREATE TABLE upd_foo (id bigint, value text); +CREATE TABLE upd_bar (id bigint, value text); +INSERT INTO upd_foo SELECT i, 'foo' || i FROM generate_series(1, 10) i; +INSERT INTO upd_bar SELECT i, 'bar' || i FROM generate_series(1, 10) i; + +-- Test UPDATE with subquery +UPDATE upd_foo SET value = (SELECT value FROM upd_bar WHERE upd_bar.id = upd_foo.id); + +-- Cleanup +DROP TABLE upd_foo, upd_bar; \ No newline at end of file diff --git a/tests/pg_regress/sql/using_index.sql b/tests/pg_regress/sql/using_index.sql new file mode 100644 index 0000000..b8a1ea2 --- /dev/null +++ b/tests/pg_regress/sql/using_index.sql @@ -0,0 +1,14 @@ +-- Test that indexed queries don't trigger errors +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_pk (id bigint PRIMARY KEY); +INSERT INTO test_pk SELECT generate_series(1,10); + + +-- Query by primary key should not error +EXPLAIN (COSTS OFF) +SELECT * FROM test_pk WHERE id=1; +SELECT * FROM test_pk WHERE id=1; + +-- Cleanup +DROP TABLE test_pk; diff --git a/tests/pg_regress/sql/view.sql b/tests/pg_regress/sql/view.sql new file mode 100644 index 0000000..6f025f2 --- /dev/null +++ b/tests/pg_regress/sql/view.sql @@ -0,0 +1,14 @@ +-- Test view detection +LOAD 'pg_no_seqscan'; +SET pg_no_seqscan.level = ERROR; +CREATE TABLE test_view_table AS (SELECT * FROM generate_series(1,10) as id); +CREATE VIEW test_view AS SELECT * FROM test_view_table; + +-- Test querying view +EXPLAIN (COSTS OFF) +SELECT * FROM test_view; +SELECT * FROM test_view; + +-- Cleanup +DROP VIEW test_view; +DROP TABLE test_view_table;