// 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 fixtures::*;
use rstest::*;
use sqlx::PgConnection;
#[fixture]
fn setup_test_table(mut conn: PgConnection) -> PgConnection {
let sql = r#"
CREATE TABLE test (
id SERIAL8 NOT NULL PRIMARY KEY,
col_boolean boolean DEFAULT false,
col_text text,
col_int8 int8
);
"#;
sql.execute(&mut conn);
let sql = r#"
CREATE INDEX idxtest ON test USING bm25 (id, col_boolean, col_text, col_int8)
WITH (key_field='id', text_fields = '{"col_text": {"fast": true, "tokenizer": {"type":"raw"}}}');
"#;
sql.execute(&mut conn);
"INSERT INTO test (id) VALUES (1);".execute(&mut conn);
"INSERT INTO test (id, col_text) VALUES (2, 'foo');".execute(&mut conn);
"INSERT INTO test (id, col_text, col_int8) VALUES (3, 'bar', 333);".execute(&mut conn);
"INSERT INTO test (id, col_int8) VALUES (4, 444);".execute(&mut conn);
"SET enable_indexscan TO off;".execute(&mut conn);
"SET enable_bitmapscan TO off;".execute(&mut conn);
"SET max_parallel_workers TO 0;".execute(&mut conn);
conn
}
mod string_fast_field_exec {
use super::*;
#[rstest]
fn with_range(#[from(setup_test_table)] mut conn: PgConnection) {
let res = r#"
SELECT * FROM test
WHERE id @@@ paradedb.range(field => 'id', range => int8range(1, 5, '[]'))
ORDER BY id;
"#
.fetch::<(i64, bool, Option, Option)>(&mut conn);
assert_eq!(
res,
vec![
(1, false, None, None),
(2, false, Some(String::from("foo")), None),
(3, false, Some(String::from("bar")), Some(333)),
(4, false, None, Some(444))
]
);
}
#[rstest]
fn with_filter(#[from(setup_test_table)] mut conn: PgConnection) {
let res = r#"
SELECT * FROM test
WHERE col_text IS NULL and id @@@ '>2'
ORDER BY id;
"#
.fetch::<(i64, bool, Option, Option)>(&mut conn);
assert_eq!(res, vec![(4, false, None, Some(444))]);
}
#[rstest]
fn with_multiple_filters(#[from(setup_test_table)] mut conn: PgConnection) {
let res = r#"
SELECT * FROM test
WHERE col_text IS NULL
AND col_int8 IS NOT NULL
AND id @@@ paradedb.range(field => 'id', range => int8range(1, 5, '[]'))
ORDER BY id;
"#
.fetch::<(i64, bool, Option, Option)>(&mut conn);
assert_eq!(res, vec![(4, false, None, Some(444))]);
}
#[rstest]
fn with_not_null(#[from(setup_test_table)] mut conn: PgConnection) {
let res = r#"
SELECT * FROM test
WHERE col_text IS NOT NULL and id @@@ '>2'
ORDER BY id;
"#
.fetch::<(i64, bool, Option, Option)>(&mut conn);
assert_eq!(res, vec![(3, false, Some(String::from("bar")), Some(333))]);
}
#[rstest]
fn with_null(#[from(setup_test_table)] mut conn: PgConnection) {
let res = r#"
SELECT * FROM test
WHERE col_text IS NULL and id @@@ '<=2'
ORDER BY id;
"#
.fetch::<(i64, bool, Option, Option)>(&mut conn);
assert_eq!(res, vec![(1, false, None, None)]);
}
#[rstest]
fn with_count(#[from(setup_test_table)] mut conn: PgConnection) {
let count = r#"
SELECT count(*) FROM test
WHERE col_text IS NOT NULL and id @@@ '>2';
"#
.fetch::<(i64,)>(&mut conn);
assert_eq!(count, vec![(1,)]);
let count = r#"
SELECT count(*) FROM test
WHERE col_text IS NULL and id @@@ '>2';
"#
.fetch::<(i64,)>(&mut conn);
assert_eq!(count, vec![(1,)]);
}
#[rstest]
fn with_empty_string(#[from(setup_test_table)] mut conn: PgConnection) {
"INSERT INTO test (id, col_text) VALUES (5, '');".execute(&mut conn);
let res = r#"
SELECT * FROM test
WHERE col_text = ''
ORDER BY id;
"#
.fetch::<(i64, bool, Option, Option)>(&mut conn);
assert_eq!(res, vec![(5, false, Some(String::from("")), None)]);
}
#[rstest]
fn with_all_null_segment(mut conn: PgConnection) {
let sql = r#"
CREATE TABLE another_test (
id SERIAL8 NOT NULL PRIMARY KEY,
col_boolean boolean DEFAULT false,
col_text text,
col_int8 int8
);
"#;
sql.execute(&mut conn);
let sql = r#"
CREATE INDEX another_idxtest ON another_test USING bm25 (id, col_boolean, col_text, col_int8)
WITH (key_field='id', text_fields = '{"col_text": {"fast": true, "tokenizer": {"type":"raw"}}}');
"#;
sql.execute(&mut conn);
"INSERT INTO another_test (id) VALUES (1);".execute(&mut conn);
"INSERT INTO another_test (id, col_int8) VALUES (3, 333);".execute(&mut conn);
"INSERT INTO another_test (id, col_int8) VALUES (4, 444);".execute(&mut conn);
"INSERT INTO another_test (id, col_text) VALUES (6, NULL), (7, NULL), (8, NULL);"
.execute(&mut conn);
"SET enable_indexscan TO off;".execute(&mut conn);
"SET enable_bitmapscan TO off;".execute(&mut conn);
"SET max_parallel_workers TO 0;".execute(&mut conn);
let count = r#"
SELECT count(*) FROM another_test
WHERE col_text IS NULL and id @@@ '>2';
"#
.fetch::<(i64,)>(&mut conn);
assert_eq!(count, vec![(5,)]);
let res = r#"
SELECT * FROM another_test
WHERE id @@@ paradedb.range(field => 'id', range => int8range(1, 8, '[]'))
ORDER BY id;
"#
.fetch::<(i64, bool, Option, Option)>(&mut conn);
assert_eq!(
res,
vec![
(1, false, None, None),
(3, false, None, Some(333)),
(4, false, None, Some(444)),
(6, false, None, None),
(7, false, None, None),
(8, false, None, None)
]
);
let count = r#"
SELECT count(*) FROM another_test
WHERE id @@@ paradedb.range(field => 'id', range => int8range(1, 8, '[]'))
AND col_text IS NOT NULL
"#
.fetch::<(i64,)>(&mut conn);
assert_eq!(count, vec![(0,)])
}
}