Welcome to mirror list, hosted at ThFree Co, Russian Federation.

ChangeProcessor.cs « System.Data.Linq « referencesource « class « mcs - github.com/mono/mono.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 0affa3cb60ade99aa0ed6733d62098a248a26775 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Diagnostics;

namespace System.Data.Linq {
    using System.Data.Linq.Mapping;
    using System.Data.Linq.Provider;

    /// <summary>
    /// Describes the type of change the entity will undergo when submitted to the database.
    /// </summary>
    public enum ChangeAction {
        /// <summary>
        /// The entity will not be submitted.
        /// </summary>
        None = 0,
        /// <summary>
        /// The entity will be deleted.
        /// </summary>
        Delete,
        /// <summary>
        /// The entity will be inserted.
        /// </summary>
        Insert,
        /// <summary>
        /// The entity will be updated.
        /// </summary>
        Update
    }

    internal class ChangeProcessor {
        CommonDataServices services;
        DataContext context;
        ChangeTracker tracker;
        ChangeDirector changeDirector;
        EdgeMap currentParentEdges;
        EdgeMap originalChildEdges;
        ReferenceMap originalChildReferences;

        internal ChangeProcessor(CommonDataServices services, DataContext context) {
            this.services = services;
            this.context = context;
            this.tracker = services.ChangeTracker;
            this.changeDirector = services.ChangeDirector;
            this.currentParentEdges = new EdgeMap();
            this.originalChildEdges = new EdgeMap();
            this.originalChildReferences = new ReferenceMap();
        }

        internal void SubmitChanges(ConflictMode failureMode) {
            this.TrackUntrackedObjects();
            // Must apply inferred deletions only after any untracked objects
            // are tracked
            this.ApplyInferredDeletions();
            this.BuildEdgeMaps();

            var list = this.GetOrderedList();

            ValidateAll(list);

            int numUpdatesAttempted = 0;
            ChangeConflictSession conflictSession = new ChangeConflictSession(this.context);
            List<ObjectChangeConflict> conflicts = new List<ObjectChangeConflict>();
            List<TrackedObject> deletedItems = new List<TrackedObject>();
            List<TrackedObject> insertedItems = new List<TrackedObject>();
            List<TrackedObject> syncDependentItems = new List<TrackedObject>();
            
            foreach (TrackedObject item in list) {
                try {
                    if (item.IsNew) {
                        if (item.SynchDependentData()) {
                            syncDependentItems.Add(item);
                        }
                        changeDirector.Insert(item);
                        // store all inserted items for post processing
                        insertedItems.Add(item);
                    }
                    else if (item.IsDeleted) {
                        // Delete returns 1 if the delete was successfull, 0 if the row exists
                        // but wasn't deleted due to an OC conflict, or -1 if the row was
                        // deleted by another context (no OC conflict in this case)
                        numUpdatesAttempted++;
                        int ret = changeDirector.Delete(item);
                        if (ret == 0) {
                            conflicts.Add(new ObjectChangeConflict(conflictSession, item, false));
                        }
                        else {
                            // store all deleted items for post processing
                            deletedItems.Add(item);
                        }
                    }
                    else if (item.IsPossiblyModified) {
                        if (item.SynchDependentData()) {
                            syncDependentItems.Add(item);
                        }
                        if (item.IsModified) {
                            CheckForInvalidChanges(item);
                            numUpdatesAttempted++;
                            if (changeDirector.Update(item) <= 0) {
                                conflicts.Add(new ObjectChangeConflict(conflictSession, item));
                            }
                        }
                    }
                }
                catch (ChangeConflictException) {
                    conflicts.Add(new ObjectChangeConflict(conflictSession, item));
                }
                if (conflicts.Count > 0 && failureMode == ConflictMode.FailOnFirstConflict) {
                    break;
                }
            }

            // if we have accumulated any failed updates, throw the exception now
            if (conflicts.Count > 0) {
                // First we need to rollback any value that have already been auto-sync'd, since the values are no longer valid on the server
                changeDirector.RollbackAutoSync();
                // Also rollback any dependent items that were sync'd, since their parent values may have been rolled back
                foreach (TrackedObject syncDependentItem in syncDependentItems) {
                    Debug.Assert(syncDependentItem.IsNew || syncDependentItem.IsPossiblyModified, "SynchDependent data should only be rolled back for new and modified objects.");
                    syncDependentItem.SynchDependentData();
                }
                this.context.ChangeConflicts.Fill(conflicts);
                throw CreateChangeConflictException(numUpdatesAttempted, conflicts.Count);
            }
            else {
                // No conflicts occurred, so we don't need to save the rollback values anymore
                changeDirector.ClearAutoSyncRollback();
            }

            // Only after all updates have been sucessfully processed do we want to make
            // post processing modifications to the objects and/or cache state.
            PostProcessUpdates(insertedItems, deletedItems);
        }

