langgraph-checkpoint-sqlite 2.0.8__tar.gz → 2.0.9__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.
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/PKG-INFO +1 -1
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/langgraph/store/sqlite/aio.py +2 -3
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/langgraph/store/sqlite/base.py +75 -379
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/pyproject.toml +1 -1
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/tests/test_async_store.py +165 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/tests/test_store.py +165 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/uv.lock +1 -1
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/.gitignore +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/Makefile +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/README.md +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/langgraph/cache/sqlite/__init__.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/langgraph/checkpoint/sqlite/__init__.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/langgraph/checkpoint/sqlite/aio.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/langgraph/checkpoint/sqlite/py.typed +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/langgraph/checkpoint/sqlite/utils.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/langgraph/store/sqlite/__init__.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/tests/__init__.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/tests/test_aiosqlite.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/tests/test_sqlite.py +0 -0
- {langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/tests/test_ttl.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-checkpoint-sqlite
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.9
|
|
4
4
|
Summary: Library with a SQLite implementation of LangGraph checkpoint saver.
|
|
5
5
|
Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
|
|
6
6
|
License-Expression: MIT
|
|
@@ -511,12 +511,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
511
511
|
|
|
512
512
|
# Setup dot_product function if it doesn't exist
|
|
513
513
|
if embedding_requests and self.embeddings:
|
|
514
|
-
# Generate embeddings for search queries
|
|
515
514
|
vectors = await self.embeddings.aembed_documents(
|
|
516
515
|
[query for _, query in embedding_requests]
|
|
517
516
|
)
|
|
518
517
|
|
|
519
|
-
# Replace placeholders with actual embeddings
|
|
520
518
|
for (idx, _), embedding in zip(embedding_requests, vectors):
|
|
521
519
|
_params_list: list = queries[idx][1]
|
|
522
520
|
for i, param in enumerate(_params_list):
|
|
@@ -527,7 +525,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
527
525
|
await cur.execute(query, params)
|
|
528
526
|
rows = await cur.fetchall()
|
|
529
527
|
|
|
530
|
-
if "score" in query:
|
|
528
|
+
if "score" in query:
|
|
531
529
|
items = [
|
|
532
530
|
_row_to_search_item(
|
|
533
531
|
_decode_ns_text(row[0]),
|
|
@@ -579,5 +577,6 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|
|
579
577
|
queries = self._get_batch_list_namespaces_queries(list_ops)
|
|
580
578
|
for (query, params), (idx, _) in zip(queries, list_ops):
|
|
581
579
|
await cur.execute(query, params)
|
|
580
|
+
|
|
582
581
|
rows = await cur.fetchall()
|
|
583
582
|
results[idx] = [_decode_ns_text(row[0]) for row in rows]
|
|
@@ -530,63 +530,95 @@ class BaseSqliteStore:
|
|
|
530
530
|
return queries, embedding_requests
|
|
531
531
|
|
|
532
532
|
def _get_batch_list_namespaces_queries(
|
|
533
|
-
self,
|
|
533
|
+
self,
|
|
534
|
+
list_ops: Sequence[tuple[int, ListNamespacesOp]],
|
|
534
535
|
) -> list[tuple[str, Sequence]]:
|
|
535
536
|
queries: list[tuple[str, Sequence]] = []
|
|
537
|
+
|
|
536
538
|
for _, op in list_ops:
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if op.max_depth is not None:
|
|
540
|
-
# SQLite doesn't have a built-in function for string splitting/joining with depth limit
|
|
541
|
-
# We'll use a more basic approach
|
|
542
|
-
query = """
|
|
543
|
-
WITH RECURSIVE split_prefix(prefix, remainder, depth) AS (
|
|
544
|
-
SELECT '', prefix || '.', 0 FROM (SELECT DISTINCT prefix FROM store)
|
|
545
|
-
UNION ALL
|
|
546
|
-
SELECT
|
|
547
|
-
CASE WHEN instr(remainder, '.') > 0
|
|
548
|
-
THEN prefix || CASE WHEN prefix = '' THEN '' ELSE '.' END || substr(remainder, 1, instr(remainder, '.') - 1)
|
|
549
|
-
ELSE prefix || CASE WHEN prefix = '' THEN '' ELSE '.' END || remainder
|
|
550
|
-
END,
|
|
551
|
-
CASE WHEN instr(remainder, '.') > 0
|
|
552
|
-
THEN substr(remainder, instr(remainder, '.') + 1)
|
|
553
|
-
ELSE ''
|
|
554
|
-
END,
|
|
555
|
-
depth + 1
|
|
556
|
-
FROM split_prefix
|
|
557
|
-
WHERE remainder != '' AND depth < ?
|
|
558
|
-
)
|
|
559
|
-
SELECT DISTINCT prefix FROM split_prefix WHERE depth > 0
|
|
560
|
-
"""
|
|
561
|
-
params: list[Any] = [op.max_depth]
|
|
562
|
-
else:
|
|
563
|
-
# If no max_depth is specified, we can just use a simpler query
|
|
564
|
-
query = "SELECT DISTINCT prefix FROM store"
|
|
565
|
-
params = []
|
|
539
|
+
where_clauses: list[str] = []
|
|
540
|
+
params: list[Any] = []
|
|
566
541
|
|
|
567
|
-
conditions = []
|
|
568
542
|
if op.match_conditions:
|
|
569
|
-
for
|
|
570
|
-
if
|
|
571
|
-
|
|
543
|
+
for cond in op.match_conditions:
|
|
544
|
+
if cond.match_type == "prefix":
|
|
545
|
+
where_clauses.append("prefix LIKE ?")
|
|
572
546
|
params.append(
|
|
573
|
-
f"{_namespace_to_text(
|
|
547
|
+
f"{_namespace_to_text(cond.path, handle_wildcards=True)}%"
|
|
574
548
|
)
|
|
575
|
-
elif
|
|
576
|
-
|
|
549
|
+
elif cond.match_type == "suffix":
|
|
550
|
+
where_clauses.append("prefix LIKE ?")
|
|
577
551
|
params.append(
|
|
578
|
-
f"%{_namespace_to_text(
|
|
552
|
+
f"%{_namespace_to_text(cond.path, handle_wildcards=True)}"
|
|
579
553
|
)
|
|
580
554
|
else:
|
|
581
555
|
logger.warning(
|
|
582
|
-
|
|
556
|
+
"Unknown match_type in list_namespaces: %s", cond.match_type
|
|
583
557
|
)
|
|
584
558
|
|
|
585
|
-
if
|
|
586
|
-
|
|
559
|
+
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
|
560
|
+
|
|
561
|
+
if op.max_depth is not None:
|
|
562
|
+
query = f"""
|
|
563
|
+
WITH RECURSIVE split(original, truncated, remainder, depth) AS (
|
|
564
|
+
SELECT
|
|
565
|
+
prefix AS original,
|
|
566
|
+
'' AS truncated,
|
|
567
|
+
prefix AS remainder,
|
|
568
|
+
0 AS depth
|
|
569
|
+
FROM (SELECT DISTINCT prefix FROM store {where_sql})
|
|
570
|
+
|
|
571
|
+
UNION ALL
|
|
572
|
+
|
|
573
|
+
SELECT
|
|
574
|
+
original,
|
|
575
|
+
CASE
|
|
576
|
+
WHEN depth = 0
|
|
577
|
+
THEN substr(remainder,
|
|
578
|
+
1,
|
|
579
|
+
CASE
|
|
580
|
+
WHEN instr(remainder, '.') > 0
|
|
581
|
+
THEN instr(remainder, '.') - 1
|
|
582
|
+
ELSE length(remainder)
|
|
583
|
+
END)
|
|
584
|
+
ELSE
|
|
585
|
+
truncated || '.' ||
|
|
586
|
+
substr(remainder,
|
|
587
|
+
1,
|
|
588
|
+
CASE
|
|
589
|
+
WHEN instr(remainder, '.') > 0
|
|
590
|
+
THEN instr(remainder, '.') - 1
|
|
591
|
+
ELSE length(remainder)
|
|
592
|
+
END)
|
|
593
|
+
END AS truncated,
|
|
594
|
+
CASE
|
|
595
|
+
WHEN instr(remainder, '.') > 0
|
|
596
|
+
THEN substr(remainder, instr(remainder, '.') + 1)
|
|
597
|
+
ELSE ''
|
|
598
|
+
END AS remainder,
|
|
599
|
+
depth + 1 AS depth
|
|
600
|
+
FROM split
|
|
601
|
+
WHERE remainder <> ''
|
|
602
|
+
AND depth < ?
|
|
603
|
+
)
|
|
604
|
+
SELECT DISTINCT truncated AS prefix
|
|
605
|
+
FROM split
|
|
606
|
+
WHERE depth = ? OR remainder = ''
|
|
607
|
+
ORDER BY prefix
|
|
608
|
+
LIMIT ? OFFSET ?
|
|
609
|
+
"""
|
|
610
|
+
params.extend([op.max_depth, op.max_depth, op.limit, op.offset])
|
|
611
|
+
|
|
612
|
+
else:
|
|
613
|
+
query = f"""
|
|
614
|
+
SELECT DISTINCT prefix
|
|
615
|
+
FROM store
|
|
616
|
+
{where_sql}
|
|
617
|
+
ORDER BY prefix
|
|
618
|
+
LIMIT ? OFFSET ?
|
|
619
|
+
"""
|
|
620
|
+
params.extend([op.limit, op.offset])
|
|
587
621
|
|
|
588
|
-
query += " ORDER BY prefix LIMIT ? OFFSET ?"
|
|
589
|
-
params.extend([op.limit, op.offset])
|
|
590
622
|
queries.append((query, tuple(params)))
|
|
591
623
|
|
|
592
624
|
return queries
|
|
@@ -828,342 +860,6 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|
|
828
860
|
|
|
829
861
|
return results
|
|
830
862
|
|
|
831
|
-
def _prepare_batch_PUT_queries(
|
|
832
|
-
self, put_ops: Sequence[tuple[int, PutOp]]
|
|
833
|
-
) -> tuple[
|
|
834
|
-
list[tuple[str, Sequence]],
|
|
835
|
-
Optional[tuple[str, Sequence[tuple[str, str, str, str]]]],
|
|
836
|
-
]:
|
|
837
|
-
# Last-write wins
|
|
838
|
-
dedupped_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
|
|
839
|
-
for _, op in put_ops:
|
|
840
|
-
dedupped_ops[(op.namespace, op.key)] = op
|
|
841
|
-
|
|
842
|
-
inserts: list[PutOp] = []
|
|
843
|
-
deletes: list[PutOp] = []
|
|
844
|
-
for op in dedupped_ops.values():
|
|
845
|
-
if op.value is None:
|
|
846
|
-
deletes.append(op)
|
|
847
|
-
else:
|
|
848
|
-
inserts.append(op)
|
|
849
|
-
|
|
850
|
-
queries: list[tuple[str, Sequence]] = []
|
|
851
|
-
|
|
852
|
-
if deletes:
|
|
853
|
-
namespace_groups: dict[tuple[str, ...], list[str]] = defaultdict(list)
|
|
854
|
-
for op in deletes:
|
|
855
|
-
namespace_groups[op.namespace].append(op.key)
|
|
856
|
-
for namespace, keys in namespace_groups.items():
|
|
857
|
-
placeholders = ",".join(["?" for _ in keys])
|
|
858
|
-
query = (
|
|
859
|
-
f"DELETE FROM store WHERE prefix = ? AND key IN ({placeholders})"
|
|
860
|
-
)
|
|
861
|
-
params = (_namespace_to_text(namespace), *keys)
|
|
862
|
-
queries.append((query, params))
|
|
863
|
-
|
|
864
|
-
embedding_request: Optional[tuple[str, Sequence[tuple[str, str, str, str]]]] = (
|
|
865
|
-
None
|
|
866
|
-
)
|
|
867
|
-
if inserts:
|
|
868
|
-
values = []
|
|
869
|
-
insertion_params = []
|
|
870
|
-
vector_values = []
|
|
871
|
-
embedding_request_params = []
|
|
872
|
-
now = datetime.datetime.now(datetime.timezone.utc)
|
|
873
|
-
|
|
874
|
-
# First handle main store insertions
|
|
875
|
-
for op in inserts:
|
|
876
|
-
if op.ttl is None:
|
|
877
|
-
expires_at = None
|
|
878
|
-
else:
|
|
879
|
-
expires_at = now + datetime.timedelta(minutes=op.ttl)
|
|
880
|
-
values.append("(?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?, ?)")
|
|
881
|
-
insertion_params.extend(
|
|
882
|
-
[
|
|
883
|
-
_namespace_to_text(op.namespace),
|
|
884
|
-
op.key,
|
|
885
|
-
orjson.dumps(cast(dict, op.value)),
|
|
886
|
-
expires_at,
|
|
887
|
-
op.ttl,
|
|
888
|
-
]
|
|
889
|
-
)
|
|
890
|
-
|
|
891
|
-
# Then handle embeddings if configured
|
|
892
|
-
if self.index_config:
|
|
893
|
-
for op in inserts:
|
|
894
|
-
if op.index is False:
|
|
895
|
-
continue
|
|
896
|
-
value = op.value
|
|
897
|
-
ns = _namespace_to_text(op.namespace)
|
|
898
|
-
k = op.key
|
|
899
|
-
|
|
900
|
-
if op.index is None:
|
|
901
|
-
paths = self.index_config["__tokenized_fields"]
|
|
902
|
-
else:
|
|
903
|
-
paths = [(ix, tokenize_path(ix)) for ix in op.index]
|
|
904
|
-
|
|
905
|
-
for path, tokenized_path in paths:
|
|
906
|
-
texts = get_text_at_path(value, tokenized_path)
|
|
907
|
-
for i, text in enumerate(texts):
|
|
908
|
-
pathname = f"{path}.{i}" if len(texts) > 1 else path
|
|
909
|
-
vector_values.append(
|
|
910
|
-
"(?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
|
|
911
|
-
)
|
|
912
|
-
embedding_request_params.append((ns, k, pathname, text))
|
|
913
|
-
|
|
914
|
-
values_str = ",".join(values)
|
|
915
|
-
query = f"""
|
|
916
|
-
INSERT OR REPLACE INTO store (prefix, key, value, created_at, updated_at, expires_at, ttl_minutes)
|
|
917
|
-
VALUES {values_str}
|
|
918
|
-
"""
|
|
919
|
-
queries.append((query, insertion_params))
|
|
920
|
-
|
|
921
|
-
if vector_values:
|
|
922
|
-
values_str = ",".join(vector_values)
|
|
923
|
-
query = f"""
|
|
924
|
-
INSERT OR REPLACE INTO store_vectors (prefix, key, field_name, embedding, created_at, updated_at)
|
|
925
|
-
VALUES {values_str}
|
|
926
|
-
"""
|
|
927
|
-
embedding_request = (query, embedding_request_params)
|
|
928
|
-
|
|
929
|
-
return queries, embedding_request
|
|
930
|
-
|
|
931
|
-
def _prepare_batch_search_queries(
|
|
932
|
-
self, search_ops: Sequence[tuple[int, SearchOp]]
|
|
933
|
-
) -> tuple[
|
|
934
|
-
list[tuple[str, list[Union[None, str, list[float]]]]], # queries, params
|
|
935
|
-
list[tuple[int, str]], # idx, query_text pairs to embed
|
|
936
|
-
]:
|
|
937
|
-
"""
|
|
938
|
-
Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
|
|
939
|
-
Returns:
|
|
940
|
-
- queries: list of (SQL, param_list)
|
|
941
|
-
- embedding_requests: list of (original_index_in_search_ops, text_query)
|
|
942
|
-
"""
|
|
943
|
-
queries = []
|
|
944
|
-
embedding_requests = []
|
|
945
|
-
|
|
946
|
-
for idx, (_, op) in enumerate(search_ops):
|
|
947
|
-
# Build filter conditions first
|
|
948
|
-
filter_params = []
|
|
949
|
-
filter_conditions = []
|
|
950
|
-
if op.filter:
|
|
951
|
-
for key, value in op.filter.items():
|
|
952
|
-
if isinstance(value, dict):
|
|
953
|
-
for op_name, val in value.items():
|
|
954
|
-
condition, filter_params_ = self._get_filter_condition(
|
|
955
|
-
key, op_name, val
|
|
956
|
-
)
|
|
957
|
-
filter_conditions.append(condition)
|
|
958
|
-
filter_params.extend(filter_params_)
|
|
959
|
-
else:
|
|
960
|
-
# SQLite json_extract returns unquoted string values
|
|
961
|
-
if isinstance(value, str):
|
|
962
|
-
filter_conditions.append(
|
|
963
|
-
"json_extract(value, '$."
|
|
964
|
-
+ key
|
|
965
|
-
+ "') = '"
|
|
966
|
-
+ value.replace("'", "''")
|
|
967
|
-
+ "'"
|
|
968
|
-
)
|
|
969
|
-
elif value is None:
|
|
970
|
-
filter_conditions.append(
|
|
971
|
-
"json_extract(value, '$." + key + "') IS NULL"
|
|
972
|
-
)
|
|
973
|
-
elif isinstance(value, bool):
|
|
974
|
-
# SQLite JSON stores booleans as integers
|
|
975
|
-
filter_conditions.append(
|
|
976
|
-
"json_extract(value, '$."
|
|
977
|
-
+ key
|
|
978
|
-
+ "') = "
|
|
979
|
-
+ ("1" if value else "0")
|
|
980
|
-
)
|
|
981
|
-
elif isinstance(value, (int, float)):
|
|
982
|
-
filter_conditions.append(
|
|
983
|
-
"json_extract(value, '$." + key + "') = " + str(value)
|
|
984
|
-
)
|
|
985
|
-
else:
|
|
986
|
-
# For complex objects, use param binding with JSON serialization
|
|
987
|
-
filter_conditions.append(
|
|
988
|
-
"json_extract(value, '$." + key + "') = ?"
|
|
989
|
-
)
|
|
990
|
-
filter_params.append(orjson.dumps(value))
|
|
991
|
-
|
|
992
|
-
# Vector search branch
|
|
993
|
-
if op.query and self.index_config:
|
|
994
|
-
embedding_requests.append((idx, op.query))
|
|
995
|
-
|
|
996
|
-
# Choose the similarity function and score expression based on distance type
|
|
997
|
-
distance_type = self.index_config.get("distance_type", "cosine")
|
|
998
|
-
|
|
999
|
-
if distance_type == "cosine":
|
|
1000
|
-
score_expr = "1.0 - vec_distance_cosine(sv.embedding, ?)"
|
|
1001
|
-
elif distance_type == "l2":
|
|
1002
|
-
score_expr = "vec_distance_L2(sv.embedding, ?)"
|
|
1003
|
-
elif distance_type == "inner_product":
|
|
1004
|
-
# For inner product, we want higher values to be better, so negate the result
|
|
1005
|
-
# since inner product similarity is higher when vectors are more similar
|
|
1006
|
-
score_expr = "-1 * vec_distance_L1(sv.embedding, ?)"
|
|
1007
|
-
else:
|
|
1008
|
-
# Default to cosine similarity
|
|
1009
|
-
score_expr = "1.0 - vec_distance_cosine(sv.embedding, ?)"
|
|
1010
|
-
|
|
1011
|
-
filter_str = (
|
|
1012
|
-
""
|
|
1013
|
-
if not filter_conditions
|
|
1014
|
-
else " AND " + " AND ".join(filter_conditions)
|
|
1015
|
-
)
|
|
1016
|
-
if op.namespace_prefix:
|
|
1017
|
-
prefix_filter_str = f"WHERE s.prefix LIKE ? {filter_str} "
|
|
1018
|
-
ns_args: Sequence = (f"{_namespace_to_text(op.namespace_prefix)}%",)
|
|
1019
|
-
else:
|
|
1020
|
-
ns_args = ()
|
|
1021
|
-
if filter_str:
|
|
1022
|
-
prefix_filter_str = f"WHERE {filter_str[5:]} "
|
|
1023
|
-
else:
|
|
1024
|
-
prefix_filter_str = ""
|
|
1025
|
-
|
|
1026
|
-
# We use a CTE to compute scores, with a SQLite-compatible approach for distinct results
|
|
1027
|
-
base_query = f"""
|
|
1028
|
-
WITH scored AS (
|
|
1029
|
-
SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, s.expires_at, s.ttl_minutes,
|
|
1030
|
-
{score_expr} AS score
|
|
1031
|
-
FROM store s
|
|
1032
|
-
JOIN store_vectors sv ON s.prefix = sv.prefix AND s.key = sv.key
|
|
1033
|
-
{prefix_filter_str}
|
|
1034
|
-
ORDER BY score DESC
|
|
1035
|
-
LIMIT ?
|
|
1036
|
-
),
|
|
1037
|
-
ranked AS (
|
|
1038
|
-
SELECT prefix, key, value, created_at, updated_at, expires_at, ttl_minutes, score,
|
|
1039
|
-
ROW_NUMBER() OVER (PARTITION BY prefix, key ORDER BY score DESC) as rn
|
|
1040
|
-
FROM scored
|
|
1041
|
-
)
|
|
1042
|
-
SELECT prefix, key, value, created_at, updated_at, expires_at, ttl_minutes, score
|
|
1043
|
-
FROM ranked
|
|
1044
|
-
WHERE rn = 1
|
|
1045
|
-
ORDER BY score DESC
|
|
1046
|
-
LIMIT ?
|
|
1047
|
-
OFFSET ?
|
|
1048
|
-
"""
|
|
1049
|
-
params = [
|
|
1050
|
-
_PLACEHOLDER, # Vector placeholder
|
|
1051
|
-
*ns_args,
|
|
1052
|
-
*filter_params,
|
|
1053
|
-
op.limit * 2, # Expanded limit for better results
|
|
1054
|
-
op.limit,
|
|
1055
|
-
op.offset,
|
|
1056
|
-
]
|
|
1057
|
-
# Regular search branch (no vector search)
|
|
1058
|
-
else:
|
|
1059
|
-
base_query = """
|
|
1060
|
-
SELECT prefix, key, value, created_at, updated_at, expires_at, ttl_minutes, NULL as score
|
|
1061
|
-
FROM store
|
|
1062
|
-
WHERE prefix LIKE ?
|
|
1063
|
-
"""
|
|
1064
|
-
params = [f"{_namespace_to_text(op.namespace_prefix)}%"]
|
|
1065
|
-
|
|
1066
|
-
if filter_conditions:
|
|
1067
|
-
params.extend(filter_params)
|
|
1068
|
-
base_query += " AND " + " AND ".join(filter_conditions)
|
|
1069
|
-
|
|
1070
|
-
base_query += " ORDER BY updated_at DESC"
|
|
1071
|
-
base_query += " LIMIT ? OFFSET ?"
|
|
1072
|
-
params.extend([op.limit, op.offset])
|
|
1073
|
-
|
|
1074
|
-
# Debug the query
|
|
1075
|
-
logger.debug(f"Search query: {base_query}")
|
|
1076
|
-
logger.debug(f"Search params: {params}")
|
|
1077
|
-
|
|
1078
|
-
# Handle TTL refresh if requested
|
|
1079
|
-
if (
|
|
1080
|
-
op.refresh_ttl
|
|
1081
|
-
and self.ttl_config
|
|
1082
|
-
and self.ttl_config.get("refresh_on_read", False)
|
|
1083
|
-
):
|
|
1084
|
-
final_sql = f"""
|
|
1085
|
-
WITH search_results AS (
|
|
1086
|
-
{base_query}
|
|
1087
|
-
),
|
|
1088
|
-
updated AS (
|
|
1089
|
-
UPDATE store
|
|
1090
|
-
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
|
|
1091
|
-
WHERE (prefix, key) IN (SELECT prefix, key FROM search_results)
|
|
1092
|
-
AND ttl_minutes IS NOT NULL
|
|
1093
|
-
)
|
|
1094
|
-
SELECT * FROM search_results
|
|
1095
|
-
"""
|
|
1096
|
-
final_params = params[:] # copy params
|
|
1097
|
-
else:
|
|
1098
|
-
final_sql = base_query
|
|
1099
|
-
final_params = params
|
|
1100
|
-
|
|
1101
|
-
queries.append((final_sql, final_params))
|
|
1102
|
-
|
|
1103
|
-
return queries, embedding_requests
|
|
1104
|
-
|
|
1105
|
-
def _get_batch_list_namespaces_queries(
|
|
1106
|
-
self, list_ops: Sequence[tuple[int, ListNamespacesOp]]
|
|
1107
|
-
) -> list[tuple[str, Sequence]]:
|
|
1108
|
-
queries: list[tuple[str, Sequence]] = []
|
|
1109
|
-
for _, op in list_ops:
|
|
1110
|
-
# In SQLite, we need to use a different approach for namespace segmentation
|
|
1111
|
-
# since there's no direct equivalent to PostgreSQL's string aggregation
|
|
1112
|
-
if op.max_depth is not None:
|
|
1113
|
-
# SQLite doesn't have a built-in function for string splitting/joining with depth limit
|
|
1114
|
-
# We'll use a more basic approach
|
|
1115
|
-
query = """
|
|
1116
|
-
WITH RECURSIVE split_prefix(prefix, remainder, depth) AS (
|
|
1117
|
-
SELECT '', prefix || '.', 0 FROM (SELECT DISTINCT prefix FROM store)
|
|
1118
|
-
UNION ALL
|
|
1119
|
-
SELECT
|
|
1120
|
-
CASE WHEN instr(remainder, '.') > 0
|
|
1121
|
-
THEN prefix || CASE WHEN prefix = '' THEN '' ELSE '.' END || substr(remainder, 1, instr(remainder, '.') - 1)
|
|
1122
|
-
ELSE prefix || CASE WHEN prefix = '' THEN '' ELSE '.' END || remainder
|
|
1123
|
-
END,
|
|
1124
|
-
CASE WHEN instr(remainder, '.') > 0
|
|
1125
|
-
THEN substr(remainder, instr(remainder, '.') + 1)
|
|
1126
|
-
ELSE ''
|
|
1127
|
-
END,
|
|
1128
|
-
depth + 1
|
|
1129
|
-
FROM split_prefix
|
|
1130
|
-
WHERE remainder != '' AND depth < ?
|
|
1131
|
-
)
|
|
1132
|
-
SELECT DISTINCT prefix FROM split_prefix WHERE depth > 0
|
|
1133
|
-
"""
|
|
1134
|
-
params: list[Any] = [op.max_depth]
|
|
1135
|
-
else:
|
|
1136
|
-
# If no max_depth is specified, we can just use a simpler query
|
|
1137
|
-
query = "SELECT DISTINCT prefix FROM store"
|
|
1138
|
-
params = []
|
|
1139
|
-
|
|
1140
|
-
conditions = []
|
|
1141
|
-
if op.match_conditions:
|
|
1142
|
-
for condition in op.match_conditions:
|
|
1143
|
-
if condition.match_type == "prefix":
|
|
1144
|
-
conditions.append("prefix LIKE ?")
|
|
1145
|
-
params.append(
|
|
1146
|
-
f"{_namespace_to_text(condition.path, handle_wildcards=True)}%"
|
|
1147
|
-
)
|
|
1148
|
-
elif condition.match_type == "suffix":
|
|
1149
|
-
conditions.append("prefix LIKE ?")
|
|
1150
|
-
params.append(
|
|
1151
|
-
f"%{_namespace_to_text(condition.path, handle_wildcards=True)}"
|
|
1152
|
-
)
|
|
1153
|
-
else:
|
|
1154
|
-
logger.warning(
|
|
1155
|
-
f"Unknown match_type in list_namespaces: {condition.match_type}"
|
|
1156
|
-
)
|
|
1157
|
-
|
|
1158
|
-
if conditions:
|
|
1159
|
-
query += " WHERE " + " AND ".join(conditions)
|
|
1160
|
-
|
|
1161
|
-
query += " ORDER BY prefix LIMIT ? OFFSET ?"
|
|
1162
|
-
params.extend([op.limit, op.offset])
|
|
1163
|
-
queries.append((query, tuple(params)))
|
|
1164
|
-
|
|
1165
|
-
return queries
|
|
1166
|
-
|
|
1167
863
|
def _get_filter_condition(self, key: str, op: str, value: Any) -> tuple[str, list]:
|
|
1168
864
|
"""Helper to generate filter conditions."""
|
|
1169
865
|
# We need to properly format values for SQLite JSON extraction comparison
|
{langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/tests/test_async_store.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import os
|
|
4
4
|
import tempfile
|
|
5
|
+
import uuid
|
|
5
6
|
from collections.abc import AsyncIterator, Generator, Iterable
|
|
6
7
|
from contextlib import asynccontextmanager
|
|
7
8
|
from typing import Optional, Union, cast
|
|
@@ -492,3 +493,167 @@ async def test_embed_with_path(
|
|
|
492
493
|
assert len(results) == 2
|
|
493
494
|
assert results[0].score < 0.9
|
|
494
495
|
assert results[1].score < 0.9
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
async def test_basic_store_ops(
|
|
499
|
+
fake_embeddings: CharacterEmbeddings,
|
|
500
|
+
) -> None:
|
|
501
|
+
"""Test vector search with specific text fields in SQLite store."""
|
|
502
|
+
async with create_vector_store(
|
|
503
|
+
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
|
504
|
+
) as store:
|
|
505
|
+
uid = uuid.uuid4().hex
|
|
506
|
+
namespace = (uid, "test", "documents")
|
|
507
|
+
item_id = "doc1"
|
|
508
|
+
item_value = {"title": "Test Document", "content": "Hello, World!"}
|
|
509
|
+
results = await store.asearch((uid,))
|
|
510
|
+
assert len(results) == 0
|
|
511
|
+
|
|
512
|
+
await store.aput(namespace, item_id, item_value)
|
|
513
|
+
item = await store.aget(namespace, item_id)
|
|
514
|
+
|
|
515
|
+
assert item is not None
|
|
516
|
+
assert item.namespace == namespace
|
|
517
|
+
assert item.key == item_id
|
|
518
|
+
assert item.value == item_value
|
|
519
|
+
assert item.created_at is not None
|
|
520
|
+
assert item.updated_at is not None
|
|
521
|
+
|
|
522
|
+
updated_value = {
|
|
523
|
+
"title": "Updated Test Document",
|
|
524
|
+
"content": "Hello, LangGraph!",
|
|
525
|
+
}
|
|
526
|
+
await asyncio.sleep(1.01)
|
|
527
|
+
await store.aput(namespace, item_id, updated_value)
|
|
528
|
+
updated_item = await store.aget(namespace, item_id)
|
|
529
|
+
assert updated_item is not None
|
|
530
|
+
|
|
531
|
+
assert updated_item.value == updated_value
|
|
532
|
+
assert updated_item.updated_at > item.updated_at
|
|
533
|
+
different_namespace = (uid, "test", "other_documents")
|
|
534
|
+
item_in_different_namespace = await store.aget(different_namespace, item_id)
|
|
535
|
+
assert item_in_different_namespace is None
|
|
536
|
+
|
|
537
|
+
new_item_id = "doc2"
|
|
538
|
+
new_item_value = {"title": "Another Document", "content": "Greetings!"}
|
|
539
|
+
await store.aput(namespace, new_item_id, new_item_value)
|
|
540
|
+
|
|
541
|
+
items = await store.asearch((uid, "test"), limit=10)
|
|
542
|
+
assert len(items) == 2
|
|
543
|
+
assert any(item.key == item_id for item in items)
|
|
544
|
+
assert any(item.key == new_item_id for item in items)
|
|
545
|
+
|
|
546
|
+
namespaces = await store.alist_namespaces(prefix=(uid, "test"))
|
|
547
|
+
assert (uid, "test", "documents") in namespaces
|
|
548
|
+
|
|
549
|
+
await store.adelete(namespace, item_id)
|
|
550
|
+
await store.adelete(namespace, new_item_id)
|
|
551
|
+
deleted_item = await store.aget(namespace, item_id)
|
|
552
|
+
assert deleted_item is None
|
|
553
|
+
|
|
554
|
+
deleted_item = await store.aget(namespace, new_item_id)
|
|
555
|
+
assert deleted_item is None
|
|
556
|
+
|
|
557
|
+
empty_search_results = await store.asearch((uid, "test"), limit=10)
|
|
558
|
+
assert len(empty_search_results) == 0
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
async def test_list_namespaces(
|
|
562
|
+
fake_embeddings: CharacterEmbeddings,
|
|
563
|
+
) -> None:
|
|
564
|
+
"""Test list namespaces functionality with various filters."""
|
|
565
|
+
async with create_vector_store(
|
|
566
|
+
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
|
567
|
+
) as store:
|
|
568
|
+
test_pref = str(uuid.uuid4())
|
|
569
|
+
test_namespaces = [
|
|
570
|
+
(test_pref, "test", "documents", "public", test_pref),
|
|
571
|
+
(test_pref, "test", "documents", "private", test_pref),
|
|
572
|
+
(test_pref, "test", "images", "public", test_pref),
|
|
573
|
+
(test_pref, "test", "images", "private", test_pref),
|
|
574
|
+
(test_pref, "prod", "documents", "public", test_pref),
|
|
575
|
+
(test_pref, "prod", "documents", "some", "nesting", "public", test_pref),
|
|
576
|
+
(test_pref, "prod", "documents", "private", test_pref),
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
# Add test data
|
|
580
|
+
for namespace in test_namespaces:
|
|
581
|
+
await store.aput(namespace, "dummy", {"content": "dummy"})
|
|
582
|
+
|
|
583
|
+
# Test prefix filtering
|
|
584
|
+
prefix_result = await store.alist_namespaces(prefix=(test_pref, "test"))
|
|
585
|
+
assert len(prefix_result) == 4
|
|
586
|
+
assert all(ns[1] == "test" for ns in prefix_result)
|
|
587
|
+
|
|
588
|
+
# Test specific prefix
|
|
589
|
+
specific_prefix_result = await store.alist_namespaces(
|
|
590
|
+
prefix=(test_pref, "test", "documents")
|
|
591
|
+
)
|
|
592
|
+
assert len(specific_prefix_result) == 2
|
|
593
|
+
assert all(ns[1:3] == ("test", "documents") for ns in specific_prefix_result)
|
|
594
|
+
|
|
595
|
+
# Test suffix filtering
|
|
596
|
+
suffix_result = await store.alist_namespaces(suffix=("public", test_pref))
|
|
597
|
+
assert len(suffix_result) == 4
|
|
598
|
+
assert all(ns[-2] == "public" for ns in suffix_result)
|
|
599
|
+
|
|
600
|
+
# Test combined prefix and suffix
|
|
601
|
+
prefix_suffix_result = await store.alist_namespaces(
|
|
602
|
+
prefix=(test_pref, "test"), suffix=("public", test_pref)
|
|
603
|
+
)
|
|
604
|
+
assert len(prefix_suffix_result) == 2
|
|
605
|
+
assert all(
|
|
606
|
+
ns[1] == "test" and ns[-2] == "public" for ns in prefix_suffix_result
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# Test wildcard in prefix
|
|
610
|
+
wildcard_prefix_result = await store.alist_namespaces(
|
|
611
|
+
prefix=(test_pref, "*", "documents")
|
|
612
|
+
)
|
|
613
|
+
assert len(wildcard_prefix_result) == 5
|
|
614
|
+
assert all(ns[2] == "documents" for ns in wildcard_prefix_result)
|
|
615
|
+
|
|
616
|
+
# Test wildcard in suffix
|
|
617
|
+
wildcard_suffix_result = await store.alist_namespaces(
|
|
618
|
+
suffix=("*", "public", test_pref)
|
|
619
|
+
)
|
|
620
|
+
assert len(wildcard_suffix_result) == 4
|
|
621
|
+
assert all(ns[-2] == "public" for ns in wildcard_suffix_result)
|
|
622
|
+
|
|
623
|
+
wildcard_single = await store.alist_namespaces(
|
|
624
|
+
suffix=("some", "*", "public", test_pref)
|
|
625
|
+
)
|
|
626
|
+
assert len(wildcard_single) == 1
|
|
627
|
+
assert wildcard_single[0] == (
|
|
628
|
+
test_pref,
|
|
629
|
+
"prod",
|
|
630
|
+
"documents",
|
|
631
|
+
"some",
|
|
632
|
+
"nesting",
|
|
633
|
+
"public",
|
|
634
|
+
test_pref,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# Test max depth
|
|
638
|
+
max_depth_result = await store.alist_namespaces(max_depth=3)
|
|
639
|
+
assert all(len(ns) <= 3 for ns in max_depth_result)
|
|
640
|
+
|
|
641
|
+
max_depth_result = await store.alist_namespaces(
|
|
642
|
+
max_depth=4, prefix=(test_pref, "*", "documents")
|
|
643
|
+
)
|
|
644
|
+
assert len(set(res for res in max_depth_result)) == len(max_depth_result) == 5
|
|
645
|
+
|
|
646
|
+
# Test pagination
|
|
647
|
+
limit_result = await store.alist_namespaces(prefix=(test_pref,), limit=3)
|
|
648
|
+
assert len(limit_result) == 3
|
|
649
|
+
|
|
650
|
+
offset_result = await store.alist_namespaces(prefix=(test_pref,), offset=3)
|
|
651
|
+
assert len(offset_result) == len(test_namespaces) - 3
|
|
652
|
+
|
|
653
|
+
empty_prefix_result = await store.alist_namespaces(prefix=(test_pref,))
|
|
654
|
+
assert len(empty_prefix_result) == len(test_namespaces)
|
|
655
|
+
assert set(empty_prefix_result) == set(test_namespaces)
|
|
656
|
+
|
|
657
|
+
# Clean up
|
|
658
|
+
for namespace in test_namespaces:
|
|
659
|
+
await store.adelete(namespace, "dummy")
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import tempfile
|
|
5
|
+
import uuid
|
|
5
6
|
from collections.abc import Generator, Iterable
|
|
6
7
|
from contextlib import contextmanager
|
|
7
8
|
from typing import Any, Literal, Optional, Union, cast
|
|
@@ -822,3 +823,167 @@ def test_nonnull_migrations() -> None:
|
|
|
822
823
|
for migration in SqliteStore.MIGRATIONS:
|
|
823
824
|
statement = _leading_comment_remover.sub("", migration).split()[0]
|
|
824
825
|
assert statement.strip(), f"Empty migration statement found: {migration}"
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def test_basic_store_operations(
|
|
829
|
+
fake_embeddings: CharacterEmbeddings,
|
|
830
|
+
) -> None:
|
|
831
|
+
"""Test basic store operations with SQLite store."""
|
|
832
|
+
with create_vector_store(
|
|
833
|
+
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
|
834
|
+
) as store:
|
|
835
|
+
uid = uuid.uuid4().hex
|
|
836
|
+
namespace = (uid, "test", "documents")
|
|
837
|
+
item_id = "doc1"
|
|
838
|
+
item_value = {"title": "Test Document", "content": "Hello, World!"}
|
|
839
|
+
results = store.search((uid,))
|
|
840
|
+
assert len(results) == 0
|
|
841
|
+
|
|
842
|
+
store.put(namespace, item_id, item_value)
|
|
843
|
+
item = store.get(namespace, item_id)
|
|
844
|
+
|
|
845
|
+
assert item is not None
|
|
846
|
+
assert item.namespace == namespace
|
|
847
|
+
assert item.key == item_id
|
|
848
|
+
assert item.value == item_value
|
|
849
|
+
assert item.created_at is not None
|
|
850
|
+
assert item.updated_at is not None
|
|
851
|
+
|
|
852
|
+
updated_value = {
|
|
853
|
+
"title": "Updated Test Document",
|
|
854
|
+
"content": "Hello, LangGraph!",
|
|
855
|
+
}
|
|
856
|
+
store.put(namespace, item_id, updated_value)
|
|
857
|
+
updated_item = store.get(namespace, item_id)
|
|
858
|
+
assert updated_item is not None
|
|
859
|
+
|
|
860
|
+
assert updated_item.value == updated_value
|
|
861
|
+
assert updated_item.updated_at >= item.updated_at
|
|
862
|
+
|
|
863
|
+
different_namespace = (uid, "test", "other_documents")
|
|
864
|
+
item_in_different_namespace = store.get(different_namespace, item_id)
|
|
865
|
+
assert item_in_different_namespace is None
|
|
866
|
+
|
|
867
|
+
new_item_id = "doc2"
|
|
868
|
+
new_item_value = {"title": "Another Document", "content": "Greetings!"}
|
|
869
|
+
store.put(namespace, new_item_id, new_item_value)
|
|
870
|
+
|
|
871
|
+
items = store.search((uid, "test"), limit=10)
|
|
872
|
+
assert len(items) == 2
|
|
873
|
+
assert any(item.key == item_id for item in items)
|
|
874
|
+
assert any(item.key == new_item_id for item in items)
|
|
875
|
+
|
|
876
|
+
namespaces = store.list_namespaces(prefix=(uid, "test"))
|
|
877
|
+
assert (uid, "test", "documents") in namespaces
|
|
878
|
+
|
|
879
|
+
store.delete(namespace, item_id)
|
|
880
|
+
store.delete(namespace, new_item_id)
|
|
881
|
+
deleted_item = store.get(namespace, item_id)
|
|
882
|
+
assert deleted_item is None
|
|
883
|
+
|
|
884
|
+
deleted_item = store.get(namespace, new_item_id)
|
|
885
|
+
assert deleted_item is None
|
|
886
|
+
|
|
887
|
+
empty_search_results = store.search((uid, "test"), limit=10)
|
|
888
|
+
assert len(empty_search_results) == 0
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def test_list_namespaces_operations(
|
|
892
|
+
fake_embeddings: CharacterEmbeddings,
|
|
893
|
+
) -> None:
|
|
894
|
+
"""Test list namespaces functionality with various filters."""
|
|
895
|
+
with create_vector_store(
|
|
896
|
+
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
|
897
|
+
) as store:
|
|
898
|
+
test_pref = str(uuid.uuid4())
|
|
899
|
+
test_namespaces = [
|
|
900
|
+
(test_pref, "test", "documents", "public", test_pref),
|
|
901
|
+
(test_pref, "test", "documents", "private", test_pref),
|
|
902
|
+
(test_pref, "test", "images", "public", test_pref),
|
|
903
|
+
(test_pref, "test", "images", "private", test_pref),
|
|
904
|
+
(test_pref, "prod", "documents", "public", test_pref),
|
|
905
|
+
(test_pref, "prod", "documents", "some", "nesting", "public", test_pref),
|
|
906
|
+
(test_pref, "prod", "documents", "private", test_pref),
|
|
907
|
+
]
|
|
908
|
+
|
|
909
|
+
# Add test data
|
|
910
|
+
for namespace in test_namespaces:
|
|
911
|
+
store.put(namespace, "dummy", {"content": "dummy"})
|
|
912
|
+
|
|
913
|
+
# Test prefix filtering
|
|
914
|
+
prefix_result = store.list_namespaces(prefix=(test_pref, "test"))
|
|
915
|
+
assert len(prefix_result) == 4
|
|
916
|
+
assert all(ns[1] == "test" for ns in prefix_result)
|
|
917
|
+
|
|
918
|
+
# Test specific prefix
|
|
919
|
+
specific_prefix_result = store.list_namespaces(
|
|
920
|
+
prefix=(test_pref, "test", "documents")
|
|
921
|
+
)
|
|
922
|
+
assert len(specific_prefix_result) == 2
|
|
923
|
+
assert all(ns[1:3] == ("test", "documents") for ns in specific_prefix_result)
|
|
924
|
+
|
|
925
|
+
# Test suffix filtering
|
|
926
|
+
suffix_result = store.list_namespaces(suffix=("public", test_pref))
|
|
927
|
+
assert len(suffix_result) == 4
|
|
928
|
+
assert all(ns[-2] == "public" for ns in suffix_result)
|
|
929
|
+
|
|
930
|
+
# Test combined prefix and suffix
|
|
931
|
+
prefix_suffix_result = store.list_namespaces(
|
|
932
|
+
prefix=(test_pref, "test"), suffix=("public", test_pref)
|
|
933
|
+
)
|
|
934
|
+
assert len(prefix_suffix_result) == 2
|
|
935
|
+
assert all(
|
|
936
|
+
ns[1] == "test" and ns[-2] == "public" for ns in prefix_suffix_result
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
# Test wildcard in prefix
|
|
940
|
+
wildcard_prefix_result = store.list_namespaces(
|
|
941
|
+
prefix=(test_pref, "*", "documents")
|
|
942
|
+
)
|
|
943
|
+
assert len(wildcard_prefix_result) == 5
|
|
944
|
+
assert all(ns[2] == "documents" for ns in wildcard_prefix_result)
|
|
945
|
+
|
|
946
|
+
# Test wildcard in suffix
|
|
947
|
+
wildcard_suffix_result = store.list_namespaces(
|
|
948
|
+
suffix=("*", "public", test_pref)
|
|
949
|
+
)
|
|
950
|
+
assert len(wildcard_suffix_result) == 4
|
|
951
|
+
assert all(ns[-2] == "public" for ns in wildcard_suffix_result)
|
|
952
|
+
|
|
953
|
+
wildcard_single = store.list_namespaces(
|
|
954
|
+
suffix=("some", "*", "public", test_pref)
|
|
955
|
+
)
|
|
956
|
+
assert len(wildcard_single) == 1
|
|
957
|
+
assert wildcard_single[0] == (
|
|
958
|
+
test_pref,
|
|
959
|
+
"prod",
|
|
960
|
+
"documents",
|
|
961
|
+
"some",
|
|
962
|
+
"nesting",
|
|
963
|
+
"public",
|
|
964
|
+
test_pref,
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
# Test max depth
|
|
968
|
+
max_depth_result = store.list_namespaces(max_depth=3)
|
|
969
|
+
assert all(len(ns) <= 3 for ns in max_depth_result)
|
|
970
|
+
|
|
971
|
+
max_depth_result = store.list_namespaces(
|
|
972
|
+
max_depth=4, prefix=(test_pref, "*", "documents")
|
|
973
|
+
)
|
|
974
|
+
assert len(set(res for res in max_depth_result)) == len(max_depth_result) == 5
|
|
975
|
+
|
|
976
|
+
# Test pagination
|
|
977
|
+
limit_result = store.list_namespaces(prefix=(test_pref,), limit=3)
|
|
978
|
+
assert len(limit_result) == 3
|
|
979
|
+
|
|
980
|
+
offset_result = store.list_namespaces(prefix=(test_pref,), offset=3)
|
|
981
|
+
assert len(offset_result) == len(test_namespaces) - 3
|
|
982
|
+
|
|
983
|
+
empty_prefix_result = store.list_namespaces(prefix=(test_pref,))
|
|
984
|
+
assert len(empty_prefix_result) == len(test_namespaces)
|
|
985
|
+
assert set(empty_prefix_result) == set(test_namespaces)
|
|
986
|
+
|
|
987
|
+
# Clean up
|
|
988
|
+
for namespace in test_namespaces:
|
|
989
|
+
store.delete(namespace, "dummy")
|
|
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
|
{langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/tests/test_aiosqlite.py
RENAMED
|
File without changes
|
{langgraph_checkpoint_sqlite-2.0.8 → langgraph_checkpoint_sqlite-2.0.9}/tests/test_sqlite.py
RENAMED
|
File without changes
|
|
File without changes
|