rushdb 3.0.0__tar.gz → 3.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {rushdb-3.0.0 → rushdb-3.1.0}/PKG-INFO +29 -11
  2. {rushdb-3.0.0 → rushdb-3.1.0}/README.md +26 -8
  3. {rushdb-3.0.0 → rushdb-3.1.0}/pyproject.toml +11 -34
  4. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/ai.py +43 -21
  5. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/query.py +1 -1
  6. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/direct/capabilities.py +2 -1
  7. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/direct/router.py +2 -1
  8. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/models/result.py +43 -3
  9. {rushdb-3.0.0 → rushdb-3.1.0}/tests/test_ai_vector_search.py +28 -24
  10. {rushdb-3.0.0 → rushdb-3.1.0}/tests/test_direct_mode.py +1 -0
  11. {rushdb-3.0.0 → rushdb-3.1.0}/uv.lock +1 -1
  12. {rushdb-3.0.0 → rushdb-3.1.0}/.env.example +0 -0
  13. {rushdb-3.0.0 → rushdb-3.1.0}/.github/workflows/ci.yml +0 -0
  14. {rushdb-3.0.0 → rushdb-3.1.0}/.gitignore +0 -0
  15. {rushdb-3.0.0 → rushdb-3.1.0}/CODE_OF_CONDUCT.md +0 -0
  16. {rushdb-3.0.0 → rushdb-3.1.0}/CONTRIBUTING.md +0 -0
  17. {rushdb-3.0.0 → rushdb-3.1.0}/LICENSE +0 -0
  18. {rushdb-3.0.0 → rushdb-3.1.0}/rushdb-logo.svg +0 -0
  19. {rushdb-3.0.0 → rushdb-3.1.0}/src/__init__.py +0 -0
  20. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/__init__.py +0 -0
  21. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/__init__.py +0 -0
  22. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/base.py +0 -0
  23. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/labels.py +0 -0
  24. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/properties.py +0 -0
  25. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/records.py +0 -0
  26. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/relationship_patterns.py +0 -0
  27. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/relationships.py +0 -0
  28. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/text_indexes.py +0 -0
  29. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/api/transactions.py +0 -0
  30. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/client.py +0 -0
  31. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/common.py +0 -0
  32. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/direct/__init__.py +0 -0
  33. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/direct/engine.py +0 -0
  34. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/direct/schema.py +0 -0
  35. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/models/__init__.py +0 -0
  36. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/models/api_response.py +0 -0
  37. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/models/property.py +0 -0
  38. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/models/record.py +0 -0
  39. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/models/relationship.py +0 -0
  40. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/models/search_query.py +0 -0
  41. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/models/transaction.py +0 -0
  42. {rushdb-3.0.0 → rushdb-3.1.0}/src/rushdb/utils/token_prefix.py +0 -0
  43. {rushdb-3.0.0 → rushdb-3.1.0}/tests/__init__.py +0 -0
  44. {rushdb-3.0.0 → rushdb-3.1.0}/tests/test_base_setup.py +0 -0
  45. {rushdb-3.0.0 → rushdb-3.1.0}/tests/test_create_import.py +0 -0
  46. {rushdb-3.0.0 → rushdb-3.1.0}/tests/test_direct_mode_live.py +0 -0
  47. {rushdb-3.0.0 → rushdb-3.1.0}/tests/test_relationship_patterns.py +0 -0
  48. {rushdb-3.0.0 → rushdb-3.1.0}/tests/test_search_query.py +0 -0
  49. {rushdb-3.0.0 → rushdb-3.1.0}/tests/test_search_result.py +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rushdb
3
- Version: 3.0.0
4
- Summary: RushDB Python SDK — memory layer for AI agents and modern apps
3
+ Version: 3.1.0
4
+ Summary: RushDB SDK for Python: the client for RushDB, the database for AI (RushDB Cloud or self-hosted)
5
5
  Project-URL: Homepage, https://github.com/rush-db/rushdb-python
6
6
  Project-URL: Repository, https://github.com/rush-db/rushdb-python
7
7
  Project-URL: Documentation, https://docs.rushdb.com