        private void PostProcessUpdates(List<TrackedObject> insertedItems, List<TrackedObject> deletedItems) {
            // perform post delete processing
            foreach (TrackedObject deletedItem in deletedItems) {
                // remove deleted item from identity cache
                this.services.RemoveCachedObjectLike(deletedItem.Type, deletedItem.Original);
                ClearForeignKeyReferences(deletedItem);
            }

            // perform post insert processing
            foreach (TrackedObject insertedItem in insertedItems) {
                object lookup = this.services.InsertLookupCachedObject(insertedItem.Type, insertedItem.Current);
                if (lookup != insertedItem.Current) {
                    throw new DuplicateKeyException(insertedItem.Current, Strings.DatabaseGeneratedAlreadyExistingKey);
                }
                insertedItem.InitializeDeferredLoaders();
            }
        }

        /// <summary>
        /// Clears out the foreign key values and parent object references for deleted objects on the child side of a relationship.
        /// For bi-directional relationships, also performs the following fixup:
        ///   - for 1:N we remove the deleted entity from the opposite EntitySet or collection
        ///   - for 1:1 we null out the back reference
        /// </summary>
        private void ClearForeignKeyReferences(TrackedObject to) {
            Debug.Assert(to.IsDeleted, "Foreign key reference cleanup should only happen on Deleted objects.");
            foreach (MetaAssociation assoc in to.Type.Associations) {
                if (assoc.IsForeignKey) {
                    // If there is a member on the other side referring back to us (i.e. this is a bi-directional relationship),
                    // we want to do a cache lookup to find the other side, then will remove ourselves from that collection.
                    // This cache lookup is only possible if the other key is the primary key, since that is the only way items can be found in the cache.
                    if (assoc.OtherMember != null && assoc.OtherKeyIsPrimaryKey) {
                        Debug.Assert(assoc.OtherMember.IsAssociation, "OtherMember of the association is expected to also be an association.");
                        // Search the cache for the target of the association, since
                        // it might not be loaded on the object being deleted, and we
                        // don't want to force a load.
                        object[] keyValues = CommonDataServices.GetForeignKeyValues(assoc, to.Current);
                        object cached = this.services.IdentityManager.Find(assoc.OtherType, keyValues);

                        if (cached != null) {
                            if (assoc.OtherMember.Association.IsMany) {
                                // Note that going through the IList interface handles 
                                // EntitySet as well as POCO collections that implement IList 
                                // and are not FixedSize.
                                System.Collections.IList collection = assoc.OtherMember.MemberAccessor.GetBoxedValue(cached) as System.Collections.IList;
                                if (collection != null && !collection.IsFixedSize) {
                                    collection.Remove(to.Current);
                                    // Explicitly clear the foreign key values and parent object reference
                                    ClearForeignKeysHelper(assoc, to.Current);
                                }
                            }
                            else {
                                // Null out the other association.  Since this is a 1:1 association,
                                // we're not concerned here with causing a deferred load, since the
                                // target is already cached (since we're deleting it).
                                assoc.OtherMember.MemberAccessor.SetBoxedValue(ref cached, null);
                                // Explicitly clear the foreign key values and parent object reference
                                ClearForeignKeysHelper(assoc, to.Current);
                            }
                        }
                        // else the item was not found in the cache, so there is no fixup that has to be done
                        // We are explicitly not calling ClearForeignKeysHelper because it breaks existing shipped behavior and we want to maintain backward compatibility
                    }
                    else {
                        // This is a unidirectional relationship or we have no way to look up the other side in the cache, so just clear our own side
                        ClearForeignKeysHelper(assoc, to.Current);
                    }                    
                }
                // else this is not the 1-side (foreign key) of the relationship, so there is nothing for us to do
            }
        }

