from __future__ import absolute_import from __future__ import with_statement import sys, re, os from sqlalchemy import * import sqlalchemy from sqlalchemy.types import UnicodeText from sqlalchemy.exc import UnboundExecutionError from sqlalchemy.ext.compiler import compiles from sqlalchemy.orm import synonym, aliased, joinedload # Why is this here, not in db? because it's easier to resolve dependencies # this way # From Turbogears, originally from sqlalchemy.orm import create_session as orm_create_session import sqlalchemy.orm sqlalchemy.orm.ScopedSession = sqlalchemy.orm.scoped_session from bazbase.benchmark import benchmarking from bazbase.dependencies import Dependencies TXT=u"txt" class Symbol(object): pass NOT_BLANK = Symbol() def to_str(s): """Takes a unicode or str, and returns a str.""" # assert not isinstance(s, str), s if hasattr(s, 'encode'): return s.encode('utf-8') else: return s def one_of_list(l): if len(l) > 1: raise MultipleResultsFound(l) elif len(l) == 0: raise NoResultFound(l) else: return l[0] class LongBlob(LargeBinary): __visit_name__ = 'long_blob' @compiles(LongBlob) def compile_long_blob(type_, compiler, **kw): return "BLOB" # Need to allow for pretty large cached values and uploaded files # in prod. @compiles(LongBlob, "mysql") def compile_long_blob_mysql(type_, compiler, **kw): return "LONGBLOB" def get_engine(): """Retrieve the engine based on the current configuration.""" global _engine if not _engine: args = custom.get_sqlalchemy_args() url = args['url'] assert url is not None,args del args['url'] _engine = sqlalchemy.create_engine(url, **args) if not metadata.is_bound(): metadata.bind = _engine return _engine def reset_engine(): global _engine, metadata, session _engine = None metadata = sqlalchemy.MetaData() session = scoped_session(create_session) elixir.metadata, elixir.session = metadata, session from sqlalchemy.orm import sessionmaker, scoped_session # autocommit is necessary to not have us start in a transaction, # for some reason. Session = sessionmaker(autocommit=True) def create_session(): """Create a session that uses the engine from thread-local metadata.""" if not metadata.is_bound(): get_engine() return Session() import elixir from elixir import (ManyToOne, Entity, Field, OneToMany, using_options, using_table_options, ManyToMany, setup_all, drop_all, create_all) from . import NoResultFound, MultipleResultsFound from bazbase.flavors import FLAVORS from bazbase import custom, db from datetime import datetime reset_engine() elixir.options_defaults['table_options'] = dict(mysql_engine='InnoDB', mysql_charset='utf8', mysql_collate='utf8_bin') def bazname(ent): return 'baz_'+ent.__name__.lower() class SynManyToOne(ManyToOne): def __init__(self,of_kind,synonym,*args,**kw): ManyToOne.__init__(self,of_kind,*args,**kw) self.synonym = synonym def create_properties(self): ManyToOne.create_properties(self) self.add_mapper_property(self.synonym,synonym(self.name)) class Element(Entity): _ename = Field(Unicode(128), unique=True, required=True, index=True, colname='ename', synonym='ename') # http://www.sitepoint.com/article/hierarchical-data-database/2/ # Not unique because there's no standard SQL way to make our crazy # updates not break unique constraints treeleft = Field(Integer,required=True,index=True) treeright = Field(Integer,required=True,index=True) propvals = OneToMany('Propval') # can be "normal" or "toplevel" _orgmode = Field(Unicode(32),required=True,default=u'normal', colname='orgmode',synonym='orgmode') # Version is incremented when our ancestry or children change, # or a propval is added to or removed from us. version = Field(Integer, required=True, default=0) using_options(order_by='treeleft', tablename=bazname) @staticmethod def get(*args, **kw): if len(args) > 0: assert len(args) == 1 ret = Element.get_by(ename=args[0]) if ret is None: raise NoResultFound(args[0]) return ret else: return one_of_list(Element.__search(kw)) @staticmethod def get_raw(**kw): if len(kw) == 1: k, v = kw.items()[0] prop = Prop.get(unicode(k, 'utf-8')) pv = Propval.get_by(prop=prop, value=v) if pv is None: raise NoResultFound("No propval with %s='%s'!" % (k, v)) else: return pv.element else: return one_of_list(Element.__search(kw, raw=True)) @classmethod def get_with(cls, __dct, **kw): if __dct is not None: return one_of_list(cls.__search(__dct, is_unicode=True)) else: return one_of_list(cls.__search(kw)) def __init__(self, ename, parent=False, treeleft=0,treeright=0, fromhook=False): assert '.' not in ename assert ' ' not in ename assert '/' not in ename if fromhook: assert treeleft != 0 assert treeright != 0 else: assert treeleft == 0 assert parent is not False Entity.__init__(self,_ename=ename,treeleft=treeleft, treeright=treeright) if not fromhook: from . import structure # This has a side-effect that sets treeleft,treeright properly if parent is not None: parent = structure.SqlElement(parent) db.esetattr(structure.SqlElement(self), 'parent', None, parent) @property def parent(self): if self.treeleft == 0: # Not done being constructed yet return False else: return (Element.query.filter( and_(Element.treeleft < self.treeleft, Element.treeright > self.treeright)) .order_by(Element.treeright-Element.treeleft).first()) @property def orgmode(self): return self._orgmode @staticmethod def getRoot(): return Element.get_by(treeleft=1) def isRoot(self): return self.treeleft == 1 @classmethod def getAll(cls): """Returns all elements, starting with the root element and always returning the parent before its children.""" return cls.query.order_by(Element.treeleft).all() def __querySubtypes(self): midparent = aliased(Element) return (Element.query.filter( and_(Element.treeleft > self.treeleft, Element.treeright < self.treeright, ~exists().where( and_(midparent.treeleft > self.treeleft, midparent.treeright < self.treeright, Element.treeleft > midparent.treeleft, Element.treeright < midparent.treeright))))) def getChildren(self): return self.__querySubtypes().all() def hasChildren(self): return self.treeright - self.treeleft > 1 def __listDescendants(self, includeMe=False, restrictions={}, query=None): if includeMe: a = Element.treeleft >= self.treeleft b = Element.treeright <= self.treeright else: a = Element.treeleft > self.treeleft b = Element.treeright < self.treeright if query is None: query = Element.query query = query.filter(and_(a, b)).order_by(Element.treeleft) if restrictions: return Element.__search(restrictions, query=query) else: return query.all() def getDescendants(self,**kw): """Return a list of this Element's descendents, depth-first order.""" return self.__listDescendants(restrictions=kw) def withDescendants(self,**kw): """Return a list of this Element and its descendents.""" return self.__listDescendants(includeMe=True, restrictions=kw) def countDescendants(self): return (self.treeright - self.treeleft - 1) / 2 def getLeaves(self): return self.__listDescendants(includeMe=True, query=Element.query.filter( Element.treeright-Element.treeleft==1)) def getNonleaves(self): return self.__listDescendants(includeMe=True, query=Element.filter( Element.treeright-Element.treeleft!=1)) def __queryAncestors(self): return (Element.query.filter(and_(Element.treeleft < self.treeleft, Element.treeright > self.treeright)) .order_by(Element.treeleft)) def getAncestors(self,*args,**kw): return self.__queryAncestors(*args,**kw).all() def isAncestorOf(self,element): return (self.treeleft < element.treeleft and self.treeright > element.treeright) def childTree(self): """Return a child tree structure. Since there's only a single root, just returns that root, not a list of roots.""" return Element.asTree(self.__listDescendants(includeMe=True))[0] def queryRelations(*elements): """Return a query for all elements either an ancestor or a descendent of any of the given elements. Includes the given elements. Can be called like e.queryRelations() or Element.queryRelations(e1,e2,e3).""" return Element.query.filter( or_(or_(and_(Element.treeleft < e.treeleft, Element.treeright > e.treeright) for e in elements), or_(and_(Element.treeleft >= e.treeleft, Element.treeright <= e.treeright) for e in elements))).order_by(Element.treeleft) @staticmethod def asTree(elms): """Formats elms as a tree. elms should be a list of elements ordered by treeleft. The return value is a list of roots. A node is either a leaf or a subtree. A leaf is [element]. A subtree is [element, [<>], [<>]].""" ret = [] stack = [ret] while len(elms) > 0: e = elms.pop(0) while len(stack) > 1 and stack[-1][0].treeright < e.treeleft: stack.pop() bubble = [e] stack[-1].append(bubble) stack.append(bubble) return ret @property def ename(self): return self._ename def __unicode__(self): return self.ename def __repr__(self): return ""%self.ename.encode('utf-8') def __contains__(self, key): return key in self.propname_list() def __getitem__(self, key): try: pv = Propval.query.join(Propval.prop).filter( and_(Prop.name == key, Propval.element_id == self.id)).one() return pv except NoResultFound: raise KeyError("Element %s does not have prop %s!" % (self.ename, key)) def propval_for_prop(self, prop): try: pv = Propval.query.filter( and_(Propval.prop_id == prop.id, Propval.element_id == self.id)).one() return pv except NoResultFound: raise KeyError("Element %s does not have prop %s!" % (self.ename, prop.name)) def __setitem__(self, key, value): prop = Prop.set_or_create(name=key) if not prop: raise KeyError("Prop '%s' is not defined!"%key) else: self.set_value_for_prop(prop, value) def set_value_for_prop(self, prop, value): # -? typecheck? Propval.set_or_create(element=self,prop=prop,value=value) def __delitem__(self, key): prop = Prop.get_by(name=key) if not prop: raise KeyError("Prop '%s' is not defined!"%key) pv = Propval.get_by(prop=prop,element=self) if pv is None: raise KeyError("Element %s has no value for %s!" % (self.ename, key)) if key not in self.parent: pv.delete() else: pv.value = prop.eval_default(self) def __iter__(self): return iter(self.propname_list()) def propname_list(self): from . import structure return structure.SqlElement(self).list_props() @classmethod def __search(cls, restrictions, query=None, is_unicode=False, raw=False): """elm.search(category='mon',name='sakura') returns a list of elements with those propvals, using inheritance and such. restrictions must have string keys, like it game from **kw, unless is_unicode is True.""" if query is None: query = cls.query elms = query.all() for pname in restrictions: newelms = [] # TODO(xavid): deal with this more cleanly. if not is_unicode: pname = unicode(pname, 'utf-8') prop = Prop.get_by(name=pname) assert FLAVORS[prop.flavor].indexed if raw: assert FLAVORS[prop.flavor].raw else: assert not FLAVORS[prop.flavor].raw target = restrictions[pname] # TODO(xavid): replace by hasattr with a method if isinstance(target, Element): target = FLAVORS['reference'].element_to_string(target) for e in elms: if pname in e: if raw: rendered = e[pname].value else: from .wiki import NoContentException try: rendered = e[pname].render() except NoContentException: continue if target is NOT_BLANK: if rendered != '': newelms.append(e) else: if rendered == target: newelms.append(e) elms = newelms return elms @classmethod def search(cls, __dct=None, **kw): if __dct is not None: return cls.__search(__dct, is_unicode=True) else: return cls.__search(kw) def delete(self): db.edelete(self) def __eq__(self, other): return hasattr(other, 'ename') and self.ename == other.ename def __hash__(self): return hash(self.ename) PARENT_DOT_THIS = '<>' PLACEHOLDER_PAT = re.compile(r'^\s*<<<([\w.-]+)\s*/>>>') # Valid flavors are currently u'string',u'text',u'integer',u'references', # or u'boolean' # but are basically defined in flavors and extended in custom # All props with the same name have the same flavor, even if "unrelated" class Prop(Entity): _name = Field(Unicode(128),unique=True,required=True,index=True, colname='name', synonym='name') _flavor = Field(Unicode(128),colname='flavor',synonym='flavor') _default = Field(LargeBinary, required=True, colname='default', synonym='default') # True for props that get viewed directly outside the edit view, # like "product". _visible = Field(Boolean, colname='visible', synonym='visible', required=True) _comment = Field(UnicodeText(), colname='comment', synonym='comment') propvals = OneToMany('Propval') using_options(tablename=bazname) @staticmethod def get(pname): ret = Prop.get_by(name=pname) if ret is None: raise NoResultFound("No property '%s' defined!"%pname) return ret def __init__(self, name, flavor=None, default=None, visible=False, comment=u'', fromhook=False): assert ' ' not in name assert '.' not in name assert ':' not in name assert '/' not in name assert '=' not in name assert '@' not in name assert '|' not in name assert '^' not in name assert '>' not in name if flavor is None: flavor = custom.DEFAULT_FLAVOR if default is None: default = PARENT_DOT_THIS else: # assert isinstance(default, str), repr(default) pass Entity.__init__(self,_name=name, _flavor=flavor, _default=default, _visible=visible, _comment=comment) if not fromhook: from . import structure sqlp = structure.SqlProp(self) db.psetattr(sqlp, 'flavor', flavor) db.psetattr(sqlp, 'default', default) db.psetattr(sqlp, 'visible', visible) db.psetattr(sqlp, 'comment', comment) def __set_name(self,newname): db.psetattr(self,'pname',newname) def __get_name(self): return self._name name = property(__get_name,__set_name) def __set_flavor(self,newflavor): db.psetattr(self,'flavor',newflavor) def __get_flavor(self): return self._flavor flavor = property(__get_flavor,__set_flavor) def __set_default(self,newdefault): db.psetattr(self,'default',newdefault) def __get_default(self): return self._default default = property(__get_default,__set_default) def eval_default(self, element): m = PLACEHOLDER_PAT.search(self.default) if m is not None: holder = m.group(1) if holder == "now": return FLAVORS['timestamp'].now() elif holder == "ename": if (element.parent[self.name].value and element.parent[self.name].value != element.parent.ename): return PARENT_DOT_THIS else: return element.ename.encode('utf-8') elif holder == "parent.this": return element.parent[self.name].value else: raise KeyError("Unknown prop default placeholder '%s'!" % holder) else: return self.default def __set_visible(self, newvisible): db.psetattr(self, 'visible', newvisible) def __get_visible(self): return self._visible visible = property(__get_visible, __set_visible) def __set_comment(self, newcomment): db.psetattr(self, 'comment', newcomment) def __get_comment(self): return self._comment comment = property(__get_comment, __set_comment) def delete(self): db.pdelete(self) @classmethod def getAll(cls): return cls.query.all() def elementTree(self): return Element.asTree(Element.query.join(Element.propvals) .filter(Propval.prop_id == self.id) .order_by(Element.treeleft).all()) @classmethod def set_or_create(cls,name,flavor=None): ret = cls.get_by(name=name) if ret is None: if flavor is None: flavor = custom.DEFAULT_FLAVOR ret = cls(name=name, flavor=flavor) elif flavor is not None: ret.flavor = flavor return ret class Propval(Entity): prop = ManyToOne('Prop',required=True) element = ManyToOne('Element',required=True) _value = Field(LongBlob, required=True, colname='value', synonym='value') # A map of overlay identifiers to overlayed values. overlays = Field(PickleType, default={}) version = Field(Integer, required=True, default=0) #format = Field(Unicode(32), required=True, default=u'creole') using_options(tablename=bazname) def __init__(self,element,prop,value=None,fromhook=False): assert element is not None,kw if prop.flavor != u'blob': assert '<<<' not in str(value), repr(str(value)) if value is None: value = prop.eval_default(element) assert '<<<' not in str(value), repr(str(value)) # assert isinstance(value, str), repr(value) Entity.__init__(self, element=element, prop=prop, _value=value) if not fromhook: db.setprop(element, prop.name, value) @classmethod def getAll(cls): return cls.query.all() #@property #def value(self): # return self._value def get_value_and_format(self): if self.prop.flavor == 'blob': if self._value.startswith('<<" % (self.element.ename.encode('utf-8'), self.prop.name.encode('utf-8')) except UnboundExecutionError: return "" def __unicode__(self): return unicode(self.render(method='unicode', offset=1)) def render(self, format=TXT, filters={}, method='render', offset=0): from . import conversion im = conversion.convert_any(self.element.ename, self.prop.name, [format], filters, method=method, offset=offset + 1) return im.asData() def has_overlay(self, identifier): return identifier in self.overlays def get_overlay(self, identifier): return self.overlays[identifier] def set_overlay(self, identifier, value): self.overlays[identifier] = value setup_all() # Indexes -- sort of a hack Index('PropvalInd',Propval._descriptor.get_column('prop_id'), Propval._descriptor.get_column('element_id'), unique=True) # Needs SQLAlchemy 0.7+ #Index('PropAndValue', Propval._descriptor.get_column('prop_id'), # Propval._descriptor.get_column('value'), mysql_length=16) blobdir = None def blobname(ename, pname): return os.path.join(blobdir, '%s.%s.blob' % (ename, pname)) CREOLE_COMPAT_FORMATS = ('creole', '.creole', '.txt') class hook(object): def begin(self): session.begin() def prepare(self): session.flush() def commit(self): session.commit() session.close() def abort(self): # Close does an implicit rollback(), but if we do an explicit one # we don't session.expunge_all() properly # session.rollback() session.close() def invalidate(self, propvals): for pv in propvals: if pv.version is None: pv.version = 1 else: pv.version += 1 def invalidate_propval(self, propval): self.invalidate(propvals=[propval]) def invalidate_prop(self, prop): self.invalidate(propvals=prop.propvals) def invalidate_element(self, element): if element.version is None: element.version = 1 else: element.version += 1 def esetattr(self,e,attr, oldval, val): e = e._e assert attr in ('parent','ename','orgmode') if attr == 'ename': assert e is not None oldename = e.ename e._ename = val # Cache entries include the element ID in addition to ename, # so this can't reuse old cache entries. elif attr == 'orgmode': e._orgmode = val elif attr == 'parent': parent = val._e if val else None if e is None or e.treeleft == 0: self.oldparent = None self.created = True precount = Element.query.filter(Element.treeleft != 0).count() if parent is None: assert precount == 0,precount # Must be the toplevel element, which is unique treeleft = 1 treeright = 2 else: assert Element.getRoot().treeright == precount*2,( "precondition", Element.getRoot().treeright,precount*2) # Move old nodes for insert session.flush() point = parent.treeright Element.query.filter(Element.treeright >= point).update(dict(treeright=Element.treeright+2)) Element.query.filter(Element.treeleft >= point).update(dict(treeleft= Element.treeleft+2)) treeleft = point treeright = point+1 session.expire_all() e.treeleft = treeleft e.treeright = treeright if parent is not None: assert e.parent == parent,(repr(point), repr(e), (e.treeleft,e.treeright), repr(e.parent), (e.parent.treeleft, e.parent.treeright), repr(parent), (parent.treeleft, parent.treeright)) assert Element.getRoot().treeright == (precount+1)*2,( Element.getRoot().treeright, (precount+1)*2) else: # Changing parent self.oldparent = e.parent self.created = False assert self.oldparent is not None,(e.ename,e.treeleft) assert parent is not None assert not (parent.treeleft > e.treeleft and parent.treeright < e.treeright),"An element cannot be its own grandpa!" # Mod tree oldleft = e.treeleft oldright = e.treeright delta = oldright-oldleft #rightq = parent.treeright > self.oldparent.treeright rightq = parent.treeright > oldright if rightq: newleft = parent.treeright - delta - 1 else: newleft = parent.treeright newright = newleft + delta dist = newleft-oldleft # We need to get the ids of us and children first, because the # following updates will cause confusion descandme = [e.id for e in Element.query.filter( and_(Element.treeleft >= oldleft, Element.treeright <= oldright))] session.flush() if rightq: session.execute(Element.table.update( and_(Element.treeright > oldright, Element.treeright <= newright), values=dict(treeright=Element.treeright-delta-1))) session.execute(Element.table.update( and_(Element.treeleft > oldright, Element.treeleft <= newright), values=dict(treeleft=Element.treeleft-delta-1))) else: session.execute(Element.table.update( and_(Element.treeright >= newleft, Element.treeright < oldleft), values=dict(treeright=Element.treeright+delta+1))) session.execute(Element.table.update( and_(Element.treeleft >= newleft, Element.treeleft < oldleft), values=dict(treeleft=Element.treeleft+delta+1))) # Me and my children, so both left and right must be in the range session.execute(Element.table.update( Element.id.in_(descandme), values=dict(treeleft=Element.treeleft + dist, treeright=Element.treeright + dist) )) session.expire_all() # Clear defaulting propvals we no longer need npropval = aliased(Propval) opropval = aliased(Propval) for pv in Propval.query.join(Propval.prop).filter( and_(Propval.element_id == e.id, Propval.value == Prop.default, exists().where( and_(opropval.element_id == self.oldparent.id, opropval.prop_id == Propval.prop_id)), ~exists().where( and_(npropval.element_id == parent.id, npropval.prop_id == Propval.prop_id)))): pv.delete() def post_esetattr(self, se, attr, oldval, val): e = se._e if attr == 'parent': if e.treeleft != 1: parent = e.parent # Defaulting propvals for pv in Propval.query.filter_by(element_id=parent.id): mev = Propval.get_by(prop_id=pv.prop_id, element_id=e.id) if mev is None: # Create defaults to parent db.setprop(se, pv.prop.name, pv.prop.eval_default(e), 'creole') # Expire here, because otherwise the invalidations we're # about to do can break things session.flush() session.expire_all() self.invalidate_element(e) self.invalidate_element(parent) # Invalidate descendants, because this may add or remove # propvals from them. for d in e.getDescendants(): self.invalidate_element(d) def edelete(self,se): e = se._e assert e.treeleft != 1,"You can't delete the root element." assert e.treeright - e.treeleft == 1,"You can't delete an element with children." for pv in e.propvals: self.delete(se,pv.prop.name,invalidateDependencies=False, unconditionally=True) oldright = e.treeright # The element no longer exitsts, so its cache entries won't be used. Entity.delete(e) # Fill in the gap; we know that we have no children session.flush() session.execute(Element.table.update( Element.treeright > oldright, values=dict(treeright=Element.treeright-2))) session.execute(Element.table.update( Element.treeleft > oldright, values=dict(treeleft=Element.treeleft-2))) session.expire_all() def psetattr(self,p,attr,val): if p is None: assert False #assert attr != 'pname' #p=Prop(name=pname,fromhook=True, # **{attr:val}) else: p = p._p if attr in ('flavor', 'visible', 'comment'): setattr(p, '_'+attr, val) if attr != 'comment': self.invalidate_prop(p) elif attr == 'default': olddefault = p.default p._default = to_str(val) # Mod whatever was defaulting session.flush() session.execute(Propval.table.update( and_(Propval.prop == p, Propval.value == olddefault), values=dict(value=val))) session.expire_all() # TODO(xavid): could get away with only invalidating the # ones that were changed. self.invalidate_prop(p) elif attr == 'pname': oldname = p._name p._name = val # Things are cached by both propname and prop id, so this # can't steal cache entries. else: raise Exception('Unknown pattr %s!'%attr) def pdelete(self,p): assert len(p._p.propvals) <= 0,"You can't delete an in-use Prop! Used by: " + repr(p._p.propvals) Entity.delete(p._p) def setprop(self, e, pname, val, format): e = e._e assert isinstance(val, str),repr(val) p = Prop.set_or_create(name=pname) if not FLAVORS[p.flavor].binary and not FLAVORS[p.flavor].raw: assert '<<<' not in val, repr(val) is_blob = FLAVORS[p.flavor].binary if is_blob and val != '' and val != PARENT_DOT_THIS: with open(blobname(e.ename, pname), 'w') as fil: fil.write(val) assert format != 'creole', (pname, val) val = '<<= e.treeleft, Element.treeright <= e.treeright) ).count()) __all__ = ['Element', 'Prop', 'Propval']