diff --git a/src/operators/~~.sql b/src/operators/~~.sql index 5fcebafc5..d6cc6b471 100644 --- a/src/operators/~~.sql +++ b/src/operators/~~.sql @@ -10,6 +10,9 @@ --! Uses bloom filter index terms to test substring containment without decryption. --! Requires 'match' index configuration on the column. --! +--! Marked IMMUTABLE so the planner inlines the body and a functional index on +--! `eql_v2.bloom_filter(col)` can match `WHERE eql_v2.like(col, val)`. +--! --! @param a eql_v2_encrypted Haystack (value to search in) --! @param b eql_v2_encrypted Needle (pattern to search for) --! @return Boolean True if bloom filter of a contains bloom filter of b @@ -19,9 +22,11 @@ --! @see eql_v2.add_search_config CREATE FUNCTION eql_v2.like(a eql_v2_encrypted, b eql_v2_encrypted) RETURNS boolean +LANGUAGE SQL +IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.bloom_filter(a) @> eql_v2.bloom_filter(b); -$$ LANGUAGE SQL; +$$; --! @brief Case-insensitive pattern matching helper --! @internal @@ -39,9 +44,11 @@ $$ LANGUAGE SQL; --! @see eql_v2.add_search_config CREATE FUNCTION eql_v2.ilike(a eql_v2_encrypted, b eql_v2_encrypted) RETURNS boolean +LANGUAGE SQL +IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT eql_v2.bloom_filter(a) @> eql_v2.bloom_filter(b); -$$ LANGUAGE SQL; +$$; --! @brief LIKE operator for encrypted values (pattern matching) --! diff --git a/tests/sqlx/tests/like_operator_tests.rs b/tests/sqlx/tests/like_operator_tests.rs index 121c6aea0..ad0627274 100644 --- a/tests/sqlx/tests/like_operator_tests.rs +++ b/tests/sqlx/tests/like_operator_tests.rs @@ -160,3 +160,39 @@ async fn ilike_operator_case_insensitive_matches(pool: PgPool) -> Result<()> { Ok(()) } + +/// Regression test for issue #189: eql_v2.like / eql_v2.ilike must be IMMUTABLE +/// so the planner inlines them and a functional bloom_filter index can match +/// `WHERE eql_v2.like(col, val)`. Without this, queries silently seq-scan. +#[sqlx::test] +async fn like_and_ilike_are_immutable(pool: PgPool) -> Result<()> { + let sql = "SELECT proname, provolatile::text \ + FROM pg_proc \ + WHERE pronamespace = 'eql_v2'::regnamespace \ + AND proname IN ('like', 'ilike') \ + AND pronargs = 2 \ + ORDER BY proname"; + + let rows = sqlx::query(sql) + .fetch_all(&pool) + .await + .context("querying pg_proc for like/ilike volatility")?; + + assert_eq!( + rows.len(), + 2, + "expected eql_v2.like and eql_v2.ilike to exist" + ); + + for row in rows { + let name: String = row.try_get("proname")?; + let volatility: String = row.try_get("provolatile")?; + assert_eq!( + volatility, "i", + "eql_v2.{} must be IMMUTABLE (provolatile='i') for index inlining; got '{}'", + name, volatility, + ); + } + + Ok(()) +}