        // Ensure the the member and foreign keys are nulled so that after trackedInstance is deleted,
        // the object does not appear to be associated with the other side anymore. This prevents the deleted object
        // from referencing objects still in the cache, but also will prevent the related object from being implicitly loaded
        private static void ClearForeignKeysHelper(MetaAssociation assoc, object trackedInstance) {            
            Debug.Assert(assoc.IsForeignKey, "Foreign key clearing should only happen on foreign key side of the association.");
            Debug.Assert(assoc.ThisMember.IsAssociation, "Expected ThisMember of an association to always be an association.");
            
            // If this member is one of our deferred loaders, and it does not already have a value, explicitly set the deferred source to
            // null so that when we set the association member itself to null later, it doesn't trigger an implicit load.
            // This is only necessary if the value has not already been assigned or set, because otherwise we won't implicitly load anyway when the member is accessed.
            MetaDataMember thisMember = assoc.ThisMember;

            if (thisMember.IsDeferred &&
                !(thisMember.StorageAccessor.HasAssignedValue(trackedInstance) || thisMember.StorageAccessor.HasLoadedValue(trackedInstance)))
            {
                // If this is a deferred member, set the value directly in the deferred accessor instead of going 
                // through the normal member accessor, so that we don't trigger an implicit load.                                            
                thisMember.DeferredSourceAccessor.SetBoxedValue(ref trackedInstance, null);
            }

            // Notify the object that the relationship should be considered deleted.
            // This allows the object to do its own fixup even when we can't do it automatically.
            thisMember.MemberAccessor.SetBoxedValue(ref trackedInstance, null);

            // Also set the foreign key values to null if possible
            for (int i = 0, n = assoc.ThisKey.Count; i < n; i++) {
                MetaDataMember thisKey = assoc.ThisKey[i];
                if (thisKey.CanBeNull) {
                    thisKey.StorageAccessor.SetBoxedValue(ref trackedInstance, null);
                }
            }
        }

        private static void ValidateAll(IEnumerable<TrackedObject> list) {
            foreach (var item in list) {
                if (item.IsNew) {
                    item.SynchDependentData();
                    if (item.Type.HasAnyValidateMethod) {
                        SendOnValidate(item.Type, item, ChangeAction.Insert);
                    }
                } else if (item.IsDeleted) {
                    if (item.Type.HasAnyValidateMethod) {
                        SendOnValidate(item.Type, item, ChangeAction.Delete);
                    }
                } else if (item.IsPossiblyModified) {
                    item.SynchDependentData();
                    if (item.IsModified && item.Type.HasAnyValidateMethod) {
                        SendOnValidate(item.Type, item, ChangeAction.Update);
                    }
                }
            }
        }

        private static void SendOnValidate(MetaType type, TrackedObject item, ChangeAction changeAction) {
            if (type != null) {
                SendOnValidate(type.InheritanceBase, item, changeAction);

                if (type.OnValidateMethod != null) {
                    try {
                        type.OnValidateMethod.Invoke(item.Current, new object[] { changeAction });
                    } catch (TargetInvocationException tie) {
                        if (tie.InnerException != null) {
                            throw tie.InnerException;
                        }

                        throw;
                    }
                }
            }
        }

        internal string GetChangeText() {
            this.ObserveUntrackedObjects();
            // Must apply inferred deletions only after any untracked objects
            // are tracked
            this.ApplyInferredDeletions();
            this.BuildEdgeMaps();

            // append change text only
            StringBuilder changeText = new StringBuilder();
            foreach (TrackedObject item in this.GetOrderedList()) {
                if (item.IsNew) {
                    item.SynchDependentData();
                    changeDirector.AppendInsertText(item, changeText);
                }
                else if (item.IsDeleted) {
                    changeDirector.AppendDeleteText(item, changeText);
                }
                else if (item.IsPossiblyModified) {
                    item.SynchDependentData();
                    if (item.IsModified) {
                        changeDirector.AppendUpdateText(item, changeText);
                    }
                }
            }
            return changeText.ToString();
        }

