Coverage for functions \ flipdare \ firestore \ core \ sub_comment_transaction.py: 60%
50 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
14from typing import Any
16from google.cloud.firestore import DocumentReference, Increment, Transaction, transactional
17from google.cloud.firestore_v1 import DocumentSnapshot
18from flipdare.error import DatabaseError
19from flipdare.firestore.core.app_base_model import AppBaseModel
20from flipdare.generated.shared.app_error_code import AppErrorCode
21from flipdare.generated.shared.firestore_collections import FirestoreCollections
23__all__ = ["SubCommentTransaction"]
26class SubCommentTransaction[T: AppBaseModel]:
27 def __init__(self, parent_db: Any, sub_collection_name: FirestoreCollections) -> None:
28 self.parent_db = parent_db
29 self.sub_name = sub_collection_name
30 self.client = parent_db.client
32 def create_with_increment(
33 self,
34 parent_id: str,
35 model: T,
36 count_field: str = "comment_count",
37 ) -> DocumentReference:
38 """Orchestrates the transaction to create a child and increment the parent."""
39 transaction = self.client.transaction()
41 # References
42 parent_ref = self.client.collection(self.parent_db.collection_name).document(parent_id)
43 child_ref = parent_ref.collection(self.sub_name).document()
44 if not isinstance(child_ref, DocumentReference):
45 msg = f"Failed to create child document for parent {parent_id}"
46 raise DatabaseError(
47 msg,
48 error_code=AppErrorCode.DATABASE,
49 collection_name=self.sub_name,
50 document_id=parent_id,
51 )
53 @transactional
54 def _run_transaction(trans: Transaction) -> None:
55 # 1. READ: Must happen before writes
56 snapshot = parent_ref.get(transaction=transaction)
57 self._check_valid_snapshot(parent_id, snapshot)
59 # 2. WRITE: Create Child
60 trans.set(child_ref, model.to_dict())
62 # 3. WRITE: Increment Parent
63 trans.update(parent_ref, {count_field: Increment(1)})
65 _run_transaction(transaction)
66 return child_ref
68 def delete_with_decrement(
69 self,
70 parent_id: str,
71 child_id: str,
72 count_field: str = "comment_count",
73 ) -> None:
74 """Atomically deletes a child document and decrements the parent counter."""
75 transaction = self.client.transaction()
77 # Define references
78 parent_ref = self.client.collection(self.parent_db.collection_name).document(parent_id)
79 child_ref = parent_ref.collection(self.sub_name).document(child_id)
81 @transactional
82 def _run_delete_transaction(trans: Transaction) -> None:
83 # 1. READ: Check both exist before performing any writes
84 # Transactional reads are required to ensure data consistency
85 parent_snap = parent_ref.get(transaction=trans)
86 child_snap = child_ref.get(transaction=trans)
88 self._check_valid_snapshot(parent_id, parent_snap)
89 self._check_valid_snapshot(child_id, child_snap)
91 assert isinstance(parent_snap, DocumentSnapshot) # narrowing
92 current_count = parent_snap.get(count_field) or 0
94 # 2. WRITE: Delete the child document
95 trans.delete(child_ref)
97 # 3. WRITE: Decrement the counter on the parent
98 # Passing -1 to Increment() performs a server-side atomic subtraction
99 if current_count > 0:
100 trans.update(parent_ref, {count_field: Increment(-1)})
102 _run_delete_transaction(transaction)
104 def _check_valid_snapshot(self, doc_id: str, snap: Any) -> None:
105 if not isinstance(snap, DocumentSnapshot):
106 msg = f"Got an child AwaitableSnapshot for id {doc_id}, check you didnt call await.."
107 raise TypeError(msg)
109 if not snap.exists:
110 raise ValueError(f"Failed to retrieve document {doc_id}.")