8
8
  Author-email: RushDB Team <hi@rushdb.com>
9
9
  License: Apache-2.0
10
10
  License-File: LICENSE
11
- Keywords: agent memory,agent-memory,ai agents,ai memory,ai-agents,ai-memory,cloud,database,embedding,embeddings,graph,graph database,graph-database,json normalization,json-normalization,knowledge graph,knowledge-graph,llm memory,llm-memory,mcp,memory layer,memory-layer,no-schema,persistence,rag,relationships,rest api,rest-api,retrieval augmented generation,rush db,rush-db,rushdb,schema-less,schemaless,self-hosted,vector search,vector-search
11
+ Keywords: agent-memory,ai-agents,cypher,database,database-for-ai,full-text-search,graph-database,hybrid-search,mcp,rag,rushdb,self-hosted,sql,vector-search
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
 
@@ -18,16 +18,15 @@ Description-Content-Type: text/markdown
18
18
 
19
19
  # RushDB — Python SDK
20
20
 
21
- ### The memory layer for AI agents and apps.
21
+ ### RushDB — the database for AI
22
22
 
23
- Push any JSON. Get graph relationships and vector search — automatically.
24
- No schema. No pipeline. No glue code.
23
+ One database for your records, their relationships, and search over them.
25
24
 
26
25
  ![PyPI - Version](https://img.shields.io/pypi/v/rushdb)
27
26
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/rushdb)
28
27
  ![PyPI - License](https://img.shields.io/pypi/l/rushdb)
29
28
 
30
- [📖 Documentation](https://docs.rushdb.com/python-sdk/introduction) • [🌐 Website](https://rushdb.com) • [☁️ Cloud](https://app.rushdb.com)
29
+ [📖 Documentation](https://docs.rushdb.com/3.x/connect/sdks/) • [🌐 Website](https://rushdb.com) • [☁️ Cloud](https://app.rushdb.com)
31
30
 
32
31
  </div>
33
32
 
@@ -51,6 +50,16 @@ pip install rushdb
51
50
 
52
51
  ---
53
52
 
53
+ ## Compatibility
54
+
55
+ | Package | RushDB 3.x and RushDB Cloud | RushDB 2.x (legacy) |
56
+ |---|---|---|
57
+ | `@rushdb/javascript-sdk` | 3.x | 2.11.0: `npm i @rushdb/javascript-sdk@2` |
58
+ | `rushdb` (PyPI) | 3.x | 2.10.0: `pip install "rushdb<3"` |
59
+ | `@rushdb/mcp-server` | 3.x | 2.13.0: `npx @rushdb/mcp-server@2` |
60
+
61
+ ---
62
+
54
63
  ## RushDB Cloud or a self-hosted engine
55
64
 
56
65
  The SDK works with RushDB in one of two ways:
@@ -93,7 +102,7 @@ What each offers:
93
102
  | Keyword (BM25) search (`records.text_search`), text indexes (`text_indexes.*`), hybrid search | ✓ | ✓ |
94
103
  | Transactions | ✓ | record creates, updates, sets, deletes by id, attaches and imports are staged; other writes refuse a transaction |
95
104
  | Managed embeddings: managed indexes, `records.vector_search` by text `query` | ✓ | — |
96
- | Natural-language search (`ai.search`) | ✓ | — |
105
+ | Natural-language search on SQL (`ai.search`, `ai.run_sql`) | ✓ | — |
97
106
  | Relationship-pattern suggestions (`relationships.patterns.*`) | ✓ | — |
98
107
  | Project settings (`settings.get`), dashboard, team access | ✓ | — |
99
108
 
@@ -271,6 +280,7 @@ if result:
271
280
  | `total` | `int` | Total matching records in the database |
272
281
  | `has_more` | `bool` | Whether more records exist beyond this page |
273
282
  | `search_query` | `dict` | The query that produced this result |
283
+ | `sql` | `str` | For Smart search, the SQL statement it ran |
274
284
 
275
285
  ---
276
286
 
@@ -293,13 +303,21 @@ for record in results:
293
303
  ```
294
304
 
295
305
  Use `db.ai.search()` when you want RushDB to turn a natural-language request
296
- into a SearchQuery and execute it:
306
+ into one read-only SQL statement over your schema and run it (RushDB Cloud):
297
307
 
298
308
  ```python
299
309
  results = db.ai.search('Find active memories about Q4 results for agent-42')
300
- print(results.search_query)
310
+ print(results.name) # a short title, e.g. to save the query under
311
+ print(results.sql) # the statement it ran
312
+ more = results.next() # the next page of the statement's rows
313
+
314
+ # Run a statement directly, e.g. a saved one:
315
+ rows = db.ai.run_sql('SELECT agent_id, count(*) AS memories FROM "MEMORY" GROUP BY agent_id')
301
316
  ```
302
317
 
318
+ Rows that select a record's `__id` come back as full records; aggregates come
319
+ back as plain rows.
320
+
303
321
  `db.ai.search({...})` still works as a deprecated vector-search alias, but new
304
322
  code should use `db.records.vector_search({...})`.
305
323
 
@@ -390,7 +408,7 @@ db = RushDB(
390
408
 
391
409
  ## Documentation
392
410
 
393
- [docs.rushdb.com/python-sdk](https://docs.rushdb.com/python-sdk/introduction) — full API reference, vector search, aggregations, and more.
411
+ [docs.rushdb.com/3.x/connect/sdks](https://docs.rushdb.com/3.x/connect/sdks/) and the [Python reference](https://docs.rushdb.com/3.x/learn/reference/python/) — full API reference, vector search, aggregations, and more.
394
412
 
395
413
  ---
396
414
 
@@ -4,16 +4,15 @@
4
4
 
5
5
  # RushDB — Python SDK
6
6
 
7
- ### The memory layer for AI agents and apps.
7
+ ### RushDB — the database for AI
8
8
 
9
- Push any JSON. Get graph relationships and vector search — automatically.
10
- No schema. No pipeline. No glue code.
9
+ One database for your records, their relationships, and search over them.
11
10
 
12
11
  ![PyPI - Version](https://img.shields.io/pypi/v/rushdb)
13
12
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/rushdb)
14
13
  ![PyPI - License](https://img.shields.io/pypi/l/rushdb)
15
14
 
16
- [📖 Documentation](https://docs.rushdb.com/python-sdk/introduction) • [🌐 Website](https://rushdb.com) • [☁️ Cloud](https://app.rushdb.com)
15
+ [📖 Documentation](https://docs.rushdb.com/3.x/connect/sdks/) • [🌐 Website](https://rushdb.com) • [☁️ Cloud](https://app.rushdb.com)
17
16
 
18
17
  </div>
19
18
 
@@ -37,6 +36,16 @@ pip install rushdb
37
36
 
38
37
  ---
39
38
 
39
+ ## Compatibility
40
+
41
+ | Package | RushDB 3.x and RushDB Cloud | RushDB 2.x (legacy) |
42
+ |---|---|---|
43
+ | `@rushdb/javascript-sdk` | 3.x | 2.11.0: `npm i @rushdb/javascript-sdk@2` |
44
+ | `rushdb` (PyPI) | 3.x | 2.10.0: `pip install "rushdb<3"` |
45
+ | `@rushdb/mcp-server` | 3.x | 2.13.0: `npx @rushdb/mcp-server@2` |
46
+
47
+ ---
48
+
40
49
  ## RushDB Cloud or a self-hosted engine
41
50
 
42
51
  The SDK works with RushDB in one of two ways:
@@ -79,7 +88,7 @@ What each offers:
79
88
  | Keyword (BM25) search (`records.text_search`), text indexes (`text_indexes.*`), hybrid search | ✓ | ✓ |
80
89
  | Transactions | ✓ | record creates, updates, sets, deletes by id, attaches and imports are staged; other writes refuse a transaction |
81
90
  | Managed embeddings: managed indexes, `records.vector_search` by text `query` | ✓ | — |
82
- | Natural-language search (`ai.search`) | ✓ | — |
91
+ | Natural-language search on SQL (`ai.search`, `ai.run_sql`) | ✓ | — |
83
92
  | Relationship-pattern suggestions (`relationships.patterns.*`) | ✓ | — |
84
93
  | Project settings (`settings.get`), dashboard, team access | ✓ | — |
85
94
 
@@ -257,6 +266,7 @@ if result:
257
266
  | `total` | `int` | Total matching records in the database |
258
267
  | `has_more` | `bool` | Whether more records exist beyond this page |
259
268
  | `search_query` | `dict` | The query that produced this result |
269
+ | `sql` | `str` | For Smart search, the SQL statement it ran |
260
270
 
261
271
  ---
262
272
 
@@ -279,13 +289,21 @@ for record in results:
279
289
  ```
280
290
 
281
291
  Use `db.ai.search()` when you want RushDB to turn a natural-language request
282
- into a SearchQuery and execute it:
292
+ into one read-only SQL statement over your schema and run it (RushDB Cloud):
283
293
 
284
294
  ```python
285
295
  results = db.ai.search('Find active memories about Q4 results for agent-42')
286
- print(results.search_query)
296
+ print(results.name) # a short title, e.g. to save the query under
297
+ print(results.sql) # the statement it ran
298
+ more = results.next() # the next page of the statement's rows
299
+
300
+ # Run a statement directly, e.g. a saved one:
301
+ rows = db.ai.run_sql('SELECT agent_id, count(*) AS memories FROM "MEMORY" GROUP BY agent_id')
287
302
  ```
288
303
 
304
+ Rows that select a record's `__id` come back as full records; aggregates come
305
+ back as plain rows.
306
+
289
307
  `db.ai.search({...})` still works as a deprecated vector-search alias, but new
290
308
  code should use `db.records.vector_search({...})`.
291
309
 
@@ -376,7 +394,7 @@ db = RushDB(
376
394
 
377
395
  ## Documentation
378
396
 
379
- [docs.rushdb.com/python-sdk](https://docs.rushdb.com/python-sdk/introduction) — full API reference, vector search, aggregations, and more.
397
+ [docs.rushdb.com/3.x/connect/sdks](https://docs.rushdb.com/3.x/connect/sdks/) and the [Python reference](https://docs.rushdb.com/3.x/learn/reference/python/) — full API reference, vector search, aggregations, and more.
380
398
 
381
399
  ---
382
400
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "rushdb"
3
- version = "3.0.0"
4
- description = "RushDB Python SDK — memory layer for AI agents and modern apps"
3
+ version = "3.1.0"
4
+ description = "RushDB SDK for Python: the client for RushDB, the database for AI (RushDB Cloud or self-hosted)"
5
5
  authors = [
6
6
  {name = "RushDB Team", email = "hi@rushdb.com"}
7
7
  ]
@@ -11,41 +11,18 @@ requires-python = ">=3.10"
11
11
  keywords = [
12
12
  "rushdb",
13
13
  "database",
14
- "graph database",
15
- "graph-database",
16
- "ai memory",
17
- "ai-memory",
18
- "agent memory",
19
- "agent-memory",
20
- "memory layer",
21
- "memory-layer",
22
- "ai agents",
14
+ "database-for-ai",
23
15
  "ai-agents",
24
- "llm memory",
25
- "llm-memory",
26
- "mcp",
27
- "vector search",
16
+ "graph-database",
17
+ "sql",
18
+ "cypher",
28
19
  "vector-search",
29
- "rag",
30
- "retrieval augmented generation",
31
- "embedding",
32
- "embeddings",
33
- "knowledge graph",
34
- "knowledge-graph",
35
- "json normalization",
36
- "json-normalization",
37
- "schema-less",
38
- "schemaless",
39
- "no-schema",
20
+ "full-text-search",
21
+ "hybrid-search",
22
+ "mcp",
40
23
  "self-hosted",
41
- "cloud",
42
- "rest api",
43
- "rest-api",
44
- "graph",
45
- "relationships",
46
- "persistence",
47
- "rush-db",
48
- "rush db"
24
+ "rag",
25
+ "agent-memory"
49
26
  ]
50
27
  dependencies = []
51
28
 
@@ -5,12 +5,11 @@ and embedding index management.
5
5
  """
6
6
 
7
7
  import warnings
8
- from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast
8
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Union
9
9
 
10
10
  from ..models.api_response import ApiResponse
11
11
  from ..models.record import Record
12
12
  from ..models.result import RecordSearchResult
13
- from ..models.search_query import SearchQuery
14
13
  from ..models.transaction import Transaction
15
14
  from .base import BaseAPI
16
15
 
@@ -225,14 +224,18 @@ class AIAPI(BaseAPI):
225
224
  def search(
226
225
  self,
227
226
  prompt: Union[str, Dict[str, Any]],
228
- current_query: Optional[SearchQuery] = None,
227
+ skip: Optional[int] = None,
228
+ limit: Optional[int] = None,
229
229
  transaction: Optional[Union[Transaction, str]] = None,
230
230
  ) -> RecordSearchResult:
231
231
  """Perform AI-assisted smart search from natural language.
232
232
 
233
- RushDB converts ``prompt`` into a SearchQuery using the project schema,
234
- executes it, and returns matching records with the generated query
235
- attached to ``result.search_query``.
233
+ RushDB turns ``prompt`` into one read-only SQL statement over the
234
+ project's schema, runs it, and returns the result with the statement
235
+ as ``result.sql``, a short title as ``result.name`` and any generation
236
+ warnings as ``result.warnings``. Rows that select a record's ``__id``
237
+ come back as full records; aggregates come back as plain rows.
238
+ ``result.next()`` pages through the statement.
236
239
 
237
240
  Use ``db.records.vector_search({...})`` for direct vector similarity
238
241
  over embedding indexes.
@@ -240,14 +243,12 @@ class AIAPI(BaseAPI):
240
243
  Args:
241
244
  prompt: Natural-language search request. Passing a dict is
242
245
  deprecated and delegates to ``db.records.vector_search``.
243
- current_query: Optional current SearchQuery context from a
244
- dashboard/query-builder session.
245
- transaction: Optional transaction context.
246
+ skip: Rows to skip (default 0).
247
+ limit: Rows to return, at most 1000 (default 100).
248
+ transaction: Only used by the deprecated dict form.
246
249
 
247
250
  Returns:
248
- RecordSearchResult: Matching records. The generated SearchQuery is
249
- available as ``result.search_query`` and server warnings as
250
- ``result.warnings``.
251
+ RecordSearchResult: The statement's first page of rows.
251
252
  """
252
253
  if isinstance(prompt, dict):
253
254
  warnings.warn(
@@ -258,24 +259,45 @@ class AIAPI(BaseAPI):
258
259
  )
259
260
  return self.client.records.vector_search(prompt, transaction=transaction)
260
261
 
261
- headers = Transaction._build_transaction_header(transaction)
262
262
  generated = self.client._make_request(
263
- "POST",
264
- "/ai/search-query",
265
- {"prompt": prompt, "currentQuery": current_query},
266
- headers,
263
+ "POST", "/ai/sql-search", {"prompt": prompt}
267
264
  )
268
265
  generated_data = generated.get("data") or {}
269
- search_query = generated_data.get("searchQuery") or {}
266
+ result = self.run_sql(generated_data.get("sql", ""), skip=skip, limit=limit)
267
+ result._name = generated_data.get("name")
268
+ result._warnings = generated_data.get("warnings") or []
269
+ return result
270
+
271
+ def run_sql(
272
+ self,
273
+ sql: str,
274
+ skip: Optional[int] = None,
275
+ limit: Optional[int] = None,
276
+ ) -> RecordSearchResult:
277
+ """Run one read-only SQL statement over the project's schema, as Smart
278
+ search does: a statement from ``ai.search`` (its ``sql``), or one
279
+ written by hand. Labels are tables; relationship types are
280
+ ``rushdb_edges.<type>`` tables.
270
281
 
282
+ Args:
283
+ sql: One read-only SELECT statement.
284
+ skip: Rows to skip (default 0).
285
+ limit: Rows to return, at most 1000 (default 100).
286
+
287
+ Returns:
288
+ RecordSearchResult: Full records for rows that select a record's
289
+ ``__id``, otherwise the rows themselves; ``result.sql`` holds the
290
+ statement and ``result.next()`` fetches the following page.
291
+ """
292
+ page = {"skip": skip or 0, "limit": limit or 100}
271
293
  response = self.client._make_request(
272
- "POST", "/records/search", search_query, headers
294
+ "POST", "/records/sql-search", {"sql": sql, **page}
273
295
  )
274
296
  records = [Record(self.client, item) for item in response.get("data", [])]
275
297
  return RecordSearchResult(
276
298
  data=records,
277
299
  total=response.get("total", len(records)),
278
- search_query=cast(SearchQuery, search_query),
279
300
  client=self.client,
280
- warnings=generated_data.get("warnings") or [],
301
+ sql=sql,
302
+ sql_page=page,
281
303
  )
@@ -8,7 +8,7 @@ class QueryAPI(BaseAPI):
8
8
  """API client for executing raw openCypher queries.
9
9
 
10
10
  Works on RushDB Cloud and on a self-hosted RushDB engine in direct mode
11
- (the engine must be started with ``--languages cypher``).
11
+ (the engine must be started with ``--languages cypher,sql``).
12
12
 
13
13
  Example:
14
14
  >>> from rushdb import RushDB
@@ -10,7 +10,8 @@ from typing import Dict
10
10
  _PATTERNS = "relationship-pattern suggestions are produced and stored by the platform"
11
11
 
12
12
  CLOUD_ONLY_METHODS: Dict[str, str] = {
13
- "ai.search": "natural-language search turns the prompt into a query with an LLM",
13
+ "ai.search": "natural-language search turns the prompt into a SQL statement with an LLM",
14
+ "ai.run_sql": "Smart search SQL runs on the platform, which loads the full records the statement selects",
14
15
  "relationships.patterns.list": _PATTERNS,
15
16
  "relationships.patterns.analyze": _PATTERNS,
16
17
  "relationships.patterns.approve": _PATTERNS,
@@ -1372,7 +1372,8 @@ _ROUTES: List[Tuple[str, "re.Pattern[str]", Callable[[_Request], Body]]] = [
1372
1372
  ),
1373
1373
  ("DELETE", re.compile(rf"^/text-indexes/{_ID}$"), _drop_text_index),
1374
1374
  ("POST", re.compile(r"^/search/text$"), _text_search),
1375
- ("POST", re.compile(r"^/ai/search-query$"), _cloud_only("ai.search")),
1375
+ ("POST", re.compile(r"^/ai/sql-search$"), _cloud_only("ai.search")),
1376
+ ("POST", re.compile(r"^/records/sql-search$"), _cloud_only("ai.run_sql")),
1376
1377
  ("POST", re.compile(r"^/ai/search$"), _vector_search),
1377
1378
  (
1378
1379
  "GET",
@@ -32,6 +32,9 @@ class SearchResult(Generic[T]):
32
32
  client: Optional["RushDB"] = None,
33
33
  warnings: Optional[List[str]] = None,
34
34
  meta: Optional[Dict[str, Any]] = None,
35
+ sql: Optional[str] = None,
36
+ name: Optional[str] = None,
37
+ sql_page: Optional[Dict[str, int]] = None,
35
38
  ):
36
39
  """
37
40
  Initialize search result.
@@ -44,6 +47,10 @@ class SearchResult(Generic[T]):
44
47
  warnings: Optional warnings returned by AI-assisted query generation
45
48
  meta: How a vector, keyword or hybrid search ran (``vectorSearch``,
46
49
  ``textSearch``, ``fusion``)
50
+ sql: For Smart search, the SQL statement that produced this result
51
+ name: For Smart search, a short title of the query
52
+ sql_page: For Smart search, the page (``skip``, ``limit``) of the
53
+ statement's rows this result holds
47
54
  """
48
55
  self._data = data
49
56
  self._total = total or len(data)
@@ -51,6 +58,9 @@ class SearchResult(Generic[T]):
51
58
  self._client = client
52
59
  self._warnings = warnings or []
53
60
  self._meta = meta or {}
61
+ self._sql = sql
62
+ self._name = name
63
+ self._sql_page = sql_page or {"skip": 0, "limit": 100}
54
64
 
55
65
  @property
56
66
  def data(self) -> List[T]:
@@ -67,6 +77,16 @@ class SearchResult(Generic[T]):
67
77
  """Get the search query used to generate this result."""
68
78
  return self._search_query
69
79
 
80
+ @property
81
+ def sql(self) -> Optional[str]:
82
+ """For Smart search: the SQL statement that produced this result."""
83
+ return self._sql
84
+
85
+ @property
86
+ def name(self) -> Optional[str]:
87
+ """For Smart search: a short title of the query."""
88
+ return self._name
89
+
70
90
  @property
71
91
  def warnings(self) -> List[str]:
72
92
  """Get warnings returned by AI-assisted query generation."""
@@ -87,11 +107,15 @@ class SearchResult(Generic[T]):
87
107
  @property
88
108
  def skip(self) -> int:
89
109
  """Get the number of records that were skipped."""
110
+ if self._sql:
111
+ return self._sql_page["skip"]
90
112
  return self._search_query.get("skip") or 0
91
113
 
92
114
  @property
93
115
  def limit(self) -> Optional[int]:
94
116
  """Get the limit that was applied to the search."""
117
+ if self._sql:
118
+ return self._sql_page["limit"]
95
119
  return self._search_query.get("limit")
96
120
 
97
121
  def __len__(self) -> int:
@@ -119,12 +143,14 @@ class SearchResult(Generic[T]):
119
143
  Return the result in a standardized dictionary format.
120
144
 
121
145
  Returns:
122
- Dict with keys: total, data, search_query
146
+ Dict with keys: total, data, search_query, sql, name, warnings
123
147
  """
124
148
  return {
125
149
  "total": self.total,
126
150
  "data": self.data,
127
151
  "search_query": self.search_query,
152
+ "sql": self.sql,
153
+ "name": self.name,
128
154
  "warnings": self.warnings,
129
155
  }
130
156
 
@@ -164,7 +190,8 @@ class SearchResult(Generic[T]):
164
190
  return self._get_client().records.delete_by_id(ids, transaction)
165
191
 
166
192
  def next(self, preserve_data: bool = False) -> "SearchResult":
167
- """Fetch the next page of results.
193
+ """Fetch the next page of results: of the original search query, or of
194
+ the SQL statement for a Smart search result.
168
195
 
169
196
  Args:
170
197
  preserve_data: If True, appends new records to this instance's data
@@ -174,8 +201,21 @@ class SearchResult(Generic[T]):
174
201
  SearchResult with the next page (or self when preserve_data=True)
175
202
 
176
203
  Raises:
177
- RuntimeError: If no search_query was provided at construction
204
+ RuntimeError: If neither a search_query nor a sql statement was provided
178
205
  """
206
+ if self._sql:
207
+ next_page = {
208
+ "skip": self._sql_page["skip"] + self._sql_page["limit"],
209
+ "limit": self._sql_page["limit"],
210
+ }
211
+ result = self._get_client().ai.run_sql(self._sql, **next_page)
212
+ if preserve_data:
213
+ self._data = self._data + result._data # type: ignore[operator]
214
+ self._total = result._total
215
+ self._sql_page = next_page
216
+ return self
217
+ result._name = self._name
218
+ return result
179
219
  if not self._search_query:
180
220
  raise RuntimeError(
181
221
  "SearchResult: cannot paginate — no search_query was provided."
@@ -43,13 +43,14 @@ class TestVectorAndSmartSearch(unittest.TestCase):
43
43
  self.assertEqual(result[0].get("title"), "Alpha")
44
44
  self.assertEqual(result[0].score, 0.91)
45
45
 
46
- def test_ai_search_prompt_generates_and_executes_search_query(self):
46
+ def test_ai_search_prompt_generates_and_runs_sql(self):
47
47
  client = Mock()
48
48
  client._make_request.side_effect = [
49
49
  {
50
50
  "success": True,
51
51
  "data": {
52
- "searchQuery": {"labels": ["Pilot"], "where": {"ship": "Falcon"}},
52
+ "sql": 'SELECT __id FROM "Pilot" WHERE "ship" = \'Falcon\'',
53
+ "name": "Falcon pilots",
53
54
  "warnings": ["ambiguous ship name"],
54
55
  },
55
56
  },
@@ -60,38 +61,41 @@ class TestVectorAndSmartSearch(unittest.TestCase):
60
61
  },
61
62
  ]
62
63
 
63
- result = AIAPI(client).search(
64
- "Who are piloting Falcon?",
65
- current_query={"labels": ["Pilot"]},
66
- transaction="tx_123",
67
- )
64
+ result = AIAPI(client).search("Who are piloting Falcon?", limit=10)
68
65
 
69
66
  self.assertEqual(
70
67
  client._make_request.call_args_list[0].args,
71
- (
72
- "POST",
73
- "/ai/search-query",
74
- {
75
- "prompt": "Who are piloting Falcon?",
76
- "currentQuery": {"labels": ["Pilot"]},
77
- },
78
- {"X-Transaction-Id": "tx_123"},
79
- ),
68
+ ("POST", "/ai/sql-search", {"prompt": "Who are piloting Falcon?"}),
80
69
  )
70
+ sql = 'SELECT __id FROM "Pilot" WHERE "ship" = \'Falcon\''
71
+ self.assertEqual(
72
+ client._make_request.call_args_list[1].args,
73
+ ("POST", "/records/sql-search", {"sql": sql, "skip": 0, "limit": 10}),
74
+ )
75
+ self.assertEqual(result.sql, sql)
76
+ self.assertEqual(result.name, "Falcon pilots")
77
+ self.assertEqual(result.warnings, ["ambiguous ship name"])
78
+ self.assertEqual(result.search_query, {})
79
+ self.assertEqual(result[0].get("name"), "Han")
80
+
81
+ def test_smart_search_result_pages_through_its_sql(self):
82
+ client = Mock()
83
+ client._make_request.return_value = {"success": True, "total": 3, "data": []}
84
+ client.ai = AIAPI(client)
85
+
86
+ first = client.ai.run_sql('SELECT __id FROM "Pilot"', limit=2)
87
+ self.assertEqual((first.skip, first.limit), (0, 2))
88
+ second = first.next()
89
+
81
90
  self.assertEqual(
82
91
  client._make_request.call_args_list[1].args,
83
92
  (
84
93
  "POST",
85
- "/records/search",
86
- {"labels": ["Pilot"], "where": {"ship": "Falcon"}},
87
- {"X-Transaction-Id": "tx_123"},
94
+ "/records/sql-search",
95
+ {"sql": 'SELECT __id FROM "Pilot"', "skip": 2, "limit": 2},
88
96
  ),
89
97
  )
90
- self.assertEqual(
91
- result.search_query, {"labels": ["Pilot"], "where": {"ship": "Falcon"}}
92
- )
93
- self.assertEqual(result.warnings, ["ambiguous ship name"])
94
- self.assertEqual(result[0].get("name"), "Han")
98
+ self.assertEqual((second.skip, second.limit, second.sql), (2, 2, first.sql))
95
99
 
96
100
  def test_ai_search_dict_is_deprecated_vector_search_alias(self):
97
101
  client = Mock()
@@ -271,6 +271,7 @@ EVERY_METHOD = {
271
271
  "book-vec", {"items": [{"recordId": RECORD["id"], "vector": [1, 0, 0]}]}
272
272
  ),
273
273
  "ai.search": lambda db: db.ai.search("books about deserts"),
274
+ "ai.run_sql": lambda db: db.ai.run_sql('SELECT __id FROM "Book"'),
274
275
  "settings.get": lambda db: db.settings.get(),
275
276
  }
276
277
 
@@ -710,7 +710,7 @@ wheels = [
710
710
 
711
711
  [[package]]
712
712
  name = "rushdb"
713
- version = "3.0.0"
713
+ version = "3.1.0"
714
714
  source = { editable = "." }
715
715
 
716
716
  [package.dev-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes