default is actually stable

authorSylvain Th?nault <sylvain.thenault@logilab.fr>
changesete1cc9656bb4c
branchstable
phasepublic
hiddenno
parent revision#bc901a7460d1 closes #71157: bad analyze when using functions, #1a9f3c52176b closes #70416: add 'having' list into variable's stinfo and properly update variable graph
child revision#6afe7acaf314 allow optional on final relation (rhs only) and in having expression (hence Comparison node gain a 'optional' attribute). Closes #71415
files modified by this revision
ChangeLog
analyze.py
nodes.py
stmts.py
test/unittest_analyze.py
test/unittest_stcheck.py
# HG changeset patch
# User Sylvain Thénault <sylvain.thenault@logilab.fr>
# Date 1311228667 -7200
# Thu Jul 21 08:11:07 2011 +0200
# Branch stable
# Node ID e1cc9656bb4c100976706435cf95fb25365e5f8c
# Parent 1a9f3c52176b91f876db8a2a6b4833b6ed4ae496
# Parent bc901a7460d10c3bcc21fb3517667cfc9db5eefe
default is actually stable

diff --git a/ChangeLog b/ChangeLog
@@ -1,13 +1,23 @@
1  ChangeLog for RQL
2  =================
3 
4  --
5 -    * remove_group_var renamed into remove_group_term and fixed implementation
6 +    * #70264: remove_group_var renamed into remove_group_term and fixed
7 +      implementation
8 +
9 +    * #70416: rql annotator add 'having' list into variable's stinfo, and
10 +      properly update variable graph
11 
12 -    * rql annotator add 'having' list into variable's stinfo, and
13 -      properly update variable graph
14 +    * #71131: as_string doesn't  propagate encoding/kwargs to subqueries
15 +
16 +    * #71132: column alias scope should be handled as variable scope, not bound
17 +      to subquery
18 +
19 +    * #71157: bad analyze when using functions
20 +
21 +    * Select.replace must properly reset old node's parent attribute
22 
23      * new undo_modification context manager on select nodes
24 
25  2011-06-09  --  0.29.0
26      * support != operator for non equality
diff --git a/analyze.py b/analyze.py
@@ -491,49 +491,37 @@
27          elif isinstance(rhs, nodes.Constant) and not rschema.final:
28              # rhs.type is None <-> NULL
29              if not isinstance(lhs, nodes.VariableRef) or rhs.type is None:
30                  return True
31              self._extract_constraint(constraints, lhs.name, rhs, rschema.subjects)
32 -        else:
33 -            if not isinstance(lhs, nodes.VariableRef):
34 -                # XXX: check relation is valid
35 -                return True
36 +        elif not isinstance(lhs, nodes.VariableRef):
37 +            # XXX: check relation is valid
38 +            return True
39 +        elif isinstance(rhs, nodes.VariableRef):
40              lhsvar = lhs.name
41 -            rhsvars = []
42 -            samevar = False
43 -            if not isinstance(rhs, nodes.MathExpression):
44 -                # rhs type is the result of the math expression, not of
45 -                # individual variables, so don't add constraints on rhs
46 -                # variables
47 -                for v in rhs.iget_nodes(nodes.VariableRef):
48 -                    if v.name == lhsvar:
49 -                        samevar = True
50 -                    else:
51 -                        rhsvars.append(v.name)
52 +            rhsvar = rhs.name
53              lhsdomain = constraints.domains[lhsvar]
54 -            if rhsvars:
55 -                s2 = '=='.join(rhsvars)
56 -                # filter according to domain necessary for column aliases
57 -                rhsdomain = constraints.domains[rhsvars[0]]
58 -                res = []
59 -                for fromtype, totypes in rschema.associations():
60 -                    if not fromtype in lhsdomain:
61 -                        continue
62 -                    ptypes = [str(t) for t in totypes if t in rhsdomain]
63 -                    res.append( [ ( [lhsvar], [str(fromtype)]), (rhsvars, ptypes) ] )
64 -                constraints.or_and( res )
65 -            else:
66 -                ptypes = [str(subj) for subj in rschema.subjects()
67 -                          if subj in lhsdomain]
68 -                constraints.var_has_types( lhsvar, ptypes )
69 -            if samevar:
70 -                res = []
71 -                for fromtype, totypes in rschema.associations():
72 -                    if not (fromtype in totypes and fromtype in lhsdomain):
73 -                        continue
74 -                    res.append(str(fromtype))
75 +            # filter according to domain necessary for column aliases
76 +            rhsdomain = constraints.domains[rhsvar]
77 +            res = []
78 +            for fromtype, totypes in rschema.associations():
79 +                if not fromtype in lhsdomain:
80 +                    continue
81 +                ptypes = [str(t) for t in totypes if t in rhsdomain]
82 +                res.append( [ ([lhsvar], [str(fromtype)]),
83 +                              ([rhsvar], ptypes) ] )
84 +            constraints.or_and(res)
85 +            if rhsvar == lhsvar:
86 +                res = [str(fromtype) for fromtype, totypes in rschema.associations()
87 +                       if (fromtype in totypes and fromtype in lhsdomain)]
88                  constraints.var_has_types( lhsvar, res )
89 +        else:
90 +            # XXX consider rhs.get_type?
91 +            lhsdomain = constraints.domains[lhs.name]
92 +            ptypes = [str(subj) for subj in rschema.subjects()
93 +                      if subj in lhsdomain]
94 +            constraints.var_has_types( lhs.name, ptypes )
95          return True
96 
97      def visit_type_restriction(self, relation, constraints):
98          lhs, rhs = relation.get_parts()
99          etypes = set(c.value for c in rhs.iget_nodes(nodes.Constant)
diff --git a/nodes.py b/nodes.py
@@ -193,10 +193,11 @@
100 
101      def add_type_restriction(self, var, etype):
102          """builds a restriction node to express : variable is etype"""
103          return self.add_constant_restriction(var, 'is', etype, 'etype')
104 
105 +
106  # base RQL nodes ##############################################################
107 
108  class SubQuery(BaseNode):
109      """WITH clause"""
110      __slots__ = ('aliases', 'query')
@@ -222,11 +223,11 @@
111      def children(self):
112          return self.aliases + [self.query]
113 
114      def as_string(self, encoding=None, kwargs=None):
115          return '%s BEING (%s)' % (','.join(v.name for v in self.aliases),
116 -                                  self.query.as_string())
117 +                                  self.query.as_string(encoding, kwargs))
118      def __repr__(self):
119          return '%s BEING (%s)' % (','.join(repr(v) for v in self.aliases),
120                                    repr(self.query))
121 
122  class And(BinaryNode):
@@ -823,19 +824,25 @@
123 
124 
125  ###############################################################################
126 
127  class Referenceable(object):
128 -    __slots__ = ('name', 'stinfo')
129 +    __slots__ = ('name', 'stinfo', 'stmt')
130 
131      def __init__(self, name):
132          self.name = name.strip().encode()
133          # used to collect some global information about the syntax tree
134          self.stinfo = {
135              # link to VariableReference objects in the syntax tree
136              'references': set(),
137              }
138 +        # reference to the selection
139 +        self.stmt = None
140 +
141 +    @property
142 +    def schema(self):
143 +        return self.stmt.root.schema
144 
145      def init_copy(self, old):
146          # should copy variable's possibletypes on copy
147          if not self.stinfo.get('possibletypes'):
148              self.stinfo['possibletypes'] = old.stinfo.get('possibletypes')
@@ -860,10 +867,11 @@
149          """return all references on this variable"""
150          return tuple(self.stinfo['references'])
151 
152      def prepare_annotation(self):
153          self.stinfo.update({
154 +            'scope': None,
155              # relations where this variable is used on the lhs/rhs
156              'relations': set(),
157              'rhsrelations': set(),
158              # selection indexes if any
159              'selected': set(),
@@ -880,10 +888,22 @@
160              })
161          # remove optional st infos
162          for key in ('optrelations', 'blocsimplification', 'ftirels'):
163              self.stinfo.pop(key, None)
164 
165 +    def _set_scope(self, key, scopenode):
166 +        if scopenode is self.stmt or self.stinfo[key] is None:
167 +            self.stinfo[key] = scopenode
168 +        elif not (self.stinfo[key] is self.stmt or scopenode is self.stinfo[key]):
169 +            self.stinfo[key] = common_parent(self.stinfo[key], scopenode).scope
170 +
171 +    def set_scope(self, scopenode):
172 +        self._set_scope('scope', scopenode)
173 +    def get_scope(self):
174 +        return self.stinfo['scope']
175 +    scope = property(get_scope, set_scope)
176 +
177      def add_optional_relation(self, relation):
178          try:
179              self.stinfo['optrelations'].add(relation)
180          except KeyError:
181              self.stinfo['optrelations'] = set((relation,))
@@ -983,14 +1003,10 @@
182          self.query = query
183 
184      def __repr__(self):
185          return 'alias %s(%#X)' % (self.name, id(self))
186 
187 -    @property
188 -    def schema(self):
189 -        return self.query.root.schema
190 -
191      def get_type(self, solution=None, kwargs=None):
192          """return entity type of this object, 'Any' if not found"""
193          vtype = super(ColumnAlias, self).get_type(solution, kwargs)
194          if vtype == 'Any':
195              for select in self.query.children:
@@ -1011,55 +1027,23 @@
196                      vtypes.add(vtype)
197              if vtypes:
198                  return ', '.join(sorted(vtype for vtype in vtypes))
199          return vtype
200 
201 -    def set_scope(self, scopenode):
202 -        pass
203 -    def get_scope(self):
204 -        return self.query
205 -    scope = property(get_scope, set_scope)
206 -
207 
208  class Variable(Referenceable):
209      """
210      a variable definition, should not be directly added to the syntax tree (use
211      VariableRef instead)
212 
213      collects information about a variable use in a syntax tree
214      """
215 -    __slots__ = ('stmt',
216 -                 '_q_invariant', '_q_sql', '_q_sqltable') # XXX ginco specific
217 -
218 -    def __init__(self, name):
219 -        super(Variable, self).__init__(name)
220 -        # reference to the selection
221 -        self.stmt = None
222 +    __slots__ = ('_q_invariant', '_q_sql', '_q_sqltable') # XXX ginco specific
223 
224      def __repr__(self):
225          return '%s(%#X)' % (self.name, id(self))
226 
227 -    @property
228 -    def schema(self):
229 -        return self.stmt.root.schema
230 -
231 -    def prepare_annotation(self):
232 -        super(Variable, self).prepare_annotation()
233 -        self.stinfo['scope'] = None
234 -
235 -    def _set_scope(self, key, scopenode):
236 -        if scopenode is self.stmt or self.stinfo[key] is None:
237 -            self.stinfo[key] = scopenode
238 -        elif not (self.stinfo[key] is self.stmt or scopenode is self.stinfo[key]):
239 -            self.stinfo[key] = common_parent(self.stinfo[key], scopenode).scope
240 -
241 -    def set_scope(self, scopenode):
242 -        self._set_scope('scope', scopenode)
243 -    def get_scope(self):
244 -        return self.stinfo['scope']
245 -    scope = property(get_scope, set_scope)
246 -
247      def valuable_references(self):
248          """return the number of "valuable" references :
249          references is in selection or in a non type (is) relations
250          """
251          stinfo = self.stinfo
diff --git a/stmts.py b/stmts.py
@@ -482,11 +482,11 @@
252              s.append('OFFSET %s' % self.offset)
253          if self.where is not None:
254              s.append('WHERE ' + as_string(self.where))
255          if self.having:
256              s.append('HAVING ' + ','.join(as_string(term)
257 -                                           for term in self.having))
258 +                                          for term in self.having))
259          if self.with_:
260              s.append('WITH ' + ','.join(as_string(term)
261                                          for term in self.with_))
262          if self.distinct:
263              return 'DISTINCT Any ' + ' '.join(s)
@@ -616,10 +616,11 @@
264          """
265          if name in self.aliases:
266              return self.aliases[name]
267          if colnum is not None: # take care, may be 0
268              self.aliases[name] = calias = nodes.ColumnAlias(name, colnum)
269 +            calias.stmt = self
270              # alias may already have been used as a regular variable, replace it
271              if name in self.defined_vars:
272                  var = self.defined_vars.pop(name)
273                  calias.stinfo['references'] = var.stinfo['references']
274                  for vref in var.references():
@@ -706,12 +707,11 @@
275              self.having[self.having.index(oldnode)] = newnode
276          else:
277              raise Exception('duh XXX %s' % oldnode)
278          # XXX no undo/reference support 'by design' (eg breaks things if you add
279          # it...)
280 -        # XXX resetting oldnode parent cause pb with cw.test_views (w/ facets)
281 -        #oldnode.parent = None
282 +        oldnode.parent = None
283          newnode.parent = self
284          return oldnode, self, None
285 
286      def remove(self, node):
287          if node is self.where:
diff --git a/test/unittest_analyze.py b/test/unittest_analyze.py
@@ -529,11 +529,11 @@
288          sols = sorted(node.solutions)
289          self.assertEqual(sols, [{'P': 'Person', 'S': 'Company', 'N': 'Int'},
290                                  {'P': 'Student', 'S': 'Company', 'N': 'Int'}])
291 
292 
293 -    def test_nongrer_not_u_ownedby_u(self):
294 +    def test_nonregr_not_u_ownedby_u(self):
295          node = self.helper.parse('Any U WHERE NOT U owned_by U')
296          self.helper.compute_solutions(node, debug=DEBUG)
297          sols = sorted(node.children[0].solutions)
298          self.assertEqual(sols, [{'U': 'Person'}])
299 
diff --git a/test/unittest_stcheck.py b/test/unittest_stcheck.py
@@ -312,10 +312,12 @@
300      def test_subquery_annotation_2(self):
301          rqlst = self.parse('Any X,ET WITH X,ET BEING (Any X, ET WHERE C is ET, EXISTS(X work_for C))').children[0]
302          C = rqlst.with_[0].query.children[0].defined_vars['C']
303          self.failUnless(C.scope is rqlst.with_[0].query.children[0], C.scope)
304          self.assertEqual(len(C.stinfo['relations']), 2)
305 +        X = rqlst.get_variable('X')
306 +        self.failUnless(X.scope is rqlst, X.scope)
307 
308      def test_no_attr_var_if_uid_rel(self):
309          with self.assertRaises(BadRQLQuery) as cm:
310              self.parse('Any X, Y WHERE X work_for Z, Y work_for Z, X eid > Y')
311          self.assertEqual(str(cm.exception), 'variable Y should not be used as rhs of attribute relation X eid > Y')