kitedb 0.2.6__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
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.
- kitedb/__init__.py +273 -0
- kitedb/_kitedb.cpython-312-aarch64-linux-gnu.so +0 -0
- kitedb/_kitedb.pyi +677 -0
- kitedb/builders.py +901 -0
- kitedb/fluent.py +850 -0
- kitedb/schema.py +327 -0
- kitedb/traversal.py +1523 -0
- kitedb/vector_index.py +472 -0
- kitedb-0.2.6.dist-info/METADATA +216 -0
- kitedb-0.2.6.dist-info/RECORD +12 -0
- kitedb-0.2.6.dist-info/WHEEL +5 -0
- kitedb-0.2.6.dist-info/licenses/LICENSE +21 -0
kitedb/fluent.py
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ray Database - Fluent API
|
|
3
|
+
|
|
4
|
+
High-level, type-safe API for KiteDB matching the TypeScript fluent style.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from kitedb import ray, node, edge, prop, optional
|
|
8
|
+
>>>
|
|
9
|
+
>>> # Define schema
|
|
10
|
+
>>> user = node("user",
|
|
11
|
+
... key=lambda id: f"user:{id}",
|
|
12
|
+
... props={
|
|
13
|
+
... "name": prop.string("name"),
|
|
14
|
+
... "email": prop.string("email"),
|
|
15
|
+
... "age": optional(prop.int("age")),
|
|
16
|
+
... }
|
|
17
|
+
... )
|
|
18
|
+
>>>
|
|
19
|
+
>>> knows = edge("knows", {
|
|
20
|
+
... "since": prop.int("since"),
|
|
21
|
+
... })
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Open database
|
|
24
|
+
>>> db = ray("./my-graph", nodes=[user], edges=[knows])
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Insert nodes with fluent API
|
|
27
|
+
>>> alice = db.insert(user).values(
|
|
28
|
+
... key="alice",
|
|
29
|
+
... name="Alice",
|
|
30
|
+
... email="alice@example.com",
|
|
31
|
+
... age=30
|
|
32
|
+
... ).returning()
|
|
33
|
+
>>>
|
|
34
|
+
>>> bob = db.insert(user).values(
|
|
35
|
+
... key="bob",
|
|
36
|
+
... name="Bob",
|
|
37
|
+
... email="bob@example.com",
|
|
38
|
+
... age=25
|
|
39
|
+
... ).returning()
|
|
40
|
+
>>>
|
|
41
|
+
>>> # Create edges
|
|
42
|
+
>>> db.link(alice, knows, bob, since=2020)
|
|
43
|
+
>>>
|
|
44
|
+
>>> # Traverse graph
|
|
45
|
+
>>> friends = db.from_(alice).out(knows).nodes().to_list()
|
|
46
|
+
>>>
|
|
47
|
+
>>> # Cleanup
|
|
48
|
+
>>> db.close()
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
from contextlib import contextmanager
|
|
54
|
+
from dataclasses import dataclass
|
|
55
|
+
from typing import (
|
|
56
|
+
Any,
|
|
57
|
+
Callable,
|
|
58
|
+
Dict,
|
|
59
|
+
Generator,
|
|
60
|
+
Generic,
|
|
61
|
+
Iterator,
|
|
62
|
+
List,
|
|
63
|
+
Optional,
|
|
64
|
+
TypeVar,
|
|
65
|
+
Union,
|
|
66
|
+
overload,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
from kitedb._kitedb import Database, OpenOptions
|
|
70
|
+
|
|
71
|
+
from .builders import (
|
|
72
|
+
DeleteBuilder,
|
|
73
|
+
InsertBuilder,
|
|
74
|
+
NodeRef,
|
|
75
|
+
UpdateBuilder,
|
|
76
|
+
UpdateByRefBuilder,
|
|
77
|
+
UpdateEdgeBuilder,
|
|
78
|
+
create_link,
|
|
79
|
+
delete_link,
|
|
80
|
+
from_prop_value,
|
|
81
|
+
)
|
|
82
|
+
from .schema import EdgeDef, NodeDef, PropsSchema
|
|
83
|
+
from .traversal import PathFindingBuilder, TraversalBuilder, WeightSpec
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
N = TypeVar("N", bound=NodeDef)
|
|
87
|
+
E = TypeVar("E", bound=EdgeDef)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class EdgeData:
|
|
92
|
+
"""Edge data with source/destination refs and properties."""
|
|
93
|
+
src: NodeRef[Any]
|
|
94
|
+
dst: NodeRef[Any]
|
|
95
|
+
edge: Any
|
|
96
|
+
props: Dict[str, Any]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Ray:
|
|
100
|
+
"""
|
|
101
|
+
Ray Database - High-level fluent API.
|
|
102
|
+
|
|
103
|
+
Provides a type-safe, chainable interface for graph database operations
|
|
104
|
+
similar to the TypeScript API.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> db = ray("./my-graph", nodes=[user, company], edges=[knows, worksAt])
|
|
108
|
+
>>>
|
|
109
|
+
>>> # Insert
|
|
110
|
+
>>> alice = db.insert(user).values(key="alice", name="Alice").returning()
|
|
111
|
+
>>>
|
|
112
|
+
>>> # Update
|
|
113
|
+
>>> db.update(user).set(email="new@example.com").where(key="user:alice").execute()
|
|
114
|
+
>>>
|
|
115
|
+
>>> # Or update by reference
|
|
116
|
+
>>> db.update(alice).set(age=31).execute()
|
|
117
|
+
>>>
|
|
118
|
+
>>> # Link
|
|
119
|
+
>>> db.link(alice, knows, bob, since=2020)
|
|
120
|
+
>>>
|
|
121
|
+
>>> # Traverse
|
|
122
|
+
>>> friends = db.from_(alice).out(knows).nodes().to_list()
|
|
123
|
+
>>>
|
|
124
|
+
>>> # Close
|
|
125
|
+
>>> db.close()
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
path: str,
|
|
131
|
+
*,
|
|
132
|
+
nodes: List[NodeDef[Any]],
|
|
133
|
+
edges: List[EdgeDef],
|
|
134
|
+
options: Optional[OpenOptions] = None,
|
|
135
|
+
):
|
|
136
|
+
"""
|
|
137
|
+
Open or create a Ray database.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
path: Path to the database file
|
|
141
|
+
nodes: List of node definitions
|
|
142
|
+
edges: List of edge definitions
|
|
143
|
+
options: Optional database options
|
|
144
|
+
"""
|
|
145
|
+
self._db = Database(path, options)
|
|
146
|
+
self._nodes: Dict[str, NodeDef[Any]] = {n.name: n for n in nodes}
|
|
147
|
+
self._edges: Dict[str, EdgeDef] = {e.name: e for e in edges}
|
|
148
|
+
self._etype_ids: Dict[EdgeDef, int] = {}
|
|
149
|
+
self._prop_key_ids: Dict[str, int] = {}
|
|
150
|
+
|
|
151
|
+
# Build key prefix -> NodeDef cache for fast lookups
|
|
152
|
+
self._key_prefix_to_node_def: Dict[str, NodeDef[Any]] = {}
|
|
153
|
+
for node_def in nodes:
|
|
154
|
+
try:
|
|
155
|
+
test_key = node_def.key_fn("__test__")
|
|
156
|
+
prefix = test_key.replace("__test__", "")
|
|
157
|
+
self._key_prefix_to_node_def[prefix] = node_def
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
# Initialize schema
|
|
162
|
+
self._init_schema(nodes, edges)
|
|
163
|
+
|
|
164
|
+
def _init_schema(
|
|
165
|
+
self,
|
|
166
|
+
nodes: List[NodeDef[Any]],
|
|
167
|
+
edges: List[EdgeDef],
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Initialize edge types and property keys."""
|
|
170
|
+
self._db.begin()
|
|
171
|
+
try:
|
|
172
|
+
# Define edge types
|
|
173
|
+
for edge in edges:
|
|
174
|
+
etype_id = self._db.get_or_create_etype(edge.name)
|
|
175
|
+
self._etype_ids[edge] = etype_id
|
|
176
|
+
edge._etype_id = etype_id
|
|
177
|
+
|
|
178
|
+
# Define property keys for nodes
|
|
179
|
+
for node in nodes:
|
|
180
|
+
node._prop_key_ids = {}
|
|
181
|
+
for prop_name, prop_def in node.props.items():
|
|
182
|
+
key = f"{node.name}:{prop_def.name}"
|
|
183
|
+
if key not in self._prop_key_ids:
|
|
184
|
+
prop_key_id = self._db.get_or_create_propkey(prop_def.name)
|
|
185
|
+
self._prop_key_ids[key] = prop_key_id
|
|
186
|
+
node._prop_key_ids[prop_name] = self._prop_key_ids[key]
|
|
187
|
+
|
|
188
|
+
# Define property keys for edges
|
|
189
|
+
for edge in edges:
|
|
190
|
+
edge._prop_key_ids = {}
|
|
191
|
+
for prop_name, prop_def in edge.props.items():
|
|
192
|
+
key = f"{edge.name}:{prop_def.name}"
|
|
193
|
+
if key not in self._prop_key_ids:
|
|
194
|
+
prop_key_id = self._db.get_or_create_propkey(prop_def.name)
|
|
195
|
+
self._prop_key_ids[key] = prop_key_id
|
|
196
|
+
edge._prop_key_ids[prop_name] = self._prop_key_ids[key]
|
|
197
|
+
|
|
198
|
+
self._db.commit()
|
|
199
|
+
except Exception:
|
|
200
|
+
self._db.rollback()
|
|
201
|
+
raise
|
|
202
|
+
|
|
203
|
+
# ==========================================================================
|
|
204
|
+
# Schema Resolution Helpers
|
|
205
|
+
# ==========================================================================
|
|
206
|
+
|
|
207
|
+
def _resolve_etype_id(self, edge_def: EdgeDef) -> int:
|
|
208
|
+
"""Resolve edge type ID from definition."""
|
|
209
|
+
etype_id = self._etype_ids.get(edge_def)
|
|
210
|
+
if etype_id is None:
|
|
211
|
+
raise ValueError(f"Unknown edge type: {edge_def.name}")
|
|
212
|
+
return etype_id
|
|
213
|
+
|
|
214
|
+
def _resolve_prop_key_id(
|
|
215
|
+
self,
|
|
216
|
+
def_: Union[NodeDef[Any], EdgeDef],
|
|
217
|
+
prop_name: str,
|
|
218
|
+
) -> int:
|
|
219
|
+
"""Resolve property key ID from definition."""
|
|
220
|
+
prop_key_id = def_._prop_key_ids.get(prop_name)
|
|
221
|
+
if prop_key_id is None:
|
|
222
|
+
raise ValueError(f"Unknown property: {prop_name} on {def_.name}")
|
|
223
|
+
return prop_key_id
|
|
224
|
+
|
|
225
|
+
def _get_node_def(self, node_id: int) -> Optional[NodeDef[Any]]:
|
|
226
|
+
"""Get node definition from node ID by matching key prefix."""
|
|
227
|
+
key = self._db.get_node_key(node_id)
|
|
228
|
+
if key:
|
|
229
|
+
for prefix, node_def in self._key_prefix_to_node_def.items():
|
|
230
|
+
if key.startswith(prefix):
|
|
231
|
+
return node_def
|
|
232
|
+
|
|
233
|
+
# Fall back to first node def
|
|
234
|
+
if self._nodes:
|
|
235
|
+
return next(iter(self._nodes.values()))
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def _load_node_props(self, node_id: int, node_def: NodeDef[Any]) -> Dict[str, Any]:
|
|
239
|
+
"""Load all properties for a node using single FFI call."""
|
|
240
|
+
props: Dict[str, Any] = {}
|
|
241
|
+
# Use get_node_props() for single FFI call instead of per-property calls
|
|
242
|
+
all_props = self._db.get_node_props(node_id)
|
|
243
|
+
if all_props is None:
|
|
244
|
+
return props
|
|
245
|
+
|
|
246
|
+
# Build reverse mapping: prop_key_id -> prop_name
|
|
247
|
+
# This is cached on node_def._prop_key_ids
|
|
248
|
+
key_id_to_name = {v: k for k, v in node_def._prop_key_ids.items()}
|
|
249
|
+
|
|
250
|
+
for node_prop in all_props:
|
|
251
|
+
prop_name = key_id_to_name.get(node_prop.key_id)
|
|
252
|
+
if prop_name is not None:
|
|
253
|
+
props[prop_name] = from_prop_value(node_prop.value)
|
|
254
|
+
|
|
255
|
+
return props
|
|
256
|
+
|
|
257
|
+
# ==========================================================================
|
|
258
|
+
# Node Operations
|
|
259
|
+
# ==========================================================================
|
|
260
|
+
|
|
261
|
+
def insert(self, node: NodeDef[Any]) -> InsertBuilder[NodeDef[Any]]:
|
|
262
|
+
"""
|
|
263
|
+
Insert a new node.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
node: Node definition
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
InsertBuilder for chaining
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
>>> alice = db.insert(user).values(
|
|
273
|
+
... key="alice",
|
|
274
|
+
... name="Alice",
|
|
275
|
+
... email="alice@example.com"
|
|
276
|
+
... ).returning()
|
|
277
|
+
"""
|
|
278
|
+
return InsertBuilder(
|
|
279
|
+
db=self._db,
|
|
280
|
+
node_def=node,
|
|
281
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@overload
|
|
285
|
+
def update(self, node_or_ref: NodeDef[Any]) -> UpdateBuilder[NodeDef[Any]]: ...
|
|
286
|
+
|
|
287
|
+
@overload
|
|
288
|
+
def update(self, node_or_ref: NodeRef[Any]) -> UpdateByRefBuilder: ...
|
|
289
|
+
|
|
290
|
+
def update(
|
|
291
|
+
self,
|
|
292
|
+
node_or_ref: Union[NodeDef[Any], NodeRef[Any]],
|
|
293
|
+
) -> Union[UpdateBuilder[Any], UpdateByRefBuilder]:
|
|
294
|
+
"""
|
|
295
|
+
Update a node by definition or reference.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
node_or_ref: Node definition or node reference
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
UpdateBuilder or UpdateByRefBuilder for chaining
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
>>> # By definition with where clause
|
|
305
|
+
>>> db.update(user).set(email="new@example.com").where(key="user:alice").execute()
|
|
306
|
+
>>>
|
|
307
|
+
>>> # By reference
|
|
308
|
+
>>> db.update(alice).set(age=31).execute()
|
|
309
|
+
"""
|
|
310
|
+
if isinstance(node_or_ref, NodeRef):
|
|
311
|
+
return UpdateByRefBuilder(
|
|
312
|
+
db=self._db,
|
|
313
|
+
node_ref=node_or_ref,
|
|
314
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
315
|
+
)
|
|
316
|
+
return UpdateBuilder(
|
|
317
|
+
db=self._db,
|
|
318
|
+
node_def=node_or_ref,
|
|
319
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
@overload
|
|
323
|
+
def delete(self, node_or_ref: NodeDef[Any]) -> DeleteBuilder[NodeDef[Any]]: ...
|
|
324
|
+
|
|
325
|
+
@overload
|
|
326
|
+
def delete(self, node_or_ref: NodeRef[Any]) -> bool: ...
|
|
327
|
+
|
|
328
|
+
def delete(
|
|
329
|
+
self,
|
|
330
|
+
node_or_ref: Union[NodeDef[Any], NodeRef[Any]],
|
|
331
|
+
) -> Union[DeleteBuilder[Any], bool]:
|
|
332
|
+
"""
|
|
333
|
+
Delete a node by definition or reference.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
node_or_ref: Node definition or node reference
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
DeleteBuilder for chaining, or bool if deleting by reference
|
|
340
|
+
|
|
341
|
+
Example:
|
|
342
|
+
>>> # By definition with where clause
|
|
343
|
+
>>> db.delete(user).where(key="user:bob").execute()
|
|
344
|
+
>>>
|
|
345
|
+
>>> # By reference (immediate execution)
|
|
346
|
+
>>> db.delete(bob)
|
|
347
|
+
"""
|
|
348
|
+
if isinstance(node_or_ref, NodeRef):
|
|
349
|
+
return DeleteBuilder(self._db, node_or_ref.node_def).where(
|
|
350
|
+
id=node_or_ref.id
|
|
351
|
+
).execute()
|
|
352
|
+
return DeleteBuilder(self._db, node_or_ref)
|
|
353
|
+
|
|
354
|
+
def get(
|
|
355
|
+
self,
|
|
356
|
+
node: NodeDef[Any],
|
|
357
|
+
key: Any,
|
|
358
|
+
) -> Optional[NodeRef[Any]]:
|
|
359
|
+
"""
|
|
360
|
+
Get a node by key.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
node: Node definition
|
|
364
|
+
key: Application key (will be transformed by key function)
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
NodeRef with loaded properties, or None if not found
|
|
368
|
+
|
|
369
|
+
Example:
|
|
370
|
+
>>> alice = db.get(user, "alice")
|
|
371
|
+
>>> if alice:
|
|
372
|
+
... print(alice.name, alice.email)
|
|
373
|
+
"""
|
|
374
|
+
full_key = node.key_fn(key)
|
|
375
|
+
node_id = self._db.get_node_by_key(full_key)
|
|
376
|
+
|
|
377
|
+
if node_id is None:
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
props = self._load_node_props(node_id, node)
|
|
381
|
+
return NodeRef(id=node_id, key=full_key, node_def=node, props=props)
|
|
382
|
+
|
|
383
|
+
def get_ref(
|
|
384
|
+
self,
|
|
385
|
+
node: NodeDef[Any],
|
|
386
|
+
key: Any,
|
|
387
|
+
) -> Optional[NodeRef[Any]]:
|
|
388
|
+
"""
|
|
389
|
+
Get a lightweight node reference by key (without loading properties).
|
|
390
|
+
|
|
391
|
+
This is faster than get() when you only need the reference for
|
|
392
|
+
traversals or edge operations.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
node: Node definition
|
|
396
|
+
key: Application key
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
NodeRef without properties, or None if not found
|
|
400
|
+
"""
|
|
401
|
+
full_key = node.key_fn(key)
|
|
402
|
+
node_id = self._db.get_node_by_key(full_key)
|
|
403
|
+
|
|
404
|
+
if node_id is None:
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
return NodeRef(id=node_id, key=full_key, node_def=node, props={})
|
|
408
|
+
|
|
409
|
+
def exists(self, node_ref: NodeRef[Any]) -> bool:
|
|
410
|
+
"""Check if a node exists."""
|
|
411
|
+
return self._db.node_exists(node_ref.id)
|
|
412
|
+
|
|
413
|
+
# ==========================================================================
|
|
414
|
+
# Edge Operations
|
|
415
|
+
# ==========================================================================
|
|
416
|
+
|
|
417
|
+
def link(
|
|
418
|
+
self,
|
|
419
|
+
src: NodeRef[Any],
|
|
420
|
+
edge: EdgeDef,
|
|
421
|
+
dst: NodeRef[Any],
|
|
422
|
+
props: Optional[Dict[str, Any]] = None,
|
|
423
|
+
**kwargs: Any,
|
|
424
|
+
) -> None:
|
|
425
|
+
"""
|
|
426
|
+
Create an edge between two nodes.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
src: Source node reference
|
|
430
|
+
edge: Edge definition
|
|
431
|
+
dst: Destination node reference
|
|
432
|
+
props: Optional edge properties as dict
|
|
433
|
+
**kwargs: Optional edge properties as keyword arguments
|
|
434
|
+
|
|
435
|
+
Example:
|
|
436
|
+
>>> db.link(alice, knows, bob, since=2020)
|
|
437
|
+
>>> # or
|
|
438
|
+
>>> db.link(alice, knows, bob, {"since": 2020})
|
|
439
|
+
"""
|
|
440
|
+
all_props = {**(props or {}), **kwargs}
|
|
441
|
+
create_link(
|
|
442
|
+
db=self._db,
|
|
443
|
+
src=src,
|
|
444
|
+
edge_def=edge,
|
|
445
|
+
dst=dst,
|
|
446
|
+
props=all_props if all_props else None,
|
|
447
|
+
resolve_etype_id=self._resolve_etype_id,
|
|
448
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def unlink(
|
|
452
|
+
self,
|
|
453
|
+
src: NodeRef[Any],
|
|
454
|
+
edge: EdgeDef,
|
|
455
|
+
dst: NodeRef[Any],
|
|
456
|
+
) -> None:
|
|
457
|
+
"""
|
|
458
|
+
Remove an edge between two nodes.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
src: Source node reference
|
|
462
|
+
edge: Edge definition
|
|
463
|
+
dst: Destination node reference
|
|
464
|
+
|
|
465
|
+
Example:
|
|
466
|
+
>>> db.unlink(alice, knows, bob)
|
|
467
|
+
"""
|
|
468
|
+
delete_link(
|
|
469
|
+
db=self._db,
|
|
470
|
+
src=src,
|
|
471
|
+
edge_def=edge,
|
|
472
|
+
dst=dst,
|
|
473
|
+
resolve_etype_id=self._resolve_etype_id,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
def has_edge(
|
|
477
|
+
self,
|
|
478
|
+
src: NodeRef[Any],
|
|
479
|
+
edge: EdgeDef,
|
|
480
|
+
dst: NodeRef[Any],
|
|
481
|
+
) -> bool:
|
|
482
|
+
"""
|
|
483
|
+
Check if an edge exists between two nodes.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
src: Source node reference
|
|
487
|
+
edge: Edge definition
|
|
488
|
+
dst: Destination node reference
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
True if the edge exists
|
|
492
|
+
"""
|
|
493
|
+
etype_id = self._resolve_etype_id(edge)
|
|
494
|
+
return self._db.edge_exists(src.id, etype_id, dst.id)
|
|
495
|
+
|
|
496
|
+
def update_edge(
|
|
497
|
+
self,
|
|
498
|
+
src: NodeRef[Any],
|
|
499
|
+
edge: EdgeDef,
|
|
500
|
+
dst: NodeRef[Any],
|
|
501
|
+
) -> UpdateEdgeBuilder[EdgeDef]:
|
|
502
|
+
"""
|
|
503
|
+
Update edge properties.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
src: Source node reference
|
|
507
|
+
edge: Edge definition
|
|
508
|
+
dst: Destination node reference
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
UpdateEdgeBuilder for chaining
|
|
512
|
+
|
|
513
|
+
Example:
|
|
514
|
+
>>> db.update_edge(alice, knows, bob).set(weight=0.9).execute()
|
|
515
|
+
"""
|
|
516
|
+
return UpdateEdgeBuilder(
|
|
517
|
+
db=self._db,
|
|
518
|
+
src=src,
|
|
519
|
+
edge_def=edge,
|
|
520
|
+
dst=dst,
|
|
521
|
+
resolve_etype_id=self._resolve_etype_id,
|
|
522
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# ==========================================================================
|
|
526
|
+
# Traversal
|
|
527
|
+
# ==========================================================================
|
|
528
|
+
|
|
529
|
+
def from_(self, node: NodeRef[Any]) -> TraversalBuilder[Any]:
|
|
530
|
+
"""
|
|
531
|
+
Start a traversal from a node.
|
|
532
|
+
|
|
533
|
+
Note: Named `from_` because `from` is a Python reserved word.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
node: Starting node reference
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
TraversalBuilder for chaining
|
|
540
|
+
|
|
541
|
+
Example:
|
|
542
|
+
>>> friends = db.from_(alice).out(knows).nodes().to_list()
|
|
543
|
+
>>>
|
|
544
|
+
>>> young_friends = (
|
|
545
|
+
... db.from_(alice)
|
|
546
|
+
... .out(knows)
|
|
547
|
+
... .where_node(lambda n: n.age < 35)
|
|
548
|
+
... .nodes()
|
|
549
|
+
... .to_list()
|
|
550
|
+
... )
|
|
551
|
+
"""
|
|
552
|
+
return TraversalBuilder(
|
|
553
|
+
db=self._db,
|
|
554
|
+
start_nodes=[node],
|
|
555
|
+
resolve_etype_id=self._resolve_etype_id,
|
|
556
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
557
|
+
get_node_def=self._get_node_def,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
def shortest_path(
|
|
561
|
+
self,
|
|
562
|
+
source: NodeRef[Any],
|
|
563
|
+
weight: Optional[WeightSpec] = None,
|
|
564
|
+
) -> PathFindingBuilder[Any]:
|
|
565
|
+
"""
|
|
566
|
+
Start a pathfinding query from a node.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
source: Starting node reference
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
PathFindingBuilder for chaining
|
|
573
|
+
|
|
574
|
+
Example:
|
|
575
|
+
>>> path = db.shortest_path(alice).to(bob).find()
|
|
576
|
+
>>> if path:
|
|
577
|
+
... for node in path.nodes:
|
|
578
|
+
... print(node.key)
|
|
579
|
+
"""
|
|
580
|
+
builder = PathFindingBuilder(
|
|
581
|
+
db=self._db,
|
|
582
|
+
source=source,
|
|
583
|
+
resolve_etype_id=self._resolve_etype_id,
|
|
584
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
585
|
+
get_node_def=self._get_node_def,
|
|
586
|
+
)
|
|
587
|
+
if weight is not None:
|
|
588
|
+
builder.weight(weight)
|
|
589
|
+
return builder
|
|
590
|
+
|
|
591
|
+
# ==========================================================================
|
|
592
|
+
# Listing and Counting
|
|
593
|
+
# ==========================================================================
|
|
594
|
+
|
|
595
|
+
def all(self, node_def: NodeDef[Any]) -> Iterator[NodeRef[Any]]:
|
|
596
|
+
"""
|
|
597
|
+
Iterate all nodes of a specific type.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
node_def: Node definition to filter by
|
|
601
|
+
|
|
602
|
+
Yields:
|
|
603
|
+
NodeRef objects with properties
|
|
604
|
+
|
|
605
|
+
Example:
|
|
606
|
+
>>> for user in db.all(user):
|
|
607
|
+
... print(user.name)
|
|
608
|
+
"""
|
|
609
|
+
# Get key prefix for filtering using Rust prefix-based listing
|
|
610
|
+
try:
|
|
611
|
+
test_key = node_def.key_fn("__test__")
|
|
612
|
+
key_prefix = test_key.replace("__test__", "")
|
|
613
|
+
except Exception:
|
|
614
|
+
key_prefix = ""
|
|
615
|
+
|
|
616
|
+
# Use Rust prefix-based filtering
|
|
617
|
+
for node_id in self._db.list_nodes_with_prefix(key_prefix):
|
|
618
|
+
key = self._db.get_node_key(node_id)
|
|
619
|
+
if key:
|
|
620
|
+
props = self._load_node_props(node_id, node_def)
|
|
621
|
+
yield NodeRef(id=node_id, key=key, node_def=node_def, props=props)
|
|
622
|
+
|
|
623
|
+
def count(self, node_def: Optional[NodeDef[Any]] = None) -> int:
|
|
624
|
+
"""
|
|
625
|
+
Count nodes, optionally filtered by type.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
node_def: Optional node definition to filter by
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
Number of matching nodes
|
|
632
|
+
"""
|
|
633
|
+
if node_def is None:
|
|
634
|
+
return self._db.count_nodes()
|
|
635
|
+
|
|
636
|
+
# Filter by type using Rust prefix-based count
|
|
637
|
+
try:
|
|
638
|
+
test_key = node_def.key_fn("__test__")
|
|
639
|
+
key_prefix = test_key.replace("__test__", "")
|
|
640
|
+
except Exception:
|
|
641
|
+
return 0
|
|
642
|
+
|
|
643
|
+
return self._db.count_nodes_with_prefix(key_prefix)
|
|
644
|
+
|
|
645
|
+
def count_edges(self, edge_def: Optional[EdgeDef] = None) -> int:
|
|
646
|
+
"""
|
|
647
|
+
Count edges, optionally filtered by type.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
edge_def: Optional edge definition to filter by
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
Number of matching edges
|
|
654
|
+
"""
|
|
655
|
+
if edge_def is None:
|
|
656
|
+
return self._db.count_edges()
|
|
657
|
+
|
|
658
|
+
etype_id = self._resolve_etype_id(edge_def)
|
|
659
|
+
return self._db.count_edges_by_type(etype_id)
|
|
660
|
+
|
|
661
|
+
def all_edges(self, edge_def: Optional[EdgeDef] = None) -> Iterator[EdgeData]:
|
|
662
|
+
"""
|
|
663
|
+
Iterate all edges, optionally filtered by type.
|
|
664
|
+
|
|
665
|
+
Yields:
|
|
666
|
+
EdgeData objects with src/dst refs and edge properties
|
|
667
|
+
"""
|
|
668
|
+
etype_id: Optional[int] = None
|
|
669
|
+
if edge_def is not None:
|
|
670
|
+
etype_id = self._resolve_etype_id(edge_def)
|
|
671
|
+
|
|
672
|
+
edges = self._db.list_edges(etype_id)
|
|
673
|
+
for edge in edges:
|
|
674
|
+
src_def = self._get_node_def(edge.src)
|
|
675
|
+
dst_def = self._get_node_def(edge.dst)
|
|
676
|
+
|
|
677
|
+
src_key = self._db.get_node_key(edge.src) or f"node:{edge.src}"
|
|
678
|
+
dst_key = self._db.get_node_key(edge.dst) or f"node:{edge.dst}"
|
|
679
|
+
|
|
680
|
+
if src_def is None or dst_def is None:
|
|
681
|
+
continue
|
|
682
|
+
|
|
683
|
+
src_ref = NodeRef(id=edge.src, key=src_key, node_def=src_def, props={})
|
|
684
|
+
dst_ref = NodeRef(id=edge.dst, key=dst_key, node_def=dst_def, props={})
|
|
685
|
+
|
|
686
|
+
props: Dict[str, Any] = {}
|
|
687
|
+
if edge_def is not None and edge_def.props:
|
|
688
|
+
for prop_name in edge_def.props.keys():
|
|
689
|
+
prop_key_id = self._resolve_prop_key_id(edge_def, prop_name)
|
|
690
|
+
prop_value = self._db.get_edge_prop(edge.src, edge.etype, edge.dst, prop_key_id)
|
|
691
|
+
if prop_value is not None:
|
|
692
|
+
props[prop_name] = from_prop_value(prop_value)
|
|
693
|
+
|
|
694
|
+
yield EdgeData(src=src_ref, dst=dst_ref, edge=edge, props=props)
|
|
695
|
+
|
|
696
|
+
# ==========================================================================
|
|
697
|
+
# Database Operations
|
|
698
|
+
# ==========================================================================
|
|
699
|
+
|
|
700
|
+
def stats(self) -> Any:
|
|
701
|
+
"""Get database statistics."""
|
|
702
|
+
return self._db.stats()
|
|
703
|
+
|
|
704
|
+
def check(self) -> Any:
|
|
705
|
+
"""Check database integrity."""
|
|
706
|
+
result = self._db.check()
|
|
707
|
+
for edge_name, edge_def in self._edges.items():
|
|
708
|
+
if getattr(edge_def, "_etype_id", None) is None:
|
|
709
|
+
result.warnings.append(
|
|
710
|
+
f"Edge type '{edge_name}' has no assigned etype_id"
|
|
711
|
+
)
|
|
712
|
+
return result
|
|
713
|
+
|
|
714
|
+
def optimize(self) -> None:
|
|
715
|
+
"""Optimize the database."""
|
|
716
|
+
self._db.optimize()
|
|
717
|
+
|
|
718
|
+
def close(self) -> None:
|
|
719
|
+
"""Close the database."""
|
|
720
|
+
self._db.close()
|
|
721
|
+
|
|
722
|
+
@property
|
|
723
|
+
def raw(self) -> Database:
|
|
724
|
+
"""Get the raw database handle (escape hatch)."""
|
|
725
|
+
return self._db
|
|
726
|
+
|
|
727
|
+
# ==========================================================================
|
|
728
|
+
# Transaction Batching
|
|
729
|
+
# ==========================================================================
|
|
730
|
+
|
|
731
|
+
def batch(self, operations: List[Any]) -> List[Any]:
|
|
732
|
+
"""
|
|
733
|
+
Execute multiple operations in a single transaction.
|
|
734
|
+
|
|
735
|
+
Each item can be a callable or an executor with .execute()/.returning().
|
|
736
|
+
"""
|
|
737
|
+
self._db.begin()
|
|
738
|
+
try:
|
|
739
|
+
results: List[Any] = []
|
|
740
|
+
for op in operations:
|
|
741
|
+
if callable(op):
|
|
742
|
+
results.append(op())
|
|
743
|
+
elif hasattr(op, "returning"):
|
|
744
|
+
results.append(op.returning())
|
|
745
|
+
elif hasattr(op, "execute"):
|
|
746
|
+
results.append(op.execute())
|
|
747
|
+
else:
|
|
748
|
+
raise ValueError("Unsupported batch operation")
|
|
749
|
+
self._db.commit()
|
|
750
|
+
return results
|
|
751
|
+
except Exception:
|
|
752
|
+
self._db.rollback()
|
|
753
|
+
raise
|
|
754
|
+
|
|
755
|
+
@contextmanager
|
|
756
|
+
def transaction(self) -> Generator[Ray, None, None]:
|
|
757
|
+
"""
|
|
758
|
+
Context manager for batching multiple operations in a single transaction.
|
|
759
|
+
|
|
760
|
+
This is more efficient than letting each operation auto-commit.
|
|
761
|
+
|
|
762
|
+
Example:
|
|
763
|
+
>>> with db.transaction():
|
|
764
|
+
... alice = db.insert(user).values(key="alice", name="Alice").returning()
|
|
765
|
+
... bob = db.insert(user).values(key="bob", name="Bob").returning()
|
|
766
|
+
... db.link(alice, knows, bob, since=2024)
|
|
767
|
+
... # All operations commit together on exit
|
|
768
|
+
|
|
769
|
+
Note:
|
|
770
|
+
If an exception occurs, the transaction is rolled back.
|
|
771
|
+
"""
|
|
772
|
+
self._db.begin()
|
|
773
|
+
try:
|
|
774
|
+
yield self
|
|
775
|
+
self._db.commit()
|
|
776
|
+
except Exception:
|
|
777
|
+
self._db.rollback()
|
|
778
|
+
raise
|
|
779
|
+
|
|
780
|
+
def in_transaction(self) -> bool:
|
|
781
|
+
"""Check if currently in a transaction."""
|
|
782
|
+
return self._db.has_transaction()
|
|
783
|
+
|
|
784
|
+
# ==========================================================================
|
|
785
|
+
# Context Manager
|
|
786
|
+
# ==========================================================================
|
|
787
|
+
|
|
788
|
+
def __enter__(self) -> Ray:
|
|
789
|
+
return self
|
|
790
|
+
|
|
791
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
|
|
792
|
+
self.close()
|
|
793
|
+
return False
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
# ============================================================================
|
|
797
|
+
# Entry Point
|
|
798
|
+
# ============================================================================
|
|
799
|
+
|
|
800
|
+
def ray(
|
|
801
|
+
path: str,
|
|
802
|
+
*,
|
|
803
|
+
nodes: List[NodeDef[Any]],
|
|
804
|
+
edges: List[EdgeDef],
|
|
805
|
+
options: Optional[OpenOptions] = None,
|
|
806
|
+
) -> Ray:
|
|
807
|
+
"""
|
|
808
|
+
Open or create a Ray database.
|
|
809
|
+
|
|
810
|
+
This is the main entry point for the fluent API.
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
path: Path to the database file
|
|
814
|
+
nodes: List of node definitions
|
|
815
|
+
edges: List of edge definitions
|
|
816
|
+
options: Optional database options
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
Ray database instance
|
|
820
|
+
|
|
821
|
+
Example:
|
|
822
|
+
>>> from kitedb import ray, node, edge, prop, optional
|
|
823
|
+
>>>
|
|
824
|
+
>>> user = node("user",
|
|
825
|
+
... key=lambda id: f"user:{id}",
|
|
826
|
+
... props={
|
|
827
|
+
... "name": prop.string("name"),
|
|
828
|
+
... "email": prop.string("email"),
|
|
829
|
+
... "age": optional(prop.int("age")),
|
|
830
|
+
... }
|
|
831
|
+
... )
|
|
832
|
+
>>>
|
|
833
|
+
>>> knows = edge("knows", {
|
|
834
|
+
... "since": prop.int("since"),
|
|
835
|
+
... })
|
|
836
|
+
>>>
|
|
837
|
+
>>> db = ray("./my-graph", nodes=[user], edges=[knows])
|
|
838
|
+
>>>
|
|
839
|
+
>>> # Use as context manager
|
|
840
|
+
>>> with ray("./my-graph", nodes=[user], edges=[knows]) as db:
|
|
841
|
+
... alice = db.insert(user).values(key="alice", name="Alice").returning()
|
|
842
|
+
"""
|
|
843
|
+
return Ray(path, nodes=nodes, edges=edges, options=options)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
__all__ = [
|
|
847
|
+
"Ray",
|
|
848
|
+
"ray",
|
|
849
|
+
"EdgeData",
|
|
850
|
+
]
|