// Copyright (c) 2023-2025 ParadeDB, Inc. // // This file is part of ParadeDB - Postgres for Search and Analytics // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . mod fixtures; use crate::fixtures::querygen::arb_joins_and_wheres; use crate::fixtures::querygen::joingen::JoinType; use crate::fixtures::querygen::wheregen::arb_wheres; use fixtures::*; use futures::executor::block_on; use lockfree_object_pool::MutexObjectPool; use proptest::prelude::*; use rstest::*; use sqlx::PgConnection; fn generated_queries_setup(conn: &mut PgConnection, tables: &[(&str, usize)]) -> String { "CREATE EXTENSION pg_search;".execute(conn); "SET log_error_verbosity TO VERBOSE;".execute(conn); "SET log_min_duration_statement TO 1000;".execute(conn); let mut setup_sql = String::new(); for (tname, row_count) in tables { let sql = format!( r#" CREATE TABLE {tname} ( id serial8 not null primary key, name text, color varchar, age varchar ); INSERT into {tname} (name, color, age) VALUES ('bob', 'blue', 20); INSERT into {tname} (name, color, age) SELECT(ARRAY ['alice','bob','cloe', 'sally','brandy','brisket','anchovy']::text[])[(floor(random() * 7) + 1)::int], (ARRAY ['red','green','blue', 'orange','purple','pink','yellow']::text[])[(floor(random() * 7) + 1)::int], (floor(random() * 100) + 1)::int::text FROM generate_series(1, {row_count}); CREATE INDEX idx{tname} ON {tname} USING bm25 (id, name, color, age) WITH ( key_field = 'id', text_fields = ' {{ "name": {{ "tokenizer": {{ "type": "keyword" }}, "fast": true }}, "color": {{ "tokenizer": {{ "type": "keyword" }}, "fast": true }}, "age": {{ "tokenizer": {{ "type": "keyword" }}, "fast": true }} }}' ); CREATE INDEX idx{tname}_name ON {tname} (name); CREATE INDEX idx{tname}_color ON {tname} (color); CREATE INDEX idx{tname}_age ON {tname} (age); ANALYZE; "#, tname = tname ); (&sql).execute(conn); setup_sql.push_str(&sql); } setup_sql } /// /// Tests all JoinTypes against small tables (which are particularly important for joins which /// result in e.g. the cartesian product). /// #[rstest] #[tokio::test] async fn generated_joins_small(database: Db) { let pool = MutexObjectPool::::new( move || block_on(async { database.connection().await }), |_| {}, ); let tables_and_sizes = [("users", 10), ("products", 10), ("orders", 10)]; let tables = tables_and_sizes .iter() .map(|(table, _)| table) .collect::>(); let setup_sql = generated_queries_setup(&mut pool.pull(), &tables_and_sizes); eprintln!("{setup_sql}"); proptest!(|( (join, where_expr) in arb_joins_and_wheres( any::(), tables, vec![("id", "3"), ("name", "bob"), ("color", "blue"), ("age", "20")] ), )| { let join_clause = join.to_sql(); let from = format!("SELECT COUNT(*) {join_clause} "); let pg = format!("{from} WHERE {}", where_expr.to_sql(" = ")); let bm25 = format!("{from} WHERE {}", where_expr.to_sql("@@@")); let conn = &mut pool.pull(); // the postgres query is always run with the paradedb custom scan turned off // this ensures we get the actual, known-to-be-correct result from Postgres' // plan, and not from ours where we did some kind of pushdown r#" RESET max_parallel_workers; SET enable_seqscan TO ON; SET enable_indexscan TO ON; SET paradedb.enable_custom_scan TO OFF; "#.execute(conn); let (pg_cnt,) = (&pg).fetch_one::<(i64,)>(conn); // and for the "bm25" query, we run it a number of times with more and more scan types disabled, // always ensuring that paradedb's custom scan is turned on "SET paradedb.enable_custom_scan TO ON;".execute(conn); for scan_type in ["SET enable_seqscan TO OFF", "SET enable_indexscan TO OFF", "SET max_parallel_workers TO 0"] { scan_type.execute(conn); let (bm25_cnt,) = (&bm25).fetch_one::<(i64,)>(conn); prop_assert_eq!( pg_cnt, bm25_cnt, "\nscan_type={}\npg:\n {}\nbm25:\n {}\nexplain:\n{}\n", scan_type, pg, bm25, format!("EXPLAIN {bm25}").fetch::<(String,)>(conn).into_iter().map(|(s,)| s).collect::>().join("\n"), ); } }); } /// /// Tests only the smallest JoinType against larger tables, with a target list, and a limit. /// #[rstest] #[tokio::test] async fn generated_joins_large_limit(database: Db) { let pool = MutexObjectPool::::new( move || block_on(async { database.connection().await }), |_| {}, ); let tables_and_sizes = [("users", 10000), ("products", 10000), ("orders", 10000)]; let tables = tables_and_sizes .iter() .map(|(table, _)| table) .collect::>(); let setup_sql = generated_queries_setup(&mut pool.pull(), &tables_and_sizes); eprintln!("{setup_sql}"); proptest!(|( (join, where_expr) in arb_joins_and_wheres( Just(JoinType::Inner), tables, vec![("id", "3"), ("name", "bob"), ("color", "blue"), ("age", "20")] ), target_list in proptest::sample::subsequence(vec!["id", "name", "color", "age"], 1..=4), )| { let join_clause = join.to_sql(); let used_tables = join.used_tables(); let target_list = target_list .into_iter() .map(|column| format!("{}.{column}", used_tables[0])) .collect::>() .join(", "); let from = format!("SELECT {target_list} {join_clause} "); let pg = format!("{from} WHERE {} LIMIT 10;", where_expr.to_sql(" = ")); let bm25 = format!("{from} WHERE {} LIMIT 10;", where_expr.to_sql("@@@")); let conn = &mut pool.pull(); // the postgres query is always run with the paradedb custom scan turned off // this ensures we get the actual, known-to-be-correct result from Postgres' // plan, and not from ours where we did some kind of pushdown r#" RESET max_parallel_workers; SET enable_seqscan TO ON; SET enable_indexscan TO ON; SET paradedb.enable_custom_scan TO OFF; "#.execute(conn); // Because we use a generated target list, we fetch as dynamic to allow for comparison. let pg_rows = (&pg).fetch_dynamic(conn); // and for the "bm25" query, we run it a number of times with more and more scan types disabled, // always ensuring that paradedb's custom scan is turned on "SET paradedb.enable_custom_scan TO ON;".execute(conn); for scan_type in ["SET enable_seqscan TO OFF", "SET enable_indexscan TO OFF", "SET max_parallel_workers TO 0"] { scan_type.execute(conn); let bm25_rows = (&bm25).fetch_dynamic(conn); prop_assert_eq!( pg_rows.len(), bm25_rows.len(), "\npg:\n {}\nbm25:\n {}\nexplain:\n{}\n", pg, bm25, format!("EXPLAIN {bm25}").fetch::<(String,)>(conn).into_iter().map(|(s,)| s).collect::>().join("\n"), ); } }); } #[rstest] #[tokio::test] async fn generated_single_relation(database: Db) { let pool = MutexObjectPool::::new( move || block_on(async { database.connection().await }), |_| {}, ); let table_name = "users"; generated_queries_setup(&mut pool.pull(), &[(table_name, 10)]); proptest!(|( where_expr in arb_wheres( vec![table_name], vec![("name", "bob"), ("color", "blue"), ("age", "20")] ), )| { let where_clause = where_expr.to_sql(" = "); let pg = format!("SELECT COUNT(*) FROM {table_name} WHERE {where_clause}"); let bm25 = format!( "SELECT COUNT(*) FROM {table_name} WHERE ({where_clause}) AND id @@@ paradedb.all()" ); // force a pushdown let (pg_cnt,) = (&pg).fetch_one::<(i64,)>(&mut pool.pull()); let (bm25_cnt,) = (&bm25).fetch_one::<(i64,)>(&mut pool.pull()); prop_assert_eq!( pg_cnt, bm25_cnt, "\npg:\n {}\nbm25:\n {}\nexplain:\n{}\n", pg, bm25, format!("EXPLAIN {bm25}").fetch::<(String,)>(&mut pool.pull()).into_iter().map(|(s,)| s).collect::>().join("\n"), ); }); }