534 return self._eid_index[eid] |
555 return self._eid_index[eid] |
535 |
556 |
536 |
557 |
537 # Possible constraints ######################################################## |
558 # Possible constraints ######################################################## |
538 |
559 |
539 class RQLVocabularyConstraint(BaseConstraint): |
560 class BaseRQLConstraint(BaseConstraint): |
540 """the rql vocabulary constraint : |
561 """base class for rql constraints |
541 |
562 """ |
542 limit the proposed values to a set of entities returned by a rql query, |
563 |
543 but this is not enforced at the repository level |
564 def __init__(self, restriction, mainvars=None): |
544 |
565 self.restriction = normalize_expression(restriction) |
545 restriction is additional rql restriction that will be added to |
566 if mainvars is None: |
546 a predefined query, where the S and O variables respectivly represent |
567 mainvars = guess_rrqlexpr_mainvars(restriction) |
547 the subject and the object of the relation |
568 else: |
548 """ |
569 normmainvars = [] |
549 |
570 for mainvar in mainvars.split(','): |
550 def __init__(self, restriction): |
571 mainvar = mainvar.strip() |
551 self.restriction = restriction |
572 if not mainvar.isalpha(): |
|
573 raise Exception('bad mainvars %s' % mainvars) |
|
574 normmainvars.append(mainvar) |
|
575 assert mainvars, 'bad mainvars %s' % mainvars |
|
576 mainvars = ','.join(sorted(normmainvars)) |
|
577 self.mainvars = mainvars |
552 |
578 |
553 def serialize(self): |
579 def serialize(self): |
554 return self.restriction |
580 # start with a comma for bw compat, see below |
|
581 return ';' + self.mainvars + ';' + self.restriction |
555 |
582 |
556 def deserialize(cls, value): |
583 def deserialize(cls, value): |
557 return cls(value) |
584 # XXX < 3.5.10 bw compat |
|
585 if not value.startswith(';'): |
|
586 return cls(value) |
|
587 _, mainvars, restriction = value.split(';', 2) |
|
588 return cls(restriction, mainvars) |
558 deserialize = classmethod(deserialize) |
589 deserialize = classmethod(deserialize) |
559 |
590 |
560 def check(self, entity, rtype, value): |
591 def check(self, entity, rtype, value): |
561 """return true if the value satisfy the constraint, else false""" |
592 """return true if the value satisfy the constraint, else false""" |
562 # implemented as a hook in the repository |
593 # implemented as a hook in the repository |
566 """raise ValidationError if the relation doesn't satisfy the constraint |
597 """raise ValidationError if the relation doesn't satisfy the constraint |
567 """ |
598 """ |
568 pass # this is a vocabulary constraint, not enforce XXX why? |
599 pass # this is a vocabulary constraint, not enforce XXX why? |
569 |
600 |
570 def __str__(self): |
601 def __str__(self): |
571 return self.restriction |
602 return '%s(Any %s WHERE %s)' % (self.__class__.__name__, self.mainvars, |
|
603 self.restriction) |
572 |
604 |
573 def __repr__(self): |
605 def __repr__(self): |
574 return '<%s : %s>' % (self.__class__.__name__, repr(self.restriction)) |
606 return '<%s @%#x>' % (self.__str__(), id(self)) |
575 |
607 |
576 |
608 |
577 class RQLConstraint(RQLVocabularyConstraint): |
609 class RQLVocabularyConstraint(BaseRQLConstraint): |
578 """the rql constraint is similar to the RQLVocabularyConstraint but |
610 """the rql vocabulary constraint : |
579 are also enforced at the repository level |
611 |
580 """ |
612 limit the proposed values to a set of entities returned by a rql query, |
581 def exec_query(self, session, eidfrom, eidto): |
613 but this is not enforced at the repository level |
582 if eidto is None: |
614 |
583 rql = 'Any S WHERE S eid %(s)s, ' + self.restriction |
615 restriction is additional rql restriction that will be added to |
584 return session.unsafe_execute(rql, {'s': eidfrom}, 's', |
616 a predefined query, where the S and O variables respectivly represent |
585 build_descr=False) |
617 the subject and the object of the relation |
586 rql = 'Any S,O WHERE S eid %(s)s, O eid %(o)s, ' + self.restriction |
618 |
587 return session.unsafe_execute(rql, {'s': eidfrom, 'o': eidto}, |
619 mainvars is a string that should be used as selection variable (eg |
588 ('s', 'o'), build_descr=False) |
620 `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be |
589 def error(self, eid, rtype, msg): |
621 done to guess it according to variable used in the expression. |
590 raise ValidationError(eid, {rtype: msg}) |
622 """ |
|
623 |
|
624 |
|
625 class RepoEnforcedRQLConstraintMixIn(object): |
|
626 |
|
627 def __init__(self, restriction, mainvars=None, msg=None): |
|
628 super(RepoEnforcedRQLConstraintMixIn, self).__init__(restriction, mainvars) |
|
629 self.msg = msg |
|
630 |
|
631 def serialize(self): |
|
632 # start with a semicolon for bw compat, see below |
|
633 return ';%s;%s\n%s' % (self.mainvars, self.restriction, |
|
634 self.msg or '') |
|
635 |
|
636 def deserialize(cls, value): |
|
637 # XXX < 3.5.10 bw compat |
|
638 if not value.startswith(';'): |
|
639 return cls(value) |
|
640 value, msg = value.split('\n', 1) |
|
641 _, mainvars, restriction = value.split(';', 2) |
|
642 return cls(restriction, mainvars, msg) |
|
643 deserialize = classmethod(deserialize) |
591 |
644 |
592 def repo_check(self, session, eidfrom, rtype, eidto=None): |
645 def repo_check(self, session, eidfrom, rtype, eidto=None): |
593 """raise ValidationError if the relation doesn't satisfy the constraint |
646 """raise ValidationError if the relation doesn't satisfy the constraint |
594 """ |
647 """ |
595 if not self.exec_query(session, eidfrom, eidto): |
648 if not self.match_condition(session, eidfrom, eidto): |
596 # XXX at this point dunno if the validation error `occured` on |
649 # XXX at this point if both or neither of S and O are in mainvar we |
597 # eidfrom or eidto (from user interface point of view) |
650 # dunno if the validation error `occured` on eidfrom or eidto (from |
598 self.error(eidfrom, rtype, 'constraint %s failed' % self) |
651 # user interface point of view) |
599 |
652 if eidto is None or 'S' in self.mainvars or not 'O' in self.mainvars: |
600 |
653 maineid = eidfrom |
601 class RQLUniqueConstraint(RQLConstraint): |
654 else: |
|
655 maineid = eidto |
|
656 if self.msg: |
|
657 msg = session._(self.msg) |
|
658 else: |
|
659 msg = '%(constraint)s %(restriction)s failed' % { |
|
660 'constraint': session._(self.type()), |
|
661 'restriction': self.restriction} |
|
662 raise ValidationError(maineid, {rtype: msg}) |
|
663 |
|
664 def exec_query(self, session, eidfrom, eidto): |
|
665 if eidto is None: |
|
666 # checking constraint for an attribute relation |
|
667 restriction = 'S eid %(s)s, ' + self.restriction |
|
668 args, ck = {'s': eidfrom}, 's' |
|
669 else: |
|
670 restriction = 'S eid %(s)s, O eid %(o)s, ' + self.restriction |
|
671 args, ck = {'s': eidfrom, 'o': eidto}, ('s', 'o') |
|
672 rql = 'Any %s WHERE %s' % (self.mainvars, restriction) |
|
673 if self.distinct_query: |
|
674 rql = 'DISTINCT ' + rql |
|
675 return session.unsafe_execute(rql, args, ck, build_descr=False) |
|
676 |
|
677 |
|
678 class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint): |
|
679 """the rql constraint is similar to the RQLVocabularyConstraint but |
|
680 are also enforced at the repository level |
|
681 """ |
|
682 distinct_query = False |
|
683 |
|
684 def match_condition(self, session, eidfrom, eidto): |
|
685 return self.exec_query(session, eidfrom, eidto) |
|
686 |
|
687 |
|
688 class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint): |
602 """the unique rql constraint check that the result of the query isn't |
689 """the unique rql constraint check that the result of the query isn't |
603 greater than one |
690 greater than one |
604 """ |
691 """ |
605 def repo_check(self, session, eidfrom, rtype, eidto=None): |
692 distinct_query = True |
606 """raise ValidationError if the relation doesn't satisfy the constraint |
693 |
607 """ |
694 # XXX turns mainvars into a required argument in __init__, since we've no |
608 if len(self.exec_query(session, eidfrom, eidto)) > 1: |
695 # way to guess it correctly (eg if using S,O or U the constraint will |
609 # XXX at this point dunno if the validation error `occured` on |
696 # always be satisfied since we've to use a DISTINCT query) |
610 # eidfrom or eidto (from user interface point of view) |
697 |
611 self.error(eidfrom, rtype, 'unique constraint %s failed' % self) |
698 def match_condition(self, session, eidfrom, eidto): |
612 |
699 return len(self.exec_query(session, eidfrom, eidto)) <= 1 |
613 |
|
614 def split_expression(rqlstring): |
|
615 for expr in rqlstring.split(','): |
|
616 for word in expr.split(): |
|
617 yield word |
|
618 |
|
619 def normalize_expression(rqlstring): |
|
620 """normalize an rql expression to ease schema synchronization (avoid |
|
621 suppressing and reinserting an expression if only a space has been added/removed |
|
622 for instance) |
|
623 """ |
|
624 return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(',')) |
|
625 |
700 |
626 |
701 |
627 class RQLExpression(object): |
702 class RQLExpression(object): |
628 def __init__(self, expression, mainvars, eid): |
703 def __init__(self, expression, mainvars, eid): |
629 self.eid = eid # eid of the entity representing this rql expression |
704 self.eid = eid # eid of the entity representing this rql expression |