kitedb 0.2.5__cp313-cp313-win_amd64.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.cp313-win_amd64.pyd +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.5.dist-info/METADATA +217 -0
- kitedb-0.2.5.dist-info/RECORD +12 -0
- kitedb-0.2.5.dist-info/WHEEL +4 -0
- kitedb-0.2.5.dist-info/licenses/LICENSE +21 -0
kitedb/builders.py
ADDED
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query Builders for KiteDB
|
|
3
|
+
|
|
4
|
+
Fluent builders for insert, update, delete, and edge operations.
|
|
5
|
+
These provide a type-safe, chainable API for database operations.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> # Insert with returning
|
|
9
|
+
>>> alice = db.insert(user).values(
|
|
10
|
+
... key="alice",
|
|
11
|
+
... name="Alice",
|
|
12
|
+
... email="alice@example.com"
|
|
13
|
+
... ).returning()
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Update by key
|
|
16
|
+
>>> db.update(user).set(email="new@example.com").where(key="user:alice").execute()
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Create edge with properties
|
|
19
|
+
>>> db.link(alice, knows, bob, since=2020)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import (
|
|
26
|
+
TYPE_CHECKING,
|
|
27
|
+
Any,
|
|
28
|
+
Callable,
|
|
29
|
+
Dict,
|
|
30
|
+
Generic,
|
|
31
|
+
List,
|
|
32
|
+
Optional,
|
|
33
|
+
Protocol,
|
|
34
|
+
TypeVar,
|
|
35
|
+
Union,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .schema import EdgeDef, NodeDef, PropDef
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from kitedb._kitedb import Database, PropValue
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ============================================================================
|
|
45
|
+
# Node Reference
|
|
46
|
+
# ============================================================================
|
|
47
|
+
|
|
48
|
+
class NodeRef(Generic[TypeVar("N", bound=NodeDef)]):
|
|
49
|
+
"""
|
|
50
|
+
A reference to a node in the database.
|
|
51
|
+
|
|
52
|
+
Contains the node's internal ID, key, and properties.
|
|
53
|
+
Can be used for updates, edge operations, and traversals.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
id: Internal node ID
|
|
57
|
+
key: Full node key (e.g., "user:alice")
|
|
58
|
+
node_def: The node definition this reference belongs to
|
|
59
|
+
props: Dictionary of property values
|
|
60
|
+
"""
|
|
61
|
+
__slots__ = ('id', 'key', 'node_def', 'props')
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
id: int,
|
|
66
|
+
key: str,
|
|
67
|
+
node_def: NodeDef[Any],
|
|
68
|
+
props: Optional[Dict[str, Any]] = None,
|
|
69
|
+
):
|
|
70
|
+
self.id = id
|
|
71
|
+
self.key = key
|
|
72
|
+
self.node_def = node_def
|
|
73
|
+
self.props = props if props is not None else {}
|
|
74
|
+
|
|
75
|
+
def __getattr__(self, name: str) -> Any:
|
|
76
|
+
"""Allow attribute-style access to properties."""
|
|
77
|
+
# __slots__ classes don't have __dict__, so we check props directly
|
|
78
|
+
props = object.__getattribute__(self, 'props')
|
|
79
|
+
if name in props:
|
|
80
|
+
return props[name]
|
|
81
|
+
# Check if it's a valid property in the schema
|
|
82
|
+
node_def = object.__getattribute__(self, 'node_def')
|
|
83
|
+
if name in node_def.props:
|
|
84
|
+
return None # Property exists but wasn't set
|
|
85
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
86
|
+
|
|
87
|
+
def __repr__(self) -> str:
|
|
88
|
+
props_str = ", ".join(f"{k}={v!r}" for k, v in self.props.items())
|
|
89
|
+
return f"NodeRef(id={self.id}, key={self.key!r}, {props_str})"
|
|
90
|
+
|
|
91
|
+
def __eq__(self, other: object) -> bool:
|
|
92
|
+
if isinstance(other, NodeRef):
|
|
93
|
+
return self.id == other.id
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def __hash__(self) -> int:
|
|
97
|
+
return hash(self.id)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
N = TypeVar("N", bound=NodeDef)
|
|
101
|
+
E = TypeVar("E", bound=EdgeDef)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# PropValue Conversion
|
|
106
|
+
# ============================================================================
|
|
107
|
+
|
|
108
|
+
def to_prop_value(prop_def: PropDef[Any], value: Any, PropValue: type) -> PropValue:
|
|
109
|
+
"""Convert a Python value to a PropValue based on the property definition."""
|
|
110
|
+
if value is None:
|
|
111
|
+
return PropValue.null()
|
|
112
|
+
|
|
113
|
+
if prop_def.type == "string":
|
|
114
|
+
return PropValue.string(str(value))
|
|
115
|
+
elif prop_def.type == "int":
|
|
116
|
+
return PropValue.int(int(value))
|
|
117
|
+
elif prop_def.type == "float":
|
|
118
|
+
return PropValue.float(float(value))
|
|
119
|
+
elif prop_def.type == "bool":
|
|
120
|
+
return PropValue.bool(bool(value))
|
|
121
|
+
elif prop_def.type == "vector":
|
|
122
|
+
return PropValue.vector([float(v) for v in value])
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError(f"Unknown property type: {prop_def.type}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def from_prop_value(pv: PropValue) -> Any:
|
|
128
|
+
"""Convert a PropValue to a Python value."""
|
|
129
|
+
return pv.value()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ============================================================================
|
|
133
|
+
# Insert Builder
|
|
134
|
+
# ============================================================================
|
|
135
|
+
|
|
136
|
+
class InsertExecutor(Generic[N]):
|
|
137
|
+
"""
|
|
138
|
+
Executor for insert operations.
|
|
139
|
+
|
|
140
|
+
Can either return the created node(s) or execute without returning.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
db: Database,
|
|
146
|
+
node_def: N,
|
|
147
|
+
data: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
148
|
+
resolve_prop_key_id: Callable[[NodeDef, str], int],
|
|
149
|
+
use_batch: bool = False,
|
|
150
|
+
):
|
|
151
|
+
self._db = db
|
|
152
|
+
self._node_def = node_def
|
|
153
|
+
self._data = data if isinstance(data, list) else [data]
|
|
154
|
+
self._is_single = not isinstance(data, list)
|
|
155
|
+
self._resolve_prop_key_id = resolve_prop_key_id
|
|
156
|
+
self._use_batch = use_batch
|
|
157
|
+
|
|
158
|
+
def returning(self) -> Union[NodeRef[N], List[NodeRef[N]]]:
|
|
159
|
+
"""Execute insert and return the created node(s)."""
|
|
160
|
+
from kitedb._kitedb import PropValue
|
|
161
|
+
|
|
162
|
+
# For batch inserts with many items, use Rust batch API
|
|
163
|
+
if self._use_batch and len(self._data) > 1:
|
|
164
|
+
return self._returning_batch()
|
|
165
|
+
|
|
166
|
+
results: List[NodeRef[N]] = []
|
|
167
|
+
|
|
168
|
+
# Check if we're already in a transaction
|
|
169
|
+
in_tx = self._db.has_transaction()
|
|
170
|
+
if not in_tx:
|
|
171
|
+
self._db.begin()
|
|
172
|
+
try:
|
|
173
|
+
for item in self._data:
|
|
174
|
+
key_arg = item.pop("key", None)
|
|
175
|
+
if key_arg is None:
|
|
176
|
+
raise ValueError("Insert requires a 'key' field")
|
|
177
|
+
|
|
178
|
+
full_key = self._node_def.key_fn(key_arg)
|
|
179
|
+
|
|
180
|
+
# Create the node
|
|
181
|
+
node_id = self._db.create_node(full_key)
|
|
182
|
+
|
|
183
|
+
# Set properties
|
|
184
|
+
for prop_name, value in item.items():
|
|
185
|
+
if value is None:
|
|
186
|
+
continue
|
|
187
|
+
prop_def = self._node_def.props.get(prop_name)
|
|
188
|
+
if prop_def is None:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
prop_key_id = self._resolve_prop_key_id(self._node_def, prop_name)
|
|
192
|
+
prop_value = to_prop_value(prop_def, value, PropValue)
|
|
193
|
+
self._db.set_node_prop(node_id, prop_key_id, prop_value)
|
|
194
|
+
|
|
195
|
+
results.append(NodeRef(
|
|
196
|
+
id=node_id,
|
|
197
|
+
key=full_key,
|
|
198
|
+
node_def=self._node_def,
|
|
199
|
+
props=item,
|
|
200
|
+
))
|
|
201
|
+
|
|
202
|
+
if not in_tx:
|
|
203
|
+
self._db.commit()
|
|
204
|
+
except Exception:
|
|
205
|
+
if not in_tx:
|
|
206
|
+
self._db.rollback()
|
|
207
|
+
raise
|
|
208
|
+
|
|
209
|
+
return results[0] if self._is_single else results
|
|
210
|
+
|
|
211
|
+
def _returning_batch(self) -> Union[NodeRef[N], List[NodeRef[N]]]:
|
|
212
|
+
"""Execute batch insert using Rust batch API."""
|
|
213
|
+
from kitedb._kitedb import PropValue
|
|
214
|
+
|
|
215
|
+
# Prepare batch data: list of (key, [(prop_key_id, PropValue)])
|
|
216
|
+
batch_nodes = []
|
|
217
|
+
items_for_results = []
|
|
218
|
+
|
|
219
|
+
for item in self._data:
|
|
220
|
+
item_copy = dict(item) # Copy to preserve for results
|
|
221
|
+
key_arg = item_copy.pop("key", None)
|
|
222
|
+
if key_arg is None:
|
|
223
|
+
raise ValueError("Insert requires a 'key' field")
|
|
224
|
+
|
|
225
|
+
full_key = self._node_def.key_fn(key_arg)
|
|
226
|
+
|
|
227
|
+
# Build props list
|
|
228
|
+
props_list = []
|
|
229
|
+
for prop_name, value in item_copy.items():
|
|
230
|
+
if value is None:
|
|
231
|
+
continue
|
|
232
|
+
prop_def = self._node_def.props.get(prop_name)
|
|
233
|
+
if prop_def is None:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
prop_key_id = self._resolve_prop_key_id(self._node_def, prop_name)
|
|
237
|
+
prop_value = to_prop_value(prop_def, value, PropValue)
|
|
238
|
+
props_list.append((prop_key_id, prop_value))
|
|
239
|
+
|
|
240
|
+
batch_nodes.append((full_key, props_list))
|
|
241
|
+
items_for_results.append((full_key, item_copy))
|
|
242
|
+
|
|
243
|
+
# Execute batch insert in Rust
|
|
244
|
+
node_ids = self._db.batch_create_nodes(batch_nodes)
|
|
245
|
+
|
|
246
|
+
# Build results
|
|
247
|
+
results = []
|
|
248
|
+
for node_id, (full_key, item) in zip(node_ids, items_for_results):
|
|
249
|
+
results.append(NodeRef(
|
|
250
|
+
id=node_id,
|
|
251
|
+
key=full_key,
|
|
252
|
+
node_def=self._node_def,
|
|
253
|
+
props=item,
|
|
254
|
+
))
|
|
255
|
+
|
|
256
|
+
return results[0] if self._is_single else results
|
|
257
|
+
|
|
258
|
+
def execute(self) -> None:
|
|
259
|
+
"""Execute insert without returning."""
|
|
260
|
+
self.returning() # Just discard the result
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class InsertBuilder(Generic[N]):
|
|
264
|
+
"""
|
|
265
|
+
Builder for insert operations.
|
|
266
|
+
|
|
267
|
+
Example:
|
|
268
|
+
>>> alice = db.insert(user).values(
|
|
269
|
+
... key="alice",
|
|
270
|
+
... name="Alice",
|
|
271
|
+
... email="alice@example.com"
|
|
272
|
+
... ).returning()
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(
|
|
276
|
+
self,
|
|
277
|
+
db: Database,
|
|
278
|
+
node_def: N,
|
|
279
|
+
resolve_prop_key_id: Callable[[NodeDef, str], int],
|
|
280
|
+
):
|
|
281
|
+
self._db = db
|
|
282
|
+
self._node_def = node_def
|
|
283
|
+
self._resolve_prop_key_id = resolve_prop_key_id
|
|
284
|
+
|
|
285
|
+
def values(
|
|
286
|
+
self,
|
|
287
|
+
data: Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] = None,
|
|
288
|
+
**kwargs: Any,
|
|
289
|
+
) -> InsertExecutor[N]:
|
|
290
|
+
"""
|
|
291
|
+
Set the values to insert.
|
|
292
|
+
|
|
293
|
+
Can be called with a dict, a list of dicts, or with keyword arguments.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
data: Dictionary of property values (including 'key'),
|
|
297
|
+
or a list of dictionaries
|
|
298
|
+
**kwargs: Alternative way to pass property values
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
InsertExecutor for executing the insert
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
>>> # Using dict
|
|
305
|
+
>>> db.insert(user).values({"key": "alice", "name": "Alice"})
|
|
306
|
+
>>>
|
|
307
|
+
>>> # Using kwargs
|
|
308
|
+
>>> db.insert(user).values(key="alice", name="Alice")
|
|
309
|
+
>>>
|
|
310
|
+
>>> # Using list
|
|
311
|
+
>>> db.insert(user).values([
|
|
312
|
+
... {"key": "alice", "name": "Alice"},
|
|
313
|
+
... {"key": "bob", "name": "Bob"},
|
|
314
|
+
... ])
|
|
315
|
+
"""
|
|
316
|
+
# Avoid unnecessary dict copy
|
|
317
|
+
if data is None:
|
|
318
|
+
data = kwargs
|
|
319
|
+
elif isinstance(data, list):
|
|
320
|
+
if kwargs:
|
|
321
|
+
raise ValueError("Cannot combine list data with keyword arguments")
|
|
322
|
+
return InsertExecutor(
|
|
323
|
+
db=self._db,
|
|
324
|
+
node_def=self._node_def,
|
|
325
|
+
data=data,
|
|
326
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
327
|
+
use_batch=True,
|
|
328
|
+
)
|
|
329
|
+
elif kwargs:
|
|
330
|
+
data = {**data, **kwargs}
|
|
331
|
+
# else: use data as-is
|
|
332
|
+
|
|
333
|
+
return InsertExecutor(
|
|
334
|
+
db=self._db,
|
|
335
|
+
node_def=self._node_def,
|
|
336
|
+
data=data,
|
|
337
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def values_many(self, data: List[Dict[str, Any]], *, batch: bool = True) -> InsertExecutor[N]:
|
|
341
|
+
"""
|
|
342
|
+
Insert multiple nodes at once.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
data: List of dictionaries with property values
|
|
346
|
+
batch: Use Rust batch API for better performance (default True)
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
InsertExecutor for executing the batch insert
|
|
350
|
+
|
|
351
|
+
Example:
|
|
352
|
+
>>> users = db.insert(user).values_many([
|
|
353
|
+
... {"key": "alice", "name": "Alice"},
|
|
354
|
+
... {"key": "bob", "name": "Bob"},
|
|
355
|
+
... {"key": "carol", "name": "Carol"},
|
|
356
|
+
... ]).returning()
|
|
357
|
+
"""
|
|
358
|
+
return InsertExecutor(
|
|
359
|
+
db=self._db,
|
|
360
|
+
node_def=self._node_def,
|
|
361
|
+
data=data,
|
|
362
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
363
|
+
use_batch=batch,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ============================================================================
|
|
368
|
+
# Update Builder
|
|
369
|
+
# ============================================================================
|
|
370
|
+
|
|
371
|
+
class UpdateExecutor(Generic[N]):
|
|
372
|
+
"""Executor for update operations."""
|
|
373
|
+
|
|
374
|
+
def __init__(
|
|
375
|
+
self,
|
|
376
|
+
db: Database,
|
|
377
|
+
node_def: N,
|
|
378
|
+
data: Dict[str, Any],
|
|
379
|
+
resolve_prop_key_id: Callable[[NodeDef, str], int],
|
|
380
|
+
):
|
|
381
|
+
self._db = db
|
|
382
|
+
self._node_def = node_def
|
|
383
|
+
self._data = data
|
|
384
|
+
self._resolve_prop_key_id = resolve_prop_key_id
|
|
385
|
+
self._where_id: Optional[int] = None
|
|
386
|
+
self._where_key: Optional[str] = None
|
|
387
|
+
|
|
388
|
+
def where(
|
|
389
|
+
self,
|
|
390
|
+
*,
|
|
391
|
+
id: Optional[int] = None,
|
|
392
|
+
key: Optional[str] = None,
|
|
393
|
+
) -> UpdateExecutor[N]:
|
|
394
|
+
"""
|
|
395
|
+
Set the condition for which node to update.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
id: Update node by internal ID
|
|
399
|
+
key: Update node by full key (e.g., "user:alice")
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Self for chaining
|
|
403
|
+
"""
|
|
404
|
+
self._where_id = id
|
|
405
|
+
self._where_key = key
|
|
406
|
+
return self
|
|
407
|
+
|
|
408
|
+
def execute(self) -> None:
|
|
409
|
+
"""Execute the update."""
|
|
410
|
+
from kitedb._kitedb import PropValue
|
|
411
|
+
|
|
412
|
+
if self._where_id is None and self._where_key is None:
|
|
413
|
+
raise ValueError("Update requires a where condition (id or key)")
|
|
414
|
+
|
|
415
|
+
# Resolve node ID
|
|
416
|
+
node_id: Optional[int] = self._where_id
|
|
417
|
+
if node_id is None and self._where_key:
|
|
418
|
+
node_id = self._db.get_node_by_key(self._where_key)
|
|
419
|
+
|
|
420
|
+
if node_id is None:
|
|
421
|
+
raise ValueError(f"Node not found: {self._where_key}")
|
|
422
|
+
|
|
423
|
+
resolved_node_id: int = node_id # Now guaranteed non-None
|
|
424
|
+
|
|
425
|
+
# Check if we're already in a transaction
|
|
426
|
+
in_tx = self._db.has_transaction()
|
|
427
|
+
if not in_tx:
|
|
428
|
+
self._db.begin()
|
|
429
|
+
try:
|
|
430
|
+
for prop_name, value in self._data.items():
|
|
431
|
+
prop_def = self._node_def.props.get(prop_name)
|
|
432
|
+
if prop_def is None:
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
prop_key_id = self._resolve_prop_key_id(self._node_def, prop_name)
|
|
436
|
+
|
|
437
|
+
if value is None:
|
|
438
|
+
self._db.delete_node_prop(resolved_node_id, prop_key_id)
|
|
439
|
+
else:
|
|
440
|
+
prop_value = to_prop_value(prop_def, value, PropValue)
|
|
441
|
+
self._db.set_node_prop(resolved_node_id, prop_key_id, prop_value)
|
|
442
|
+
|
|
443
|
+
if not in_tx:
|
|
444
|
+
self._db.commit()
|
|
445
|
+
except Exception:
|
|
446
|
+
if not in_tx:
|
|
447
|
+
self._db.rollback()
|
|
448
|
+
raise
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class UpdateByRefExecutor:
|
|
452
|
+
"""Executor for updating a node by reference."""
|
|
453
|
+
|
|
454
|
+
def __init__(
|
|
455
|
+
self,
|
|
456
|
+
db: Database,
|
|
457
|
+
node_ref: NodeRef[Any],
|
|
458
|
+
data: Dict[str, Any],
|
|
459
|
+
resolve_prop_key_id: Callable[[NodeDef, str], int],
|
|
460
|
+
):
|
|
461
|
+
self._db = db
|
|
462
|
+
self._node_ref = node_ref
|
|
463
|
+
self._data = data
|
|
464
|
+
self._resolve_prop_key_id = resolve_prop_key_id
|
|
465
|
+
|
|
466
|
+
def execute(self) -> None:
|
|
467
|
+
"""Execute the update."""
|
|
468
|
+
from kitedb._kitedb import PropValue
|
|
469
|
+
|
|
470
|
+
# Check if we're already in a transaction
|
|
471
|
+
in_tx = self._db.has_transaction()
|
|
472
|
+
if not in_tx:
|
|
473
|
+
self._db.begin()
|
|
474
|
+
try:
|
|
475
|
+
for prop_name, value in self._data.items():
|
|
476
|
+
prop_def = self._node_ref.node_def.props.get(prop_name)
|
|
477
|
+
if prop_def is None:
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
prop_key_id = self._resolve_prop_key_id(self._node_ref.node_def, prop_name)
|
|
481
|
+
|
|
482
|
+
if value is None:
|
|
483
|
+
self._db.delete_node_prop(self._node_ref.id, prop_key_id)
|
|
484
|
+
else:
|
|
485
|
+
prop_value = to_prop_value(prop_def, value, PropValue)
|
|
486
|
+
self._db.set_node_prop(self._node_ref.id, prop_key_id, prop_value)
|
|
487
|
+
|
|
488
|
+
if not in_tx:
|
|
489
|
+
self._db.commit()
|
|
490
|
+
except Exception:
|
|
491
|
+
if not in_tx:
|
|
492
|
+
self._db.rollback()
|
|
493
|
+
raise
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class UpdateBuilder(Generic[N]):
|
|
497
|
+
"""
|
|
498
|
+
Builder for update operations by node definition.
|
|
499
|
+
|
|
500
|
+
Example:
|
|
501
|
+
>>> db.update(user).set(email="new@example.com").where(key="user:alice").execute()
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
def __init__(
|
|
505
|
+
self,
|
|
506
|
+
db: Database,
|
|
507
|
+
node_def: N,
|
|
508
|
+
resolve_prop_key_id: Callable[[NodeDef, str], int],
|
|
509
|
+
):
|
|
510
|
+
self._db = db
|
|
511
|
+
self._node_def = node_def
|
|
512
|
+
self._resolve_prop_key_id = resolve_prop_key_id
|
|
513
|
+
|
|
514
|
+
def set(self, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> UpdateExecutor[N]:
|
|
515
|
+
"""
|
|
516
|
+
Set the properties to update.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
data: Dictionary of property values
|
|
520
|
+
**kwargs: Alternative way to pass property values
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
UpdateExecutor for setting where condition and executing
|
|
524
|
+
"""
|
|
525
|
+
# Avoid unnecessary dict copy
|
|
526
|
+
if data is None:
|
|
527
|
+
data = kwargs
|
|
528
|
+
elif kwargs:
|
|
529
|
+
data = {**data, **kwargs}
|
|
530
|
+
# else: use data as-is
|
|
531
|
+
|
|
532
|
+
return UpdateExecutor(
|
|
533
|
+
db=self._db,
|
|
534
|
+
node_def=self._node_def,
|
|
535
|
+
data=data,
|
|
536
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class UpdateByRefBuilder:
|
|
541
|
+
"""
|
|
542
|
+
Builder for update operations by node reference.
|
|
543
|
+
|
|
544
|
+
Example:
|
|
545
|
+
>>> db.update(alice).set(age=31).execute()
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
def __init__(
|
|
549
|
+
self,
|
|
550
|
+
db: Database,
|
|
551
|
+
node_ref: NodeRef[Any],
|
|
552
|
+
resolve_prop_key_id: Callable[[NodeDef, str], int],
|
|
553
|
+
):
|
|
554
|
+
self._db = db
|
|
555
|
+
self._node_ref = node_ref
|
|
556
|
+
self._resolve_prop_key_id = resolve_prop_key_id
|
|
557
|
+
|
|
558
|
+
def set(self, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> UpdateByRefExecutor:
|
|
559
|
+
"""
|
|
560
|
+
Set the properties to update.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
data: Dictionary of property values
|
|
564
|
+
**kwargs: Alternative way to pass property values
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
UpdateByRefExecutor for executing
|
|
568
|
+
"""
|
|
569
|
+
# Avoid unnecessary dict copy
|
|
570
|
+
if data is None:
|
|
571
|
+
data = kwargs
|
|
572
|
+
elif kwargs:
|
|
573
|
+
data = {**data, **kwargs}
|
|
574
|
+
# else: use data as-is
|
|
575
|
+
|
|
576
|
+
return UpdateByRefExecutor(
|
|
577
|
+
db=self._db,
|
|
578
|
+
node_ref=self._node_ref,
|
|
579
|
+
data=data,
|
|
580
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# ============================================================================
|
|
585
|
+
# Delete Builder
|
|
586
|
+
# ============================================================================
|
|
587
|
+
|
|
588
|
+
class DeleteExecutor:
|
|
589
|
+
"""Executor for delete operations."""
|
|
590
|
+
|
|
591
|
+
def __init__(self, db: Database):
|
|
592
|
+
self._db = db
|
|
593
|
+
self._where_id: Optional[int] = None
|
|
594
|
+
self._where_key: Optional[str] = None
|
|
595
|
+
|
|
596
|
+
def where(
|
|
597
|
+
self,
|
|
598
|
+
*,
|
|
599
|
+
id: Optional[int] = None,
|
|
600
|
+
key: Optional[str] = None,
|
|
601
|
+
) -> DeleteExecutor:
|
|
602
|
+
"""
|
|
603
|
+
Set the condition for which node to delete.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
id: Delete node by internal ID
|
|
607
|
+
key: Delete node by full key (e.g., "user:alice")
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
Self for chaining
|
|
611
|
+
"""
|
|
612
|
+
self._where_id = id
|
|
613
|
+
self._where_key = key
|
|
614
|
+
return self
|
|
615
|
+
|
|
616
|
+
def execute(self) -> bool:
|
|
617
|
+
"""
|
|
618
|
+
Execute the delete.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
True if a node was deleted, False otherwise
|
|
622
|
+
"""
|
|
623
|
+
if self._where_id is None and self._where_key is None:
|
|
624
|
+
raise ValueError("Delete requires a where condition (id or key)")
|
|
625
|
+
|
|
626
|
+
# Resolve node ID
|
|
627
|
+
node_id: Optional[int] = self._where_id
|
|
628
|
+
if node_id is None and self._where_key:
|
|
629
|
+
node_id = self._db.get_node_by_key(self._where_key)
|
|
630
|
+
|
|
631
|
+
if node_id is None:
|
|
632
|
+
return False
|
|
633
|
+
|
|
634
|
+
resolved_node_id: int = node_id # Now guaranteed non-None
|
|
635
|
+
|
|
636
|
+
# Check if we're already in a transaction
|
|
637
|
+
in_tx = self._db.has_transaction()
|
|
638
|
+
if not in_tx:
|
|
639
|
+
self._db.begin()
|
|
640
|
+
try:
|
|
641
|
+
self._db.delete_node(resolved_node_id)
|
|
642
|
+
if not in_tx:
|
|
643
|
+
self._db.commit()
|
|
644
|
+
return True
|
|
645
|
+
except Exception:
|
|
646
|
+
if not in_tx:
|
|
647
|
+
self._db.rollback()
|
|
648
|
+
raise
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class DeleteBuilder(Generic[N]):
|
|
652
|
+
"""
|
|
653
|
+
Builder for delete operations.
|
|
654
|
+
|
|
655
|
+
Example:
|
|
656
|
+
>>> db.delete(user).where(key="user:bob").execute()
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
def __init__(self, db: Database, node_def: N):
|
|
660
|
+
self._db = db
|
|
661
|
+
self._node_def = node_def
|
|
662
|
+
|
|
663
|
+
def where(
|
|
664
|
+
self,
|
|
665
|
+
*,
|
|
666
|
+
id: Optional[int] = None,
|
|
667
|
+
key: Optional[str] = None,
|
|
668
|
+
) -> DeleteExecutor:
|
|
669
|
+
"""
|
|
670
|
+
Set the condition for which node to delete.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
id: Delete node by internal ID
|
|
674
|
+
key: Delete node by full key (e.g., "user:alice")
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
DeleteExecutor for executing
|
|
678
|
+
"""
|
|
679
|
+
executor = DeleteExecutor(self._db)
|
|
680
|
+
return executor.where(id=id, key=key)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
# ============================================================================
|
|
684
|
+
# Link Builder (Edge Creation)
|
|
685
|
+
# ============================================================================
|
|
686
|
+
|
|
687
|
+
def create_link(
|
|
688
|
+
db: Database,
|
|
689
|
+
src: NodeRef[Any],
|
|
690
|
+
edge_def: EdgeDef,
|
|
691
|
+
dst: NodeRef[Any],
|
|
692
|
+
props: Optional[Dict[str, Any]],
|
|
693
|
+
resolve_etype_id: Callable[[EdgeDef], int],
|
|
694
|
+
resolve_prop_key_id: Callable[[EdgeDef, str], int],
|
|
695
|
+
) -> None:
|
|
696
|
+
"""
|
|
697
|
+
Create an edge between two nodes.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
db: Database instance
|
|
701
|
+
src: Source node reference
|
|
702
|
+
edge_def: Edge definition
|
|
703
|
+
dst: Destination node reference
|
|
704
|
+
props: Optional edge properties
|
|
705
|
+
resolve_etype_id: Function to resolve edge type ID
|
|
706
|
+
resolve_prop_key_id: Function to resolve property key ID
|
|
707
|
+
"""
|
|
708
|
+
from kitedb._kitedb import PropValue
|
|
709
|
+
|
|
710
|
+
etype_id = resolve_etype_id(edge_def)
|
|
711
|
+
|
|
712
|
+
# Check if we're already in a transaction (e.g., from db.transaction() context)
|
|
713
|
+
in_tx = db.has_transaction()
|
|
714
|
+
if not in_tx:
|
|
715
|
+
db.begin()
|
|
716
|
+
try:
|
|
717
|
+
db.add_edge(src.id, etype_id, dst.id)
|
|
718
|
+
|
|
719
|
+
# Set edge properties if provided
|
|
720
|
+
if props:
|
|
721
|
+
for prop_name, value in props.items():
|
|
722
|
+
if value is None:
|
|
723
|
+
continue
|
|
724
|
+
prop_def = edge_def.props.get(prop_name)
|
|
725
|
+
if prop_def is None:
|
|
726
|
+
continue
|
|
727
|
+
|
|
728
|
+
prop_key_id = resolve_prop_key_id(edge_def, prop_name)
|
|
729
|
+
prop_value = to_prop_value(prop_def, value, PropValue)
|
|
730
|
+
db.set_edge_prop(src.id, etype_id, dst.id, prop_key_id, prop_value)
|
|
731
|
+
|
|
732
|
+
if not in_tx:
|
|
733
|
+
db.commit()
|
|
734
|
+
except Exception:
|
|
735
|
+
if not in_tx:
|
|
736
|
+
db.rollback()
|
|
737
|
+
raise
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def delete_link(
|
|
741
|
+
db: Database,
|
|
742
|
+
src: NodeRef[Any],
|
|
743
|
+
edge_def: EdgeDef,
|
|
744
|
+
dst: NodeRef[Any],
|
|
745
|
+
resolve_etype_id: Callable[[EdgeDef], int],
|
|
746
|
+
) -> None:
|
|
747
|
+
"""
|
|
748
|
+
Delete an edge between two nodes.
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
db: Database instance
|
|
752
|
+
src: Source node reference
|
|
753
|
+
edge_def: Edge definition
|
|
754
|
+
dst: Destination node reference
|
|
755
|
+
resolve_etype_id: Function to resolve edge type ID
|
|
756
|
+
"""
|
|
757
|
+
etype_id = resolve_etype_id(edge_def)
|
|
758
|
+
|
|
759
|
+
# Check if we're already in a transaction
|
|
760
|
+
in_tx = db.has_transaction()
|
|
761
|
+
if not in_tx:
|
|
762
|
+
db.begin()
|
|
763
|
+
try:
|
|
764
|
+
db.delete_edge(src.id, etype_id, dst.id)
|
|
765
|
+
if not in_tx:
|
|
766
|
+
db.commit()
|
|
767
|
+
except Exception:
|
|
768
|
+
if not in_tx:
|
|
769
|
+
db.rollback()
|
|
770
|
+
raise
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# ============================================================================
|
|
774
|
+
# Update Edge Builder
|
|
775
|
+
# ============================================================================
|
|
776
|
+
|
|
777
|
+
class UpdateEdgeExecutor:
|
|
778
|
+
"""Executor for edge property updates."""
|
|
779
|
+
|
|
780
|
+
def __init__(
|
|
781
|
+
self,
|
|
782
|
+
db: Database,
|
|
783
|
+
src: NodeRef[Any],
|
|
784
|
+
edge_def: EdgeDef,
|
|
785
|
+
dst: NodeRef[Any],
|
|
786
|
+
data: Dict[str, Any],
|
|
787
|
+
resolve_etype_id: Callable[[EdgeDef], int],
|
|
788
|
+
resolve_prop_key_id: Callable[[EdgeDef, str], int],
|
|
789
|
+
):
|
|
790
|
+
self._db = db
|
|
791
|
+
self._src = src
|
|
792
|
+
self._edge_def = edge_def
|
|
793
|
+
self._dst = dst
|
|
794
|
+
self._data = data
|
|
795
|
+
self._resolve_etype_id = resolve_etype_id
|
|
796
|
+
self._resolve_prop_key_id = resolve_prop_key_id
|
|
797
|
+
|
|
798
|
+
def execute(self) -> None:
|
|
799
|
+
"""Execute the edge property update."""
|
|
800
|
+
from kitedb._kitedb import PropValue
|
|
801
|
+
|
|
802
|
+
etype_id = self._resolve_etype_id(self._edge_def)
|
|
803
|
+
|
|
804
|
+
# Check if we're already in a transaction
|
|
805
|
+
in_tx = self._db.has_transaction()
|
|
806
|
+
if not in_tx:
|
|
807
|
+
self._db.begin()
|
|
808
|
+
try:
|
|
809
|
+
for prop_name, value in self._data.items():
|
|
810
|
+
prop_def = self._edge_def.props.get(prop_name)
|
|
811
|
+
if prop_def is None:
|
|
812
|
+
continue
|
|
813
|
+
|
|
814
|
+
prop_key_id = self._resolve_prop_key_id(self._edge_def, prop_name)
|
|
815
|
+
|
|
816
|
+
if value is None:
|
|
817
|
+
self._db.delete_edge_prop(
|
|
818
|
+
self._src.id, etype_id, self._dst.id, prop_key_id
|
|
819
|
+
)
|
|
820
|
+
else:
|
|
821
|
+
prop_value = to_prop_value(prop_def, value, PropValue)
|
|
822
|
+
self._db.set_edge_prop(
|
|
823
|
+
self._src.id, etype_id, self._dst.id, prop_key_id, prop_value
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
if not in_tx:
|
|
827
|
+
self._db.commit()
|
|
828
|
+
except Exception:
|
|
829
|
+
if not in_tx:
|
|
830
|
+
self._db.rollback()
|
|
831
|
+
raise
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class UpdateEdgeBuilder(Generic[E]):
|
|
835
|
+
"""
|
|
836
|
+
Builder for edge property updates.
|
|
837
|
+
|
|
838
|
+
Example:
|
|
839
|
+
>>> db.update_edge(alice, knows, bob).set(weight=0.9).execute()
|
|
840
|
+
"""
|
|
841
|
+
|
|
842
|
+
def __init__(
|
|
843
|
+
self,
|
|
844
|
+
db: Database,
|
|
845
|
+
src: NodeRef[Any],
|
|
846
|
+
edge_def: E,
|
|
847
|
+
dst: NodeRef[Any],
|
|
848
|
+
resolve_etype_id: Callable[[EdgeDef], int],
|
|
849
|
+
resolve_prop_key_id: Callable[[EdgeDef, str], int],
|
|
850
|
+
):
|
|
851
|
+
self._db = db
|
|
852
|
+
self._src = src
|
|
853
|
+
self._edge_def = edge_def
|
|
854
|
+
self._dst = dst
|
|
855
|
+
self._resolve_etype_id = resolve_etype_id
|
|
856
|
+
self._resolve_prop_key_id = resolve_prop_key_id
|
|
857
|
+
|
|
858
|
+
def set(self, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> UpdateEdgeExecutor:
|
|
859
|
+
"""
|
|
860
|
+
Set the edge properties to update.
|
|
861
|
+
|
|
862
|
+
Args:
|
|
863
|
+
data: Dictionary of property values
|
|
864
|
+
**kwargs: Alternative way to pass property values
|
|
865
|
+
|
|
866
|
+
Returns:
|
|
867
|
+
UpdateEdgeExecutor for executing
|
|
868
|
+
"""
|
|
869
|
+
if data is None:
|
|
870
|
+
data = kwargs
|
|
871
|
+
else:
|
|
872
|
+
data = {**data, **kwargs}
|
|
873
|
+
|
|
874
|
+
return UpdateEdgeExecutor(
|
|
875
|
+
db=self._db,
|
|
876
|
+
src=self._src,
|
|
877
|
+
edge_def=self._edge_def,
|
|
878
|
+
dst=self._dst,
|
|
879
|
+
data=data,
|
|
880
|
+
resolve_etype_id=self._resolve_etype_id,
|
|
881
|
+
resolve_prop_key_id=self._resolve_prop_key_id,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
__all__ = [
|
|
886
|
+
"NodeRef",
|
|
887
|
+
"InsertBuilder",
|
|
888
|
+
"InsertExecutor",
|
|
889
|
+
"UpdateBuilder",
|
|
890
|
+
"UpdateExecutor",
|
|
891
|
+
"UpdateByRefBuilder",
|
|
892
|
+
"UpdateByRefExecutor",
|
|
893
|
+
"DeleteBuilder",
|
|
894
|
+
"DeleteExecutor",
|
|
895
|
+
"UpdateEdgeBuilder",
|
|
896
|
+
"UpdateEdgeExecutor",
|
|
897
|
+
"create_link",
|
|
898
|
+
"delete_link",
|
|
899
|
+
"to_prop_value",
|
|
900
|
+
"from_prop_value",
|
|
901
|
+
]
|