@@ -1369,7 +1369,9 @@ async def clean_node_table(self, writer: Optional[aiosqlite.Connection] = None)
1369
1369
else :
1370
1370
await writer .execute (query , params )
1371
1371
1372
- async def get_leaf_at_minimum_height (self , root_hash : bytes32 ) -> TerminalNode :
1372
+ async def get_leaf_at_minimum_height (
1373
+ self , root_hash : bytes32 , hash_to_parent : Dict [bytes32 , InternalNode ]
1374
+ ) -> TerminalNode :
1373
1375
root_node = await self .get_node (root_hash )
1374
1376
queue : List [Node ] = [root_node ]
1375
1377
while True :
@@ -1378,11 +1380,29 @@ async def get_leaf_at_minimum_height(self, root_hash: bytes32) -> TerminalNode:
1378
1380
if isinstance (node , InternalNode ):
1379
1381
left_node = await self .get_node (node .left_hash )
1380
1382
right_node = await self .get_node (node .right_hash )
1383
+ hash_to_parent [left_node .hash ] = node
1384
+ hash_to_parent [right_node .hash ] = node
1381
1385
queue .append (left_node )
1382
1386
queue .append (right_node )
1383
1387
elif isinstance (node , TerminalNode ):
1384
1388
return node
1385
1389
1390
+ async def batch_upsert (
1391
+ self ,
1392
+ tree_id : bytes32 ,
1393
+ hash : bytes32 ,
1394
+ to_update_hashes : Set [bytes32 ],
1395
+ pending_upsert_new_hashes : Dict [bytes32 , bytes32 ],
1396
+ ) -> bytes32 :
1397
+ if hash not in to_update_hashes :
1398
+ return hash
1399
+ node = await self .get_node (hash )
1400
+ if isinstance (node , TerminalNode ):
1401
+ return pending_upsert_new_hashes [hash ]
1402
+ new_left_hash = await self .batch_upsert (tree_id , node .left_hash , to_update_hashes , pending_upsert_new_hashes )
1403
+ new_right_hash = await self .batch_upsert (tree_id , node .right_hash , to_update_hashes , pending_upsert_new_hashes )
1404
+ return await self ._insert_internal_node (new_left_hash , new_right_hash )
1405
+
1386
1406
async def insert_batch (
1387
1407
self ,
1388
1408
store_id : bytes32 ,
@@ -1410,14 +1430,19 @@ async def insert_batch(
1410
1430
1411
1431
key_hash_frequency : Dict [bytes32 , int ] = {}
1412
1432
first_action : Dict [bytes32 , str ] = {}
1433
+ last_action : Dict [bytes32 , str ] = {}
1434
+
1413
1435
for change in changelist :
1414
1436
key = change ["key" ]
1415
1437
hash = key_hash (key )
1416
1438
key_hash_frequency [hash ] = key_hash_frequency .get (hash , 0 ) + 1
1417
1439
if hash not in first_action :
1418
1440
first_action [hash ] = change ["action" ]
1441
+ last_action [hash ] = change ["action" ]
1419
1442
1420
1443
pending_autoinsert_hashes : List [bytes32 ] = []
1444
+ pending_upsert_new_hashes : Dict [bytes32 , bytes32 ] = {}
1445
+
1421
1446
for change in changelist :
1422
1447
if change ["action" ] == "insert" :
1423
1448
key = change ["key" ]
@@ -1435,8 +1460,16 @@ async def insert_batch(
1435
1460
if key_hash_frequency [hash ] == 1 or (
1436
1461
key_hash_frequency [hash ] == 2 and first_action [hash ] == "delete"
1437
1462
):
1463
+ old_node = await self .maybe_get_node_by_key (key , store_id )
1438
1464
terminal_node_hash = await self ._insert_terminal_node (key , value )
1439
- pending_autoinsert_hashes .append (terminal_node_hash )
1465
+
1466
+ if old_node is None :
1467
+ pending_autoinsert_hashes .append (terminal_node_hash )
1468
+ else :
1469
+ if key_hash_frequency [hash ] == 1 :
1470
+ raise Exception (f"Key already present: { key .hex ()} " )
1471
+ else :
1472
+ pending_upsert_new_hashes [old_node .hash ] = terminal_node_hash
1440
1473
continue
1441
1474
insert_result = await self .autoinsert (
1442
1475
key , value , store_id , True , Status .COMMITTED , root = latest_local_root
@@ -1458,17 +1491,50 @@ async def insert_batch(
1458
1491
latest_local_root = insert_result .root
1459
1492
elif change ["action" ] == "delete" :
1460
1493
key = change ["key" ]
1494
+ hash = key_hash (key )
1495
+ if key_hash_frequency [hash ] == 2 and last_action [hash ] == "insert" and enable_batch_autoinsert :
1496
+ continue
1461
1497
latest_local_root = await self .delete (key , store_id , True , Status .COMMITTED , root = latest_local_root )
1462
1498
elif change ["action" ] == "upsert" :
1463
1499
key = change ["key" ]
1464
1500
new_value = change ["value" ]
1501
+ hash = key_hash (key )
1502
+ if key_hash_frequency [hash ] == 1 and enable_batch_autoinsert :
1503
+ terminal_node_hash = await self ._insert_terminal_node (key , new_value )
1504
+ old_node = await self .maybe_get_node_by_key (key , store_id )
1505
+ if old_node is not None :
1506
+ pending_upsert_new_hashes [old_node .hash ] = terminal_node_hash
1507
+ else :
1508
+ pending_autoinsert_hashes .append (terminal_node_hash )
1509
+ continue
1465
1510
insert_result = await self .upsert (
1466
1511
key , new_value , store_id , True , Status .COMMITTED , root = latest_local_root
1467
1512
)
1468
1513
latest_local_root = insert_result .root
1469
1514
else :
1470
1515
raise Exception (f"Operation in batch is not insert or delete: { change } " )
1471
1516
1517
+ if len (pending_upsert_new_hashes ) > 0 :
1518
+ to_update_hashes : Set [bytes32 ] = set ()
1519
+ for hash in pending_upsert_new_hashes .keys ():
1520
+ while True :
1521
+ if hash in to_update_hashes :
1522
+ break
1523
+ to_update_hashes .add (hash )
1524
+ node = await self ._get_one_ancestor (hash , store_id )
1525
+ if node is None :
1526
+ break
1527
+ hash = node .hash
1528
+ assert latest_local_root is not None
1529
+ assert latest_local_root .node_hash is not None
1530
+ new_root_hash = await self .batch_upsert (
1531
+ store_id ,
1532
+ latest_local_root .node_hash ,
1533
+ to_update_hashes ,
1534
+ pending_upsert_new_hashes ,
1535
+ )
1536
+ latest_local_root = await self ._insert_root (store_id , new_root_hash , Status .COMMITTED )
1537
+
1472
1538
# Start with the leaf nodes and pair them to form new nodes at the next level up, repeating this process
1473
1539
# in a bottom-up fashion until a single root node remains. This constructs a balanced tree from the leaves.
1474
1540
while len (pending_autoinsert_hashes ) > 1 :
@@ -1488,14 +1554,15 @@ async def insert_batch(
1488
1554
if latest_local_root is None or latest_local_root .node_hash is None :
1489
1555
await self ._insert_root (store_id = store_id , node_hash = subtree_hash , status = Status .COMMITTED )
1490
1556
else :
1491
- min_height_leaf = await self .get_leaf_at_minimum_height (latest_local_root .node_hash )
1492
- ancestors = await self .get_ancestors_common (
1493
- node_hash = min_height_leaf .hash ,
1494
- store_id = store_id ,
1495
- root_hash = latest_local_root .node_hash ,
1496
- generation = latest_local_root .generation ,
1497
- use_optimized = True ,
1498
- )
1557
+ hash_to_parent : Dict [bytes32 , InternalNode ] = {}
1558
+ min_height_leaf = await self .get_leaf_at_minimum_height (latest_local_root .node_hash , hash_to_parent )
1559
+ ancestors : List [InternalNode ] = []
1560
+ hash = min_height_leaf .hash
1561
+ while hash in hash_to_parent :
1562
+ node = hash_to_parent [hash ]
1563
+ ancestors .append (node )
1564
+ hash = node .hash
1565
+
1499
1566
await self .update_ancestor_hashes_on_insert (
1500
1567
store_id = store_id ,
1501
1568
left = min_height_leaf .hash ,
@@ -1631,6 +1698,13 @@ async def get_node_by_key_latest_generation(self, key: bytes, store_id: bytes32)
1631
1698
assert isinstance (node , TerminalNode )
1632
1699
return node
1633
1700
1701
+ async def maybe_get_node_by_key (self , key : bytes , tree_id : bytes32 ) -> Optional [TerminalNode ]:
1702
+ try :
1703
+ node = await self .get_node_by_key_latest_generation (key , tree_id )
1704
+ return node
1705
+ except KeyNotFoundError :
1706
+ return None
1707
+
1634
1708
async def get_node_by_key (
1635
1709
self ,
1636
1710
key : bytes ,
0 commit comments