author | Sylvain Thénault <sylvain.thenault@logilab.fr> |
Fri, 09 Dec 2011 12:12:56 +0100 | |
branch | stable |
changeset 678 | a031d85966f1 |
parent 666 | 8828046c2d3b (current diff) |
parent 676 | 601279ab9600 (diff) |
child 679 | de1bbfec40c3 |
--- a/.hgtags Tue Oct 11 14:14:44 2011 +0200 +++ b/.hgtags Fri Dec 09 12:12:56 2011 +0100 @@ -69,3 +69,5 @@ c3ae2279fe701809096d5f6bc50f8d8b4a9fd4fd rql-debian-version-0.30.0-1 3c17b96750ad5045bd1fb43241da55c81c094bd4 rql-version-0.30.1 13cd741f8e144265c15bb225fad863234fc8ce16 rql-debian-version-0.30.1-1 +bb70a998ced6a839fd1e28686e4ff5254feefde3 rql-version-0.31.0 +f4f27e4c588e40ba9c6c9c5d92cb22f74f1207a6 rql-debian-version-0.31.0-1
--- a/ChangeLog Tue Oct 11 14:14:44 2011 +0200 +++ b/ChangeLog Fri Dec 09 12:12:56 2011 +0100 @@ -3,7 +3,11 @@ -- * #78681: don't crash on column aliases used in outer join - + * #81394: HAVING support in write queries (INSERT,SET,DELETE) + * #80799: fix wrong type analysis with 'NOT identity' + * when possible, use entity type as translation context of relation + (break cw < 3.13.10 compat) + * #81817: fix add_type_restriction for cases where some types restriction is already in there 2011-09-07 -- 0.30.1
--- a/__pkginfo__.py Tue Oct 11 14:14:44 2011 +0200 +++ b/__pkginfo__.py Fri Dec 09 12:12:56 2011 +0100 @@ -20,7 +20,7 @@ __docformat__ = "restructuredtext en" modname = "rql" -numversion = (0, 30, 1) +numversion = (0, 31, 0) version = '.'.join(str(num) for num in numversion) license = 'LGPL'
--- a/analyze.py Tue Oct 11 14:14:44 2011 +0200 +++ b/analyze.py Fri Dec 09 12:12:56 2011 +0100 @@ -469,9 +469,11 @@ """extract constraints for an relation according to it's type""" if relation.is_types_restriction(): self.visit_type_restriction(relation, constraints) - return True + return None rtype = relation.r_type lhs, rhs = relation.get_parts() + if rtype == 'identity' and relation.neged(strict=True): + return None if rtype in self.uid_func_mapping: if isinstance(relation.parent, nodes.Not) or relation.operator() != '=': # non final entity types @@ -480,22 +482,22 @@ etypes = self._uid_node_types(rhs) if etypes: constraints.var_has_types( lhs.name, etypes ) - return True + return None if isinstance(rhs, nodes.Comparison): rhs = rhs.children[0] rschema = self.schema.rschema(rtype) if isinstance(lhs, nodes.Constant): # lhs is a constant node (simplified tree) if not isinstance(rhs, nodes.VariableRef): - return True + return None self._extract_constraint(constraints, rhs.name, lhs, rschema.objects) elif isinstance(rhs, nodes.Constant) and not rschema.final: # rhs.type is None <-> NULL if not isinstance(lhs, nodes.VariableRef) or rhs.type is None: - return True + return None self._extract_constraint(constraints, lhs.name, rhs, rschema.subjects) elif not isinstance(lhs, nodes.VariableRef): # XXX: check relation is valid - return True + return None elif isinstance(rhs, nodes.VariableRef): lhsvar = lhs.name rhsvar = rhs.name @@ -520,7 +522,7 @@ ptypes = [str(subj) for subj in rschema.subjects() if subj in lhsdomain] constraints.var_has_types( lhs.name, ptypes ) - return True + return None def visit_type_restriction(self, relation, constraints): lhs, rhs = relation.get_parts()
--- a/debian/changelog Tue Oct 11 14:14:44 2011 +0200 +++ b/debian/changelog Fri Dec 09 12:12:56 2011 +0100 @@ -1,3 +1,9 @@ +rql (0.31.0-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Wed, 09 Nov 2011 18:18:01 +0100 + rql (0.30.1-1) unstable; urgency=low * new upstream release
--- a/debian/control Tue Oct 11 14:14:44 2011 +0200 +++ b/debian/control Fri Dec 09 12:12:56 2011 +0100 @@ -12,7 +12,7 @@ Architecture: any XB-Python-Version: ${python:Versions} Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends}, python-logilab-common (>= 0.35.3-1), yapps2-runtime, python-logilab-database (>= 1.6.0) -Conflicts: cubicweb-common (<= 3.8.3) +Conflicts: cubicweb-common (<= 3.13.9) Provides: ${python:Provides} Description: relationship query language (RQL) utilities A library providing the base utilities to handle RQL queries,
--- a/nodes.py Tue Oct 11 14:14:44 2011 +0200 +++ b/nodes.py Fri Dec 09 12:12:56 2011 +0100 @@ -30,7 +30,7 @@ from logilab.database import DYNAMIC_RTYPE -from rql import CoercionError +from rql import CoercionError, RQLException from rql.base import BaseNode, Node, BinaryNode, LeafNode from rql.utils import (function_description, quote, uquote, build_visitor_stub, common_parent) @@ -218,6 +218,39 @@ def add_type_restriction(self, var, etype): """builds a restriction node to express : variable is etype""" + typerel = var.stinfo.get('typerel', None) + if typerel: + istarget = typerel.children[1].children[0] + if typerel.r_type == 'is': + if isinstance(istarget, Constant): + etypes = (istarget.value,) + else: # Function (IN) + etypes = [et.value for et in istarget.children] + if etype not in etypes: + raise RQLException('%r not in %r' % (etype, etypes)) + if len(etypes) > 1: + for child in istarget.children: + if child.value != etype: + istarget.remove(child) + else: + # let's botte en touche IN cases (who would do that anyway ?) + if isinstance(istarget, Function): + msg = 'adding type restriction over is_instance_of IN is not supported' + raise NotImplementedError(msg) + schema = self.root.schema + if schema is None: + msg = 'restriction with is_instance_of cannot be done without a schema' + raise RQLException(msg) + # let's check the restriction is compatible + eschema = schema[etype] + ancestors = set(eschema.ancestors()) + ancestors.add(etype) # let's be unstrict + if istarget.value in ancestors: + istarget.value = etype + else: + raise RQLException('type restriction %s-%s cannot be made on %s' % + (var, etype, self)) + return typerel return self.add_constant_restriction(var, 'is', etype, 'etype') @@ -956,7 +989,9 @@ if solution: return solution[self.name] if self.stinfo['typerel']: - return str(self.stinfo['typerel'].children[1].children[0].value) + rhs = self.stinfo['typerel'].children[1].children[0] + if isinstance(rhs, Constant): + return str(rhs.value) schema = self.schema if schema is not None: for rel in self.stinfo['rhsrelations']: @@ -997,12 +1032,17 @@ if mainindex is not None: # relation to the main variable, stop searching lhsvar = getattr(lhs, 'variable', None) + context = None if lhsvar is not None and mainindex in lhsvar.stinfo['selected']: - return tr(rtype) + if len(lhsvar.stinfo['possibletypes']) == 1: + context = iter(lhsvar.stinfo['possibletypes']).next() + return tr(rtype, context=context) if rhsvar is not None and mainindex in rhsvar.stinfo['selected']: + if len(rhsvar.stinfo['possibletypes']) == 1: + context = iter(rhsvar.stinfo['possibletypes']).next() if schema is not None and rschema.symmetric: - return tr(rtype) - return tr(rtype + '_object') + return tr(rtype, context=context) + return tr(rtype + '_object', context=context) if rhsvar is self: rtype += '_object' if frtype is not None:
--- a/parser.g Tue Oct 11 14:14:44 2011 +0200 +++ b/parser.g Fri Dec 09 12:12:56 2011 +0100 @@ -119,9 +119,9 @@ #// Deletion ################################################################### -rule _delete<<R>>: decl_rels<<R>> where<<R>> {{ return R }} +rule _delete<<R>>: decl_rels<<R>> where<<R>> having<<R>> {{ return R }} - | decl_vars<<R>> where<<R>> {{ return R }} + | decl_vars<<R>> where<<R>> having<<R>> {{ return R }} #// Insertion ################################################################## @@ -129,14 +129,14 @@ rule _insert<<R>>: decl_vars<<R>> insert_rels<<R>> {{ return R }} -rule insert_rels<<R>>: ":" decl_rels<<R>> where<<R>> {{ return R }} +rule insert_rels<<R>>: ":" decl_rels<<R>> where<<R>> having<<R>> {{ return R }} | #// Update ##################################################################### -rule update<<R>>: decl_rels<<R>> where<<R>> {{ return R }} +rule update<<R>>: decl_rels<<R>> where<<R>> having<<R>> {{ return R }} #// Selection ##################################################################
--- a/parser.py Tue Oct 11 14:14:44 2011 +0200 +++ b/parser.py Fri Dec 09 12:12:56 2011 +0100 @@ -145,10 +145,12 @@ if _token == 'VARIABLE': decl_rels = self.decl_rels(R, _context) where = self.where(R, _context) + having = self.having(R, _context) return R else: # == 'E_TYPE' decl_vars = self.decl_vars(R, _context) where = self.where(R, _context) + having = self.having(R, _context) return R def _insert(self, R, _parent=None): @@ -164,6 +166,7 @@ self._scan('":"', context=_context) decl_rels = self.decl_rels(R, _context) where = self.where(R, _context) + having = self.having(R, _context) return R else: # == "';'" pass @@ -172,6 +175,7 @@ _context = self.Context(_parent, self._scanner, 'update', [R]) decl_rels = self.decl_rels(R, _context) where = self.where(R, _context) + having = self.having(R, _context) return R def union(self, R, _parent=None): @@ -243,12 +247,12 @@ def having(self, S, _parent=None): _context = self.Context(_parent, self._scanner, 'having', [S]) - _token = self._peek('HAVING', 'WITH', 'r"\\)"', "';'", context=_context) + _token = self._peek('HAVING', 'WITH', "';'", 'r"\\)"', context=_context) if _token == 'HAVING': HAVING = self._scan('HAVING', context=_context) logical_expr = self.logical_expr(S, _context) S.set_having([logical_expr]) - else: # in ['WITH', 'r"\\)"', "';'"] + else: # in ['WITH', "';'", 'r"\\)"'] pass def orderby(self, S, _parent=None): @@ -339,19 +343,19 @@ def where(self, S, _parent=None): _context = self.Context(_parent, self._scanner, 'where', [S]) - _token = self._peek('WHERE', 'HAVING', "';'", 'WITH', 'r"\\)"', context=_context) + _token = self._peek('WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) if _token == 'WHERE': WHERE = self._scan('WHERE', context=_context) restriction = self.restriction(S, _context) S.set_where(restriction) - else: # in ['HAVING', "';'", 'WITH', 'r"\\)"'] + else: # in ['HAVING', 'WITH', "';'", 'r"\\)"'] pass def restriction(self, S, _parent=None): _context = self.Context(_parent, self._scanner, 'restriction', [S]) rels_or = self.rels_or(S, _context) node = rels_or - while self._peek("','", 'r"\\)"', 'HAVING', "';'", 'WITH', context=_context) == "','": + while self._peek("','", 'r"\\)"', 'HAVING', 'WITH', "';'", context=_context) == "','": self._scan("','", context=_context) rels_or = self.rels_or(S, _context) node = And(node, rels_or) @@ -361,7 +365,7 @@ _context = self.Context(_parent, self._scanner, 'rels_or', [S]) rels_and = self.rels_and(S, _context) node = rels_and - while self._peek('OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', context=_context) == 'OR': + while self._peek('OR', "','", 'r"\\)"', 'HAVING', 'WITH', "';'", context=_context) == 'OR': OR = self._scan('OR', context=_context) rels_and = self.rels_and(S, _context) node = Or(node, rels_and) @@ -371,7 +375,7 @@ _context = self.Context(_parent, self._scanner, 'rels_and', [S]) rels_not = self.rels_not(S, _context) node = rels_not - while self._peek('AND', 'OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', context=_context) == 'AND': + while self._peek('AND', 'OR', "','", 'r"\\)"', 'HAVING', 'WITH', "';'", context=_context) == 'AND': AND = self._scan('AND', context=_context) rels_not = self.rels_not(S, _context) node = And(node, rels_not) @@ -434,11 +438,11 @@ def opt_right(self, S, _parent=None): _context = self.Context(_parent, self._scanner, 'opt_right', [S]) - _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'WITH', 'HAVING', "';'", context=_context) + _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", 'HAVING', context=_context) if _token == 'QMARK': QMARK = self._scan('QMARK', context=_context) return 'right' - else: # in ['AND', 'OR', "','", 'r"\\)"', 'WITH', 'HAVING', "';'"] + else: # in ['AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", 'HAVING'] pass def logical_expr(self, S, _parent=None): @@ -525,7 +529,7 @@ _context = self.Context(_parent, self._scanner, 'decl_vars', [R]) E_TYPE = self._scan('E_TYPE', context=_context) var = self.var(R, _context) - while self._peek("','", 'R_TYPE', 'QMARK', 'WHERE', '":"', 'CMP_OP', "'IN'", 'HAVING', "';'", 'POW_OP', 'BEING', 'WITH', 'MUL_OP', 'r"\\)"', 'ADD_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'AND', 'OR', context=_context) == "','": + while self._peek("','", 'R_TYPE', 'QMARK', 'WHERE', '":"', 'CMP_OP', 'HAVING', "'IN'", "';'", 'POW_OP', 'BEING', 'WITH', 'MUL_OP', 'r"\\)"', 'ADD_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'AND', 'OR', context=_context) == "','": R.add_main_variable(E_TYPE, var) self._scan("','", context=_context) E_TYPE = self._scan('E_TYPE', context=_context) @@ -535,7 +539,7 @@ def decl_rels(self, R, _parent=None): _context = self.Context(_parent, self._scanner, 'decl_rels', [R]) simple_rel = self.simple_rel(R, _context) - while self._peek("','", 'WHERE', 'HAVING', "';'", 'WITH', 'r"\\)"', context=_context) == "','": + while self._peek("','", 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','": R.add_main_relation(simple_rel) self._scan("','", context=_context) simple_rel = self.simple_rel(R, _context) @@ -566,7 +570,7 @@ if _token != 'UNARY_OP': expr_mul = self.expr_mul(S, _context) node = expr_mul - while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'ADD_OP': + while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'ADD_OP': ADD_OP = self._scan('ADD_OP', context=_context) expr_mul = self.expr_mul(S, _context) node = MathExpression( ADD_OP, node, expr_mul ) @@ -575,7 +579,7 @@ UNARY_OP = self._scan('UNARY_OP', context=_context) expr_mul = self.expr_mul(S, _context) node = UnaryExpression( UNARY_OP, expr_mul ) - while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'ADD_OP': + while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'ADD_OP': ADD_OP = self._scan('ADD_OP', context=_context) expr_mul = self.expr_mul(S, _context) node = MathExpression( ADD_OP, node, expr_mul ) @@ -585,7 +589,7 @@ _context = self.Context(_parent, self._scanner, 'expr_mul', [S]) expr_pow = self.expr_pow(S, _context) node = expr_pow - while self._peek('MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'MUL_OP': + while self._peek('MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'MUL_OP': MUL_OP = self._scan('MUL_OP', context=_context) expr_pow = self.expr_pow(S, _context) node = MathExpression( MUL_OP, node, expr_pow) @@ -595,7 +599,7 @@ _context = self.Context(_parent, self._scanner, 'expr_pow', [S]) expr_base = self.expr_base(S, _context) node = expr_base - while self._peek('POW_OP', 'MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'POW_OP': + while self._peek('POW_OP', 'MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'POW_OP': POW_OP = self._scan('POW_OP', context=_context) expr_base = self.expr_base(S, _context) node = MathExpression( MUL_OP, node, expr_base) @@ -629,7 +633,7 @@ F = Function(FUNCTION) if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"': expr_add = self.expr_add(S, _context) - while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','": + while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == "','": F.append(expr_add) self._scan("','", context=_context) expr_add = self.expr_add(S, _context) @@ -644,7 +648,7 @@ F = Function('IN') if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"': expr_add = self.expr_add(S, _context) - while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','": + while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == "','": F.append(expr_add) self._scan("','", context=_context) expr_add = self.expr_add(S, _context)
--- a/stmts.py Tue Oct 11 14:14:44 2011 +0200 +++ b/stmts.py Fri Dec 09 12:12:56 2011 +0100 @@ -60,6 +60,7 @@ solutions = () # list of possibles solutions for used variables _varmaker = None # variable names generator, built when necessary where = None # where clause node + having = () # XXX now a single node def __init__(self): # dictionnary of defined variables in the original RQL syntax tree @@ -72,6 +73,11 @@ self.where = node node.parent = self + def set_having(self, terms): + self.having = terms + for node in terms: + node.parent = self + def copy(self, copy_solutions=True, solutions=None): new = self.__class__() if self.schema is not None: @@ -249,7 +255,7 @@ returning a string """ if tr is None: - tr = lambda x: x + tr = lambda x,**k: x return [c.get_description(mainindex, tr) for c in self.children] # repr / as_string / copy ################################################# @@ -409,7 +415,6 @@ # select clauses groupby = () orderby = () - having = () # XXX now a single node with_ = () # set by the annotator has_aggregat = False @@ -577,11 +582,6 @@ for node in terms: node.parent = self - def set_having(self, terms): - self.having = terms - for node in terms: - node.parent = self - def set_with(self, terms, check=True): self.with_ = [] for node in terms: @@ -888,6 +888,8 @@ children += self.main_relations if self.where: children.append(self.where) + if self.having: + children += self.having return children @property @@ -922,6 +924,8 @@ result.append(', '.join([repr(rel) for rel in self.main_relations])) if self.where is not None: result.append(repr(self.where)) + if self.having: + result.append('HAVING ' + ','.join(repr(term) for term in self.having)) return ' '.join(result) def as_string(self, encoding=None, kwargs=None): @@ -937,6 +941,9 @@ for rel in self.main_relations])) if self.where is not None: result.append('WHERE ' + self.where.as_string(encoding, kwargs)) + if self.having: + result.append('HAVING ' + ','.join(term.as_string(encoding, kwargs) + for term in self.having)) return ' '.join(result) def copy(self): @@ -948,6 +955,8 @@ new.add_main_relation(child.copy(new)) if self.where: new.set_where(self.where.copy(new)) + if self.having: + new.set_having([sq.copy(new) for sq in self.having]) return new @@ -969,6 +978,8 @@ children += self.main_relations if self.where: children.append(self.where) + if self.having: + children += self.having return children @property @@ -1006,6 +1017,8 @@ result.append(', '.join([repr(rel) for rel in self.main_relations])) if self.where is not None: result.append('WHERE ' + repr(self.where)) + if self.having: + result.append('HAVING ' + ','.join(repr(term) for term in self.having)) return ' '.join(result) def as_string(self, encoding=None, kwargs=None): @@ -1019,6 +1032,9 @@ for rel in self.main_relations])) if self.where is not None: result.append('WHERE ' + self.where.as_string(encoding, kwargs)) + if self.having: + result.append('HAVING ' + ','.join(term.as_string(encoding, kwargs) + for term in self.having)) return ' '.join(result) def copy(self): @@ -1030,6 +1046,8 @@ new.add_main_relation(child.copy(new)) if self.where: new.set_where(self.where.copy(new)) + if self.having: + new.set_having([sq.copy(new) for sq in self.having]) return new @@ -1048,6 +1066,8 @@ children = self.main_relations[:] if self.where: children.append(self.where) + if self.having: + children += self.having return children @property @@ -1066,6 +1086,8 @@ result.append(', '.join(repr(rel) for rel in self.main_relations)) if self.where is not None: result.append('WHERE ' + repr(self.where)) + if self.having: + result.append('HAVING ' + ','.join(repr(term) for term in self.having)) return ' '.join(result) def as_string(self, encoding=None, kwargs=None): @@ -1075,6 +1097,9 @@ for rel in self.main_relations)) if self.where is not None: result.append('WHERE ' + self.where.as_string(encoding, kwargs)) + if self.having: + result.append('HAVING ' + ','.join(term.as_string(encoding, kwargs) + for term in self.having)) return ' '.join(result) def copy(self): @@ -1083,6 +1108,8 @@ new.add_main_relation(child.copy(new)) if self.where: new.set_where(self.where.copy(new)) + if self.having: + new.set_having([sq.copy(new) for sq in self.having]) return new
--- a/test/unittest_analyze.py Tue Oct 11 14:14:44 2011 +0200 +++ b/test/unittest_analyze.py Fri Dec 09 12:12:56 2011 +0100 @@ -300,6 +300,7 @@ {'X': 'Person', 'T': 'Eetype'}, {'X': 'Student', 'T': 'Eetype'}]) + def test_not(self): node = self.helper.parse('Any X WHERE NOT X is Person') self.helper.compute_solutions(node, debug=DEBUG) @@ -308,6 +309,15 @@ expected.remove({'X': 'Person'}) self.assertEqual(sols, expected) + def test_not_identity(self): + node = self.helper.parse('Any X WHERE X located A, P is Person, NOT X identity P') + self.helper.compute_solutions(node, debug=DEBUG) + sols = sorted(node.children[0].solutions) + self.assertEqual(sols, [{'X': 'Company', 'A': 'Address', 'P': 'Person'}, + {'X': 'Person', 'A': 'Address', 'P': 'Person'}, + {'X': 'Student', 'A': 'Address', 'P': 'Person'}, + ]) + def test_uid_func_mapping(self): h = self.helper def type_from_uid(name):
--- a/test/unittest_nodes.py Tue Oct 11 14:14:44 2011 +0200 +++ b/test/unittest_nodes.py Fri Dec 09 12:12:56 2011 +0100 @@ -21,7 +21,7 @@ from logilab.common.testlib import TestCase, unittest_main -from rql import nodes, stmts, parse, BadRQLQuery, RQLHelper +from rql import nodes, stmts, parse, BadRQLQuery, RQLHelper, RQLException from unittest_analyze import DummySchema schema = DummySchema() @@ -55,6 +55,43 @@ self.assertEqual(nodes.etype_from_pyobj(u'hop'), 'String') +class TypesRestrictionNodesTest(TestCase): + + def setUp(self): + self.parse = helper.parse + self.simplify = helper.simplify + + def test_add_is_type_restriction(self): + tree = self.parse('Any X WHERE X is Person') + select = tree.children[0] + x = select.get_selected_variables().next() + self.assertRaises(RQLException, select.add_type_restriction, x.variable, 'Babar') + select.add_type_restriction(x.variable, 'Person') + self.assertEqual(tree.as_string(), 'Any X WHERE X is Person') + + def test_add_new_is_type_restriction_in(self): + tree = self.parse('Any X WHERE X is IN(Person, Company)') + select = tree.children[0] + x = select.get_selected_variables().next() + select.add_type_restriction(x.variable, 'Company') + # implementation is KISS (the IN remains) + self.assertEqual(tree.as_string(), 'Any X WHERE X is IN(Company)') + + def test_add_is_in_type_restriction(self): + tree = self.parse('Any X WHERE X is IN(Person, Company)') + select = tree.children[0] + x = select.get_selected_variables().next() + self.assertRaises(RQLException, select.add_type_restriction, x.variable, 'Babar') + self.assertEqual(tree.as_string(), 'Any X WHERE X is IN(Person, Company)') + + # XXX a full schema is needed, see test in cw/server/test/unittest_security + # def test_add_is_against_isintance_type_restriction(self): + # tree = self.parse('Any X WHERE X is_instance_of Person') + # select = tree.children[0] + # x = select.get_selected_variables().next() + # select.add_type_restriction(x.variable, 'Student') + # self.parse(tree.as_string()) + class NodesTest(TestCase): def _parse(self, rql, normrql=None): tree = parse(rql + ';') @@ -508,6 +545,12 @@ tree = parse(u"Any X WHERE X creation_date TODAY") self.assertEqual(tree.as_string(), 'Any X WHERE X creation_date TODAY') + + def test_get_type_is_in(self): + tree = sparse("Any X WHERE X is IN (Person, Company)") + select = tree.children[0] + self.assertEqual(select.defined_vars['X'].get_type(), 'Any') + # sub-queries tests ####################################################### def test_subq_colalias_compat(self): @@ -527,8 +570,8 @@ self.assertEqual(X.get_type(), 'Company') self.assertEqual(X.get_type({'X': 'Person'}), 'Person') #self.assertEqual(N.get_type(), 'String') - self.assertEqual(X.get_description(0, lambda x:x), 'Company, Person, Student') - self.assertEqual(N.get_description(0, lambda x:x), 'firstname, name') + self.assertEqual(X.get_description(0, lambda x,**k:x), 'Company, Person, Student') + self.assertEqual(N.get_description(0, lambda x,**k:x), 'firstname, name') self.assertEqual(X.selected_index(), 0) self.assertEqual(N.selected_index(), None) self.assertEqual(X.main_relation(), None) @@ -604,5 +647,6 @@ self.assertEqual(sorted(x.name for x in varrefs), ['X', 'X', 'X', 'Y', 'Y']) + if __name__ == '__main__': unittest_main()