Skip to content

Commit f188142

Browse files
committed
feat: diagnostics helpers, partial/concurrent indexes, agg fix, top_hits from_
1 parent a92b652 commit f188142

10 files changed

Lines changed: 552 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Testing
2+
3+
## Unit Tests
4+
5+
Run unit tests (no database required) before every push — this is what CI checks first:
6+
7+
```bash
8+
.venv/bin/python -m pytest -m "not integration"
9+
```
10+
11+
## Integration Tests
12+
13+
Integration tests require a running ParadeDB/Postgres instance. Use the provided script to start the container and set up the correct DSN:
14+
15+
```bash
16+
PARADEDB_PASSWORD=postgres scripts/run_integration_tests.sh
17+
```
18+
19+
To run a subset of tests, pass pytest selectors:
20+
21+
```bash
22+
PARADEDB_PASSWORD=postgres scripts/run_integration_tests.sh tests/integration/test_indexing_integration.py::test_bm25_partial_index_generates_where_clause
23+
```
24+
25+
The script sets `PARADEDB_TEST_DSN` and `DATABASE_URL` automatically. The default container name is `paradedb-sqlalchemy-integration` on port `5443`.
26+
27+
Some integration tests require newer pg_search versions and will be automatically skipped if the feature is not available (e.g. diagnostics functions like `pdb.indexes()`).

paradedb/__init__.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,64 @@
11
from . import sqlalchemy
2+
from .sqlalchemy.diagnostics import (
3+
paradedb_index_segments,
4+
paradedb_indexes,
5+
paradedb_verify_all_indexes,
6+
paradedb_verify_index,
7+
)
8+
from .sqlalchemy.facets import with_rows
9+
from .sqlalchemy.indexing import BM25Field, assert_indexed, describe, tokenize
10+
from .sqlalchemy.pdb import agg, score, snippet, snippet_positions, snippets
11+
from .sqlalchemy.search import (
12+
ProximityExpr,
13+
all,
14+
fuzzy,
15+
match_all,
16+
match_any,
17+
more_like_this,
18+
near,
19+
parse,
20+
phrase,
21+
phrase_prefix,
22+
prox_array,
23+
prox_regex,
24+
proximity,
25+
range_term,
26+
regex,
27+
regex_phrase,
28+
term,
29+
)
230

3-
__all__ = ["sqlalchemy"]
31+
__all__ = [
32+
"BM25Field",
33+
"ProximityExpr",
34+
"agg",
35+
"all",
36+
"assert_indexed",
37+
"describe",
38+
"fuzzy",
39+
"match_all",
40+
"match_any",
41+
"more_like_this",
42+
"near",
43+
"parse",
44+
"paradedb_index_segments",
45+
"paradedb_indexes",
46+
"paradedb_verify_all_indexes",
47+
"paradedb_verify_index",
48+
"phrase",
49+
"phrase_prefix",
50+
"prox_array",
51+
"prox_regex",
52+
"proximity",
53+
"range_term",
54+
"regex",
55+
"regex_phrase",
56+
"score",
57+
"snippet",
58+
"snippet_positions",
59+
"snippets",
60+
"sqlalchemy",
61+
"term",
62+
"tokenize",
63+
"with_rows",
64+
]

paradedb/sqlalchemy/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from . import expr, facets, indexing, inspect, pdb, search, select_with
1+
from . import diagnostics, expr, facets, indexing, inspect, pdb, search, select_with
22

3-
__all__ = ["expr", "facets", "indexing", "inspect", "pdb", "search", "select_with"]
3+
__all__ = ["diagnostics", "expr", "facets", "indexing", "inspect", "pdb", "search", "select_with"]

