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/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
+ ]