        internal ChangeSet GetChangeSet() {
            List<object> newEntities = new List<object>();
            List<object> deletedEntities = new List<object>();
            List<object> changedEntities = new List<object>();

            this.ObserveUntrackedObjects();
            // Must apply inferred deletions only after any untracked objects
            // are tracked
            this.ApplyInferredDeletions();

            foreach (TrackedObject item in this.tracker.GetInterestingObjects()) {
                if (item.IsNew) {
                    item.SynchDependentData();
                    newEntities.Add(item.Current);
                }
                else if (item.IsDeleted) {
                    deletedEntities.Add(item.Current);
                }
                else if (item.IsPossiblyModified) {
                    item.SynchDependentData();
                    if (item.IsModified) {
                        changedEntities.Add(item.Current);
                    }
                }
            }

            return new ChangeSet(newEntities.AsReadOnly(), deletedEntities.AsReadOnly(), changedEntities.AsReadOnly());
        }

        // verify that primary key and db-generated values have not changed
        private static void CheckForInvalidChanges(TrackedObject tracked) {
            foreach (MetaDataMember mem in tracked.Type.PersistentDataMembers) {
                if (mem.IsPrimaryKey || mem.IsDbGenerated || mem.IsVersion) {
                    if (tracked.HasChangedValue(mem)) {
                        if (mem.IsPrimaryKey) {
                            throw Error.IdentityChangeNotAllowed(mem.Name, tracked.Type.Name);
                        }
                        else {
                            throw Error.DbGeneratedChangeNotAllowed(mem.Name, tracked.Type.Name);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Create an ChangeConflictException with the best message
        /// </summary>       
        static private ChangeConflictException CreateChangeConflictException(int totalUpdatesAttempted, int failedUpdates) {
            string msg = Strings.RowNotFoundOrChanged;
            if (totalUpdatesAttempted > 1) {
                msg = Strings.UpdatesFailedMessage(failedUpdates, totalUpdatesAttempted);
            }
            return new ChangeConflictException(msg);
        }

        internal void TrackUntrackedObjects() {
            Dictionary<object, object> visited = new Dictionary<object, object>();

            // search for untracked new objects
            List<TrackedObject> items = new List<TrackedObject>(this.tracker.GetInterestingObjects());
            foreach (TrackedObject item in items) {
                this.TrackUntrackedObjects(item.Type, item.Current, visited);
            }
        }
        
        internal void ApplyInferredDeletions() {
            foreach (TrackedObject item in this.tracker.GetInterestingObjects()) {
                if (item.CanInferDelete()) {
                    // based on DeleteOnNull specifications on the item's associations,
                    // a deletion can be inferred for this item.  The actual state transition
                    // is dependent on the current item state.
                    if (item.IsNew) {
                        item.ConvertToRemoved();
                    }
                    else if (item.IsPossiblyModified || item.IsModified) {
                        item.ConvertToDeleted();
                    }
                }
            }
        }

        private void TrackUntrackedObjects(MetaType type, object item, Dictionary<object, object> visited) {
            if (!visited.ContainsKey(item)) {
                visited.Add(item, item);
                TrackedObject tracked = this.tracker.GetTrackedObject(item);
                if (tracked == null) {
                    tracked = this.tracker.Track(item);
                    tracked.ConvertToNew();
                }
                else if (tracked.IsDead || tracked.IsRemoved) {
                    // ignore
                    return;
                }

                // search parents (objects we are dependent on)
                foreach (RelatedItem parent in this.services.GetParents(type, item)) {
                    this.TrackUntrackedObjects(parent.Type, parent.Item, visited);
                }

                // synch up primary key
                if (tracked.IsNew) {
                    tracked.InitializeDeferredLoaders();

                    if (!tracked.IsPendingGeneration(tracked.Type.IdentityMembers)) {
                        tracked.SynchDependentData();
                        object cached = this.services.InsertLookupCachedObject(tracked.Type, item);
                        if (cached != item) {
                            TrackedObject cachedTracked = this.tracker.GetTrackedObject(cached);
                            Debug.Assert(cachedTracked != null);
                            if (cachedTracked.IsDeleted || cachedTracked.CanInferDelete()) {
                                // adding new object with same ID as object being deleted.. turn into modified
                                tracked.ConvertToPossiblyModified(cachedTracked.Original);
                                // turn deleted to dead...
                                cachedTracked.ConvertToDead();

                                this.services.RemoveCachedObjectLike(tracked.Type, item);
                                this.services.InsertLookupCachedObject(tracked.Type, item);
                            }
                            else if (!cachedTracked.IsDead) {
                                throw new DuplicateKeyException(item, Strings.CantAddAlreadyExistingKey);
                            }
                        }
                    }
                    else {
                        // we may have a generated PK, however we set the PK on this new item to 
                        // match a deleted item
                        object cached = this.services.GetCachedObjectLike(tracked.Type, item);
                        if (cached != null) {
                            TrackedObject cachedTracked = this.tracker.GetTrackedObject(cached);
                            Debug.Assert(cachedTracked != null);
                            if (cachedTracked.IsDeleted || cachedTracked.CanInferDelete()) {
                                // adding new object with same ID as object being deleted.. turn into modified
                                tracked.ConvertToPossiblyModified(cachedTracked.Original);
                                // turn deleted to dead...
                                cachedTracked.ConvertToDead();

                                this.services.RemoveCachedObjectLike(tracked.Type, item);
                                this.services.InsertLookupCachedObject(tracked.Type, item);
                            }
                        }
                    }
                }

                // search children (objects that are dependent on us)
                foreach (RelatedItem child in this.services.GetChildren(type, item)) {
                    this.TrackUntrackedObjects(child.Type, child.Item, visited);
                }
            }
        }
        
        internal void ObserveUntrackedObjects() {
            Dictionary<object, object> visited = new Dictionary<object, object>();

            List<TrackedObject> items = new List<TrackedObject>(this.tracker.GetInterestingObjects());
            foreach (TrackedObject item in items) {
                this.ObserveUntrackedObjects(item.Type, item.Current, visited);
            }
        }

        private void ObserveUntrackedObjects(MetaType type, object item, Dictionary<object, object> visited) {
            if (!visited.ContainsKey(item)) {
                visited.Add(item, item);
                TrackedObject tracked = this.tracker.GetTrackedObject(item);
                if (tracked == null) {
                    tracked = this.tracker.Track(item);
                    tracked.ConvertToNew();
                } else if (tracked.IsDead || tracked.IsRemoved) {
                    // ignore
                    return;
                }

                // search parents (objects we are dependent on)
                foreach (RelatedItem parent in this.services.GetParents(type, item)) {
                    this.ObserveUntrackedObjects(parent.Type, parent.Item, visited);
                }

                // synch up primary key unless its generated.
                if (tracked.IsNew) {
                     if (!tracked.IsPendingGeneration(tracked.Type.IdentityMembers)) {
                        tracked.SynchDependentData();
                     } 
                }

                // search children (objects that are dependent on us)
                foreach (RelatedItem child in this.services.GetChildren(type, item)) {
                    this.ObserveUntrackedObjects(child.Type, child.Item, visited);
                }
            }
        }

        private TrackedObject GetOtherItem(MetaAssociation assoc, object instance) {
            if (instance == null)
                return null;
            object other = null;
            // Don't load unloaded references
            if (assoc.ThisMember.StorageAccessor.HasAssignedValue(instance) ||
                assoc.ThisMember.StorageAccessor.HasLoadedValue(instance)
                ) {
                other = assoc.ThisMember.MemberAccessor.GetBoxedValue(instance);
            }
            else if (assoc.OtherKeyIsPrimaryKey) {
                // Maybe it's in the cache, but not yet attached through reference.
                object[] foreignKeys = CommonDataServices.GetForeignKeyValues(assoc, instance);
                other = this.services.GetCachedObject(assoc.OtherType, foreignKeys);
            }
            // else the other key is not the primary key so there is no way to try to look it up
            return (other != null) ? this.tracker.GetTrackedObject(other) : null;
        }

        private bool HasAssociationChanged(MetaAssociation assoc, TrackedObject item) {
            if (item.Original != null && item.Current != null) {
                if (assoc.ThisMember.StorageAccessor.HasAssignedValue(item.Current) ||
                    assoc.ThisMember.StorageAccessor.HasLoadedValue(item.Current)
                    ) {
                    return this.GetOtherItem(assoc, item.Current) != this.GetOtherItem(assoc, item.Original);
                }
                else {
                    object[] currentFKs = CommonDataServices.GetForeignKeyValues(assoc, item.Current);
                    object[] originaFKs = CommonDataServices.GetForeignKeyValues(assoc, item.Original);
                    for (int i = 0, n = currentFKs.Length; i < n; i++) {
                        if (!object.Equals(currentFKs[i], originaFKs[i]))
                            return true;
                    }
                }
            }
            return false;
        }

        private void BuildEdgeMaps() {
            this.currentParentEdges.Clear();
            this.originalChildEdges.Clear();
            this.originalChildReferences.Clear();

            List<TrackedObject> list = new List<TrackedObject>(this.tracker.GetInterestingObjects());
            foreach (TrackedObject item in list) {
                bool isNew = item.IsNew;
                MetaType mt = item.Type;
                foreach (MetaAssociation assoc in mt.Associations) {
                    if (assoc.IsForeignKey) {
                        TrackedObject otherItem = this.GetOtherItem(assoc, item.Current);
                        TrackedObject dbOtherItem = this.GetOtherItem(assoc, item.Original);
                        bool pointsToDeleted = (otherItem != null && otherItem.IsDeleted) || (dbOtherItem != null && dbOtherItem.IsDeleted);
                        bool pointsToNew = (otherItem != null && otherItem.IsNew);

                        if (isNew || pointsToDeleted || pointsToNew || this.HasAssociationChanged(assoc, item)) {
                            if (otherItem != null) {
                                this.currentParentEdges.Add(assoc, item, otherItem);
                            }
                            if (dbOtherItem != null) {
                                if (assoc.IsUnique) {
                                    this.originalChildEdges.Add(assoc, dbOtherItem, item);
                                }
                                this.originalChildReferences.Add(dbOtherItem, item);
                            }
                        }
                    }
                }
            }
        }

        enum VisitState {
            Before,
            After
        }

        private List<TrackedObject> GetOrderedList() {
            var objects = this.tracker.GetInterestingObjects().ToList();

            // give list an initial order (most likely correct order) to avoid deadlocks in server
            var range = Enumerable.Range(0, objects.Count).ToList();
            range.Sort((int x, int y) => Compare(objects[x], x, objects[y], y));
            var ordered = range.Select(i => objects[i]).ToList();

            // permute order if constraint dependencies requires some changes to come before others
            var visited = new Dictionary<TrackedObject, VisitState>();
            var list = new List<TrackedObject>();
            foreach (TrackedObject item in ordered) {
                this.BuildDependencyOrderedList(item, list, visited);
            }
            return list;
        }

        private static int Compare(TrackedObject x, int xOrdinal, TrackedObject y, int yOrdinal) {
            // deal with possible nulls
            if (x == y) {
                return 0;
            }
            if (x == null) {
                return -1;
            }
            else if (y == null) {
                return 1;
            }
            // first order by action: Inserts first, Updates, Deletes last
            int xAction = x.IsNew ? 0 : x.IsDeleted ? 2 : 1;
            int yAction = y.IsNew ? 0 : y.IsDeleted ? 2 : 1;
            if (xAction < yAction) {
                return -1;
            }
            else if (xAction > yAction) {
                return 1;
            }
            // no need to order inserts (PK's may not even exist)
            if (x.IsNew) {
                // keep original order
                return xOrdinal.CompareTo(yOrdinal);
            }
            // second order by type
            if (x.Type != y.Type) {
                return string.CompareOrdinal(x.Type.Type.FullName, y.Type.Type.FullName);
            }
            // lastly, order by PK values
            int result = 0;
            foreach (MetaDataMember mm in x.Type.IdentityMembers) {
                object xValue = mm.StorageAccessor.GetBoxedValue(x.Current);
                object yValue = mm.StorageAccessor.GetBoxedValue(y.Current);
                if (xValue == null) {
                    if (yValue != null) {
                        return -1;
                    }
                }
                else {
                    IComparable xc = xValue as IComparable;
                    if (xc != null) {
                        result = xc.CompareTo(yValue);
                        if (result != 0) {
                            return result;
                        }
                    }
                }
            }
            // they are the same? leave in original order
            return xOrdinal.CompareTo(yOrdinal);
        }

        private void BuildDependencyOrderedList(TrackedObject item, List<TrackedObject> list, Dictionary<TrackedObject, VisitState> visited) {
            VisitState state;
            if (visited.TryGetValue(item, out state)) {
                if (state == VisitState.Before) {
                    throw Error.CycleDetected();
                }
                return;
            }

            visited[item] = VisitState.Before;

            if (item.IsInteresting) {
                if (item.IsDeleted) {
                    // if 'item' is deleted
                    //    all objects that used to refer to 'item' must be ordered before item
                    foreach (TrackedObject other in this.originalChildReferences[item]) {
                        if (other != item) {
                            this.BuildDependencyOrderedList(other, list, visited);
                        }
                    }
                }
                else {
                    // if 'item' is new or changed
                    //   for all objects 'other' that 'item' refers to along association 'assoc'
                    //      if 'other' is new then 'other' must be ordered before 'item'
                    //      if 'assoc' is pure one-to-one and some other item 'prevItem' used to refer to 'other'
                    //         then 'prevItem' must be ordered before 'item'
                    foreach (MetaAssociation assoc in item.Type.Associations) {
                        if (assoc.IsForeignKey) {
                            TrackedObject other = this.currentParentEdges[assoc, item];
                            if (other != null) {
                                if (other.IsNew) {
                                    // if other is new, visit other first (since item's FK depends on it)
                                    if (other != item || item.Type.DBGeneratedIdentityMember != null) {
                                        this.BuildDependencyOrderedList(other, list, visited);
                                    }
                                }
                                else if ((assoc.IsUnique || assoc.ThisKeyIsPrimaryKey)) {
                                    TrackedObject prevItem = this.originalChildEdges[assoc, other];
                                    if (prevItem != null && other != item) {
                                        this.BuildDependencyOrderedList(prevItem, list, visited);
                                    }
                                }
                            }
                        }
                    }
                }

                list.Add(item);
            }

            visited[item] = VisitState.After;
        }

        class EdgeMap {
            Dictionary<MetaAssociation, Dictionary<TrackedObject, TrackedObject>> associations;

            internal EdgeMap() {
                this.associations = new Dictionary<MetaAssociation, Dictionary<TrackedObject, TrackedObject>>();
            }

            internal void Add(MetaAssociation assoc, TrackedObject from, TrackedObject to) {
                Dictionary<TrackedObject, TrackedObject> pairs;
                if (!associations.TryGetValue(assoc, out pairs)) {
                    pairs = new Dictionary<TrackedObject, TrackedObject>();
                    associations.Add(assoc, pairs);
                }
                pairs.Add(from, to);
            }

            internal TrackedObject this[MetaAssociation assoc, TrackedObject from] {
                get {
                    Dictionary<TrackedObject, TrackedObject> pairs;
                    if (associations.TryGetValue(assoc, out pairs)) {
                        TrackedObject to;
                        if (pairs.TryGetValue(from, out to)) {
                            return to;
                        }
                    }
                    return null;
                }
            }
            internal void Clear() {
                this.associations.Clear();
            }
        }

        class ReferenceMap {
            Dictionary<TrackedObject, List<TrackedObject>> references;

            internal ReferenceMap() {
                this.references = new Dictionary<TrackedObject, List<TrackedObject>>();
            }

            internal void Add(TrackedObject from, TrackedObject to) {
                List<TrackedObject> refs;
                if (!references.TryGetValue(from, out refs)) {
                    refs = new List<TrackedObject>();
                    references.Add(from, refs);
                }
                if (!refs.Contains(to))
                    refs.Add(to);
            }

            internal IEnumerable<TrackedObject> this[TrackedObject from] {
                get {
                    List<TrackedObject> refs;
                    if (references.TryGetValue(from, out refs)) {
                        return refs;
                    }
                    return Empty;
                }
            }

            internal void Clear() {
                this.references.Clear();
            }

            private static TrackedObject[] Empty = new TrackedObject[] { };
        }
    }
}