paradedb/sqlalchemy/diagnostics.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Sequence
4+
from typing import Any
5+
6+
from sqlalchemy.engine import Engine
7+
8+
9+
def _exec_and_collect(conn, sql: str, params: list[Any]) -> list[dict[str, Any]]:
10+
# exec_driver_sql requires a tuple for single-row execution, or None for no params.
11+
result = conn.exec_driver_sql(sql, tuple(params) if params else None)
12+
columns = list(result.keys())
13+
return [dict(zip(columns, row, strict=False)) for row in result.fetchall()]
14+
15+
16+
def paradedb_indexes(engine: Engine) -> list[dict[str, Any]]:
17+
"""Return metadata for all BM25 indexes from ``pdb.indexes()``."""
18+
with engine.connect() as conn:
19+
return _exec_and_collect(conn, "SELECT * FROM pdb.indexes()", [])
20+
21+
22+
def paradedb_index_segments(engine: Engine, index: str) -> list[dict[str, Any]]:
23+
"""Return segment metadata for a BM25 index from ``pdb.index_segments()``."""
24+
with engine.connect() as conn:
25+
return _exec_and_collect(
26+
conn, "SELECT * FROM pdb.index_segments(%s::regclass)", [index]
27+
)
28+
29+
30+
def paradedb_verify_index(
31+
engine: Engine,
32+
index: str,
33+
*,
34+
heapallindexed: bool = False,
35+
sample_rate: float | None = None,
36+
report_progress: bool = False,
37+
verbose: bool = False,
38+
on_error_stop: bool = False,
39+
segment_ids: Sequence[int] | None = None,
40+
) -> list[dict[str, Any]]:
41+
"""Run ``pdb.verify_index()`` for one BM25 index."""
42+
sql_parts = ["SELECT * FROM pdb.verify_index(%s::regclass"]
43+
params: list[Any] = [index]
44+
if heapallindexed:
45+
sql_parts.append(", heapallindexed => %s::boolean")
46+
params.append(heapallindexed)
47+
if sample_rate is not None:
48+
sql_parts.append(", sample_rate => %s::double precision")
49+
params.append(sample_rate)
50+
if report_progress:
51+
sql_parts.append(", report_progress => %s::boolean")
52+
params.append(report_progress)
53+
if verbose:
54+
sql_parts.append(", verbose => %s::boolean")
55+
params.append(verbose)
56+
if on_error_stop:
57+
sql_parts.append(", on_error_stop => %s::boolean")
58+
params.append(on_error_stop)
59+
if segment_ids is not None:
60+
sql_parts.append(", segment_ids => %s::int[]")
61+
params.append(list(segment_ids))
62+
sql_parts.append(")")
63+
with engine.connect() as conn:
64+
return _exec_and_collect(conn, "".join(sql_parts), params)
65+
66+
67+
def paradedb_verify_all_indexes(
68+
engine: Engine,
69+
*,
70+
schema_pattern: str | None = None,
71+
index_pattern: str | None = None,
72+
heapallindexed: bool = False,
73+
sample_rate: float | None = None,
74+
report_progress: bool = False,
75+
on_error_stop: bool = False,
76+
) -> list[dict[str, Any]]:
77+
"""Run ``pdb.verify_all_indexes()`` across BM25 indexes."""
78+
named_params: list[tuple[str, str, Any]] = []
79+
if schema_pattern is not None:
80+
named_params.append(("schema_pattern", "text", schema_pattern))
81+
if index_pattern is not None:
82+
named_params.append(("index_pattern", "text", index_pattern))
83+
if heapallindexed:
84+
named_params.append(("heapallindexed", "boolean", heapallindexed))
85+
if sample_rate is not None:
86+
named_params.append(("sample_rate", "double precision", sample_rate))
87+
if report_progress:
88+
named_params.append(("report_progress", "boolean", report_progress))
89+
if on_error_stop:
90+
named_params.append(("on_error_stop", "boolean", on_error_stop))
91+
92+
sql_parts = ["SELECT * FROM pdb.verify_all_indexes("]
93+
params: list[Any] = []
94+
if named_params:
95+
sql_parts.append(
96+
", ".join(
97+
f"{name} => %s::{pg_type}"
98+
for name, pg_type, _ in named_params
99+
)
100+
)
101+
params.extend(value for _, _, value in named_params)
102+
sql_parts.append(")")
103+
104+
with engine.connect() as conn:
105+
return _exec_and_collect(conn, "".join(sql_parts), params)
106+
107+
108+
__all__ = [
109+
"paradedb_index_segments",
110+
"paradedb_indexes",
111+
"paradedb_verify_all_indexes",
112+
"paradedb_verify_index",
113+
]

paradedb/sqlalchemy/facets.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,15 @@ def range(*, field: str, ranges: list[dict[str, object]]) -> dict[str, dict[str,
7272
def top_hits(
7373
*,
7474
size: int | None = None,
75-
sort: list[dict[str, str]] | None = None,
75+
from_: int | None = None,
76+
sort: list[dict[str, Any]] | None = None,
7677
docvalue_fields: list[str] | None = None,
7778
) -> dict[str, dict[str, object]]:
7879
payload: dict[str, object] = {}
7980
if size is not None:
8081
payload["size"] = size
82+
if from_ is not None:
83+
payload["from"] = from_
8184
if sort is not None:
8285
payload["sort"] = sort
8386
if docvalue_fields is not None:

paradedb/sqlalchemy/pdb.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,7 @@ def agg(spec: dict[str, Any], *, approximate: bool | None = None) -> ClauseEleme
7777
payload_expr = cast(literal(payload), JSONB)
7878
if approximate is None:
7979
return func.pdb.agg(payload_expr)
80-
return PDBFunctionWithNamedArgs("agg", [payload_expr], [("approximate", approximate)])
80+
# pdb.agg() takes an optional second positional boolean: true = exact (default),
81+
# false = approximate (skip heap visibility checks, ~2-4x faster but may include
82+
# stale rows). approximate=True → pass false; approximate=False → pass true.
83+
return func.pdb.agg(payload_expr, literal(not approximate))

scripts/run_paradedb.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ else
88
RUNNING=0
99
fi
1010

11-
PARADEDB_IMAGE="${PARADEDB_IMAGE:-paradedb/paradedb:0.21.8-pg18}"
11+
PARADEDB_IMAGE="${PARADEDB_IMAGE:-paradedb/paradedb:0.21.9-pg18}"
1212
PARADEDB_CONTAINER_NAME="${PARADEDB_CONTAINER_NAME:-paradedb-sqlalchemy-integration}"
1313
PARADEDB_PORT="${PARADEDB_PORT:-5443}"
1414
PARADEDB_USER="${PARADEDB_USER:-postgres}"

0 commit comments

Comments
 (0)