summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/dynamic.py
blob: af1be28bc6bfe5243ed2d83b6ef1564cf1edde3c (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
"""'dynamic' collection API.  returns Query() objects on the 'read' side, alters
a special AttributeHistory on the 'write' side."""

from sqlalchemy import exceptions, util
from sqlalchemy.orm import attributes, object_session, util as mapperutil
from sqlalchemy.orm.query import Query
from sqlalchemy.orm.mapper import has_identity, object_mapper

class DynamicAttributeImpl(attributes.AttributeImpl):
    def __init__(self, class_, key, typecallable, target_mapper, **kwargs):
        super(DynamicAttributeImpl, self).__init__(class_, key, typecallable, **kwargs)
        self.target_mapper = target_mapper

    def get(self, state, passive=False):
        if passive:
            return self._get_collection(state, passive=True).added_items
        else:
            return AppenderQuery(self, state)

    def commit_to_state(self, state, value=attributes.NO_VALUE):
        # we have our own AttributeHistory therefore dont need CommittedState
        # instead, we reset the history stored on the attribute
        state.dict[self.key] = CollectionHistory(self, state)

    def get_collection(self, state, user_data=None):
        return self._get_collection(state, passive=True).added_items
        
    def set(self, state, value, initiator):
        if initiator is self:
            return

        old_collection = self.get(state).assign(value)

        # TODO: emit events ???
        state.modified = True

    def delete(self, *args, **kwargs):
        raise NotImplementedError()
        
    def get_history(self, state, passive=False):
        c = self._get_collection(state, passive)
        return (c.added_items, c.unchanged_items, c.deleted_items)
        
    def _get_collection(self, state, passive=False):
        try:
            c = state.dict[self.key]
        except KeyError:
            state.dict[self.key] = c = CollectionHistory(self, state)

        if not passive:
            return CollectionHistory(self, state, apply_to=c)
        else:
            return c
        
    def append(self, state, value, initiator, passive=False):
        if initiator is not self:
            self._get_collection(state, passive=True).added_items.append(value)
            self.fire_append_event(state, value, initiator)
    
    def remove(self, state, value, initiator, passive=False):
        if initiator is not self:
            self._get_collection(state, passive=True).deleted_items.append(value)
            self.fire_remove_event(state, value, initiator)

            
class AppenderQuery(Query):
    def __init__(self, attr, state):
        super(AppenderQuery, self).__init__(attr.target_mapper, None)
        self.state = state
        self.attr = attr
    
    def __session(self):
        instance = self.state.obj()
        sess = object_session(instance)
        if sess is not None and self.autoflush and sess.autoflush and instance in sess:
            sess.flush()
        if not has_identity(instance):
            return None
        else:
            return sess
    
    def _get_session(self):
        return self.__session()
    session = property(_get_session)
    
    def __iter__(self):
        sess = self.__session()
        if sess is None:
            return iter(self.attr._get_collection(self.state, passive=True).added_items)
        else:
            return iter(self._clone(sess))

    def __getitem__(self, index):
        sess = self.__session()
        if sess is None:
            return self.attr._get_collection(self.state, passive=True).added_items.__getitem__(index)
        else:
            return self._clone(sess).__getitem__(index)
    
    def count(self):
        sess = self.__session()
        if sess is None:
            return len(self.attr._get_collection(self.state, passive=True).added_items)
        else:
            return self._clone(sess).count()
    
    def _clone(self, sess=None):
        # note we're returning an entirely new Query class instance here
        # without any assignment capabilities;
        # the class of this query is determined by the session.
        instance = self.state.obj()
        if sess is None:
            sess = object_session(instance)
            if sess is None:
                try:
                    sess = object_mapper(instance).get_session()
                except exceptions.InvalidRequestError:
                    raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (mapperutil.instance_str(instance), self.attr.key))

        return sess.query(self.attr.target_mapper).with_parent(instance)

    def assign(self, collection):
        instance = self.state.obj()
        if has_identity(instance):
            oldlist = list(self)
        else:
            oldlist = []
        self.attr._get_collection(self.state, passive=True).replace(oldlist, collection)
        return oldlist
        
    def append(self, item):
        self.attr.append(self.state, item, None)

    def remove(self, item):
        self.attr.remove(self.state, item, None)

            
class CollectionHistory(object): 
    """Overrides AttributeHistory to receive append/remove events directly."""

    def __init__(self, attr, state, apply_to=None):
        if apply_to:
            deleted = util.IdentitySet(apply_to.deleted_items)
            added = apply_to.added_items
            coll = AppenderQuery(attr, state).autoflush(False)
            self.unchanged_items = [o for o in util.IdentitySet(coll) if o not in deleted]
            self.added_items = apply_to.added_items
            self.deleted_items = apply_to.deleted_items
        else:
            self.deleted_items = []
            self.added_items = []
            self.unchanged_items = []
            
    def replace(self, olditems, newitems):
        self.added_items = newitems
        self.deleted_items = olditems