@@ -114,13 +114,7 @@ func (s *ItemCRUDService) DeleteSingleWithMetadata(itemID int, metadata itemeven
114114 }); err != nil {
115115 return err
116116 }
117- repository .InvalidateItemListCountCache (s .db , workspaceID )
118-
119- // Live-update publish (WI-483): the delete has committed.
120- PublishItemChange (itemID , ItemChangeDeleted )
121- if parentID != nil {
122- PublishItemChange (* parentID , ItemChangeUpdated )
123- }
117+ s .finishItemDeletion (workspaceID , []int {itemID }, parentID )
124118 return nil
125119}
126120
@@ -131,7 +125,6 @@ func (s *ItemCRUDService) Delete(itemID int) (*DeleteResult, error) {
131125
132126// DeleteWithMetadata deletes an item subtree and records every removed item.
133127func (s * ItemCRUDService ) DeleteWithMetadata (itemID int , metadata itemevents.Metadata ) (* DeleteResult , error ) {
134- // Get parent ID before deleting
135128 parentID , err := s .repo .GetParentID (itemID )
136129 if err != nil {
137130 if err == repository .ErrNotFound {
@@ -140,86 +133,81 @@ func (s *ItemCRUDService) DeleteWithMetadata(itemID int, metadata itemevents.Met
140133 return nil , err
141134 }
142135 workspaceID , _ := s .repo .GetWorkspaceID (itemID )
143-
144- // Get all descendant IDs for cascade operations
145136 descendantIDs , err := s .repo .GetDescendantIDs (itemID )
146137 if err != nil {
147138 return nil , fmt .Errorf ("failed to get descendants: %w" , err )
148139 }
149-
150- // Delete all related data for item and descendants
151140 allIDs := append ([]int {itemID }, descendantIDs ... )
152- if err := database .WithTx (s .db , func (tx database.Tx ) error {
153- items , err := s .repo .FindByIDsForUpdateContext (context .Background (), tx , allIDs )
141+ if err := s .deleteItemSubtree (itemID , allIDs , len (descendantIDs ), metadata ); err != nil {
142+ return nil , err
143+ }
144+ s .finishItemDeletion (workspaceID , allIDs , parentID )
145+ return & DeleteResult {
146+ DeletedCount : len (allIDs ),
147+ DescendantIDs : descendantIDs ,
148+ AffectedParent : parentID ,
149+ }, nil
150+ }
151+
152+ func (s * ItemCRUDService ) deleteItemSubtree (rootID int , itemIDs []int , descendantCount int , metadata itemevents.Metadata ) error {
153+ return database .WithTx (s .db , func (tx database.Tx ) error {
154+ ctx := context .Background ()
155+ items , err := s .repo .FindByIDsForUpdateContext (ctx , tx , itemIDs )
154156 if err != nil {
155157 return err
156158 }
157- if len (items ) != len (allIDs ) {
159+ if len (items ) != len (itemIDs ) {
158160 return repository .ErrNotFound
159161 }
160162 if metadata .OccurredAt .IsZero () {
161163 metadata .OccurredAt = time .Now ()
162164 }
163165 recorder := itemevents .NewRecorder (s .db )
164- if err := recordRemovedItemLinks (context . Background () , s .db , tx , allIDs , metadata ); err != nil {
166+ if err := recordRemovedItemLinks (ctx , s .db , tx , itemIDs , metadata ); err != nil {
165167 return err
166168 }
167169 for _ , item := range items {
168- descendantCount := 0
169- if item .ID == itemID {
170- descendantCount = len ( descendantIDs )
170+ removedDescendants := 0
171+ if item .ID == rootID {
172+ removedDescendants = descendantCount
171173 }
172- if _ , err := recorder .Deleted (context . Background () , tx , item , descendantCount , metadata ); err != nil {
174+ if _ , err := recorder .Deleted (ctx , tx , item , removedDescendants , metadata ); err != nil {
173175 return err
174176 }
175177 }
176- for _ , id := range allIDs {
177- // Delete watches
178- if err := s .repo .DeleteItemWatches (tx , id ); err != nil {
178+ for _ , id := range itemIDs {
179+ if err := s .deleteItemRelationsTx (tx , id ); err != nil {
179180 return err
180181 }
181-
182- // Delete history
183- if err := s .repo .DeleteItemHistory (tx , id ); err != nil {
184- return err
185- }
186-
187- // Delete links
188- if err := s .repo .DeleteItemLinks (tx , id ); err != nil {
189- return err
190- }
191-
192- // Clear worklog references
193- if err := s .repo .ClearWorklogItemReferences (tx , id ); err != nil {
194- return err
195- }
196-
197- // Delete the item itself
198182 if err := s .repo .Delete (tx , id ); err != nil {
199183 return err
200184 }
201185 }
202186 return nil
203- }); err != nil {
204- return nil , err
187+ })
188+ }
189+
190+ func (s * ItemCRUDService ) deleteItemRelationsTx (tx database.Tx , itemID int ) error {
191+ if err := s .repo .DeleteItemWatches (tx , itemID ); err != nil {
192+ return err
205193 }
206- repository .InvalidateItemListCountCache (s .db , workspaceID )
194+ if err := s .repo .DeleteItemHistory (tx , itemID ); err != nil {
195+ return err
196+ }
197+ if err := s .repo .DeleteItemLinks (tx , itemID ); err != nil {
198+ return err
199+ }
200+ return s .repo .ClearWorklogItemReferences (tx , itemID )
201+ }
207202
208- // Live-update publish (WI-483): the cascade delete committed. Announce every
209- // removed item (so anyone viewing a descendant reconciles) and refresh the
210- // affected parent's child list.
211- for _ , id := range allIDs {
203+ func (s * ItemCRUDService ) finishItemDeletion (workspaceID int , itemIDs []int , parentID * int ) {
204+ repository .InvalidateItemListCountCache (s .db , workspaceID )
205+ for _ , id := range itemIDs {
212206 PublishItemChange (id , ItemChangeDeleted )
213207 }
214208 if parentID != nil {
215209 PublishItemChange (* parentID , ItemChangeUpdated )
216210 }
217-
218- return & DeleteResult {
219- DeletedCount : len (allIDs ),
220- DescendantIDs : descendantIDs ,
221- AffectedParent : parentID ,
222- }, nil
223211}
224212
225213// CopyOptions contains options for copying an item
@@ -485,6 +473,31 @@ func (s *ItemCRUDService) evaluateQLContext(requestCtx context.Context, qlQuery
485473 return qlSQL , qlArgs , nil
486474}
487475
476+ type resolvedItemListQL struct {
477+ sql string
478+ args []any
479+ collectionResolved bool
480+ }
481+
482+ func (s * ItemCRUDService ) resolveItemListQLContext (ctx context.Context , qlQuery string , collectionID int , subQL string , userID int ) (resolvedItemListQL , error ) {
483+ qlQuery , collectionResolved , err := s .resolveCollectionQLContext (ctx , qlQuery , collectionID )
484+ if err != nil {
485+ return resolvedItemListQL {}, err
486+ }
487+ if subQL = strings .TrimSpace (subQL ); subQL != "" {
488+ if qlQuery == "" {
489+ qlQuery = subQL
490+ } else {
491+ qlQuery = "(" + qlQuery + ") AND (" + subQL + ")"
492+ }
493+ }
494+ qlSQL , qlArgs , err := s .evaluateQLContext (ctx , qlQuery , cql .UserContext (userID ))
495+ if err != nil {
496+ return resolvedItemListQL {}, err
497+ }
498+ return resolvedItemListQL {sql : qlSQL , args : qlArgs , collectionResolved : collectionResolved }, nil
499+ }
500+
488501// BacklogParams contains parameters for retrieving backlog items
489502type BacklogParams struct {
490503 WorkspaceID int // 0 if not specified (collection-only query)
@@ -521,33 +534,17 @@ func (s *ItemCRUDService) GetBacklogItemsContext(ctx context.Context, params Bac
521534 StatusIDs : backlogStatusIDs ,
522535 }
523536
524- // Resolve QL query from collection or direct parameter
525- qlQuery , collectionResolved , err := s .resolveCollectionQLContext (ctx , params .QLQuery , params .CollectionID )
537+ resolvedQL , err := s .resolveItemListQLContext (ctx , params .QLQuery , params .CollectionID , params .SubQLQuery , params .UserID )
526538 if err != nil {
527539 return nil , 0 , err
528540 }
529-
530- // Combine with sub-filter QL if provided
531- if subQL := strings .TrimSpace (params .SubQLQuery ); subQL != "" {
532- if qlQuery != "" {
533- qlQuery = "(" + qlQuery + ") AND (" + subQL + ")"
534- } else {
535- qlQuery = subQL
536- }
537- }
538-
539- // Evaluate QL query into SQL
540- qlSQL , qlArgs , err := s .evaluateQLContext (ctx , qlQuery , cql .UserContext (params .UserID ))
541- if err != nil {
542- return nil , 0 , err
543- }
544- if qlSQL != "" {
545- filters .QLQuery = qlSQL
546- filters .QLArgs = qlArgs
541+ if resolvedQL .sql != "" {
542+ filters .QLQuery = resolvedQL .sql
543+ filters .QLArgs = resolvedQL .args
547544 }
548545
549546 // Apply workspace_id filter only when no collection was resolved
550- if ! collectionResolved && params .WorkspaceID > 0 {
547+ if ! resolvedQL . collectionResolved && params .WorkspaceID > 0 {
551548 filters .WorkspaceID = & params .WorkspaceID
552549 }
553550
@@ -596,39 +593,23 @@ func (s *ItemCRUDService) ListWithQLPageContext(ctx context.Context, params List
596593
597594 filters := params .Filters
598595
599- // Resolve QL query from collection or direct parameter
600- qlQuery , collectionResolved , err := s .resolveCollectionQLContext (ctx , params .QLQuery , params .CollectionID )
601- if err != nil {
602- return repository.ItemListPage {}, err
603- }
604-
605- // Combine with sub-filter QL if provided
606- if subQL := strings .TrimSpace (params .SubQLQuery ); subQL != "" {
607- if qlQuery != "" {
608- qlQuery = "(" + qlQuery + ") AND (" + subQL + ")"
609- } else {
610- qlQuery = subQL
611- }
612- }
613-
614- // Evaluate QL query into SQL
615- qlSQL , qlArgs , err := s .evaluateQLContext (ctx , qlQuery , cql .UserContext (params .UserID ))
596+ resolvedQL , err := s .resolveItemListQLContext (ctx , params .QLQuery , params .CollectionID , params .SubQLQuery , params .UserID )
616597 if err != nil {
617598 return repository.ItemListPage {}, err
618599 }
619- if qlSQL != "" {
620- filters .QLQuery = qlSQL
621- filters .QLArgs = qlArgs
600+ if resolvedQL . sql != "" {
601+ filters .QLQuery = resolvedQL . sql
602+ filters .QLArgs = resolvedQL . args
622603 }
623604
624605 // If collection was resolved but produced no effective query, return empty results.
625606 // A collection with no filter means "nothing to show yet."
626- if collectionResolved && filters .QLQuery == "" {
607+ if resolvedQL . collectionResolved && filters .QLQuery == "" {
627608 return repository.ItemListPage {Items : []models.Item {}}, nil
628609 }
629610
630611 // Apply workspace_id filter only when no collection was resolved
631- if ! collectionResolved && params .WorkspaceID > 0 {
612+ if ! resolvedQL . collectionResolved && params .WorkspaceID > 0 {
632613 filters .WorkspaceID = & params .WorkspaceID
633614 }
634615
0 commit comments