""" Database modification statement semantics

:Author: Aaron Watters
:Maintainers: http://gadfly.sf.net/
:Copyright: Aaron Robert Watters, 1994
:Id: $Id: operations.py,v 1.4 2002/05/11 02:59:05 richard Exp $:
"""

import semantics
import serialize

# ordering of ddef storage is important so, eg, index defs
# follow table defs.

class Ordered_DDF:
    """mixin for DDF statement sorting, subclass defines s.cmp(o)"""
    def __cmp__(self, other):
        try:
            #print "comparing", self.name, other.name
            try:
                sc = self.__class__
                oc = other.__class__
                #print sc, oc
            except:
                #print "punting 1", -1
                return -1
            if sc in ddf_order and oc in ddf_order:
                test = cmp(ddf_order.index(sc), ddf_order.index(oc))
                #print "ddforder", test
                if test: return test
                return self.cmp(other)
            else:
                test = cmp(sc, oc)
                #print "punting 2", test
                return test
        except:
            #import sys
            #print "exception!"
            #print sys.exc_type, sys.exc_value
            return -1

    def __coerce__(self, other):
        return (self, other)
    def cmp(self, other):
        """redefine if no name field"""
        return cmp(self.name, other.name)

CTFMT = """\
CREATE TABLE %s (
  %s
  )"""

class CreateTable(Ordered_DDF):
    """create table operation"""

    def __init__(self, name, colelts):
        self.name = name
        self.colelts = colelts
        self.indb = None # db in which to create

    def initargs(self):
        return (self.name, [])

    def marshaldata(self):
        return map(serialize.serialize, self.colelts)

    def demarshal(self, args):
        self.colelts = map(serialize.deserialize, args)

    def __repr__(self):
        from string import join
        elts = list(self.colelts)
        elts = map(repr, elts)
        return CTFMT % (self.name, join(elts, ",\n  "))

    def relbind(self, db):
        """check that table doesn't already exist"""
        if db.has_relation(self.name):
            raise NameError, "cannot create %s, exists" % (self.name,)
        self.indb = db
        return self

    def eval(self, dyn=None):
        "create the relation now"
        # datatypes currently happily ignored :)
        db = self.indb
        if db is None:
            raise ValueError, "unbound or executed"
        self.indb = None
        name = self.name
        if db.has_relation(self.name):
            raise NameError, "relation %s exists, cannot create" % (self.name,)
        db.touched = 1
        attnames = []
        for x in self.colelts:
            attnames.append(x.colid)
        from store import Relation0
        r = Relation0(attnames)
        # must store if new (unset for reloads)
        r.touched = 1
        db[name] = r
        db.add_datadef(name, self)
        log = db.log
        if log is not None:
            log.log(self)

viewfmt = """\
CREATE VIEW %s (%s) AS
%s"""

class CreateView(semantics.SimpleRecursive, Ordered_DDF):
    """CREATE VIEW name (namelist) AS selection"""

    # note: no check for cross-references on drops!
    def __init__(self, name, namelist, selection):
        self.name = name
        self.namelist = namelist
        self.selection = selection
        self.indb = None

    def __repr__(self):
        return viewfmt % (self.name, self.namelist, self.selection)

    def initargs(self):
        return (self.name, self.namelist, self.selection)

    def relbind(self, db):
        self.indb = db
        name = self.name
        if db.has_datadef(name):
            raise NameError, "(view) datadef %s exists" % name
        # don't bind the selection yet
        return self

    def eval(self, dyn=None):
        "create the view"
        db = self.indb
        name = self.name
        if db is None:
            raise ValueError, "create view %s unbound or executed" % name
        self.indb = None
        if db.has_relation(name):
            raise ValueError, "create view %s, name exists" % name
        db.touched = 1
        from store import View
        v = View(self.name, self.namelist, self.selection, db)
        db[name] = v
        db.add_datadef(name, self)
        log = db.log
        if log is not None:
            log.log(self)

CREATEINDEXFMT = """\
CREATE %sINDEX %s ON %s (
   %s
   )"""

class CreateIndex(semantics.SimpleRecursive, Ordered_DDF):
    """create index operation"""
    def __init__(self, name, tablename, atts, unique=0):
        self.name = name
        self.tablename=tablename
        self.atts = atts
        self.indb = None
        self.target = None
        self.unique = unique

    def initargs(self):
        return (self.name, self.tablename, self.atts, self.unique)

    def __cmp__(self, other):
        oc = other.__class__
        if oc is CreateTable:
            return 1 # after all create tables
        sc = self.__class__
        if oc is not sc:
            return cmp(sc, oc)
        else:
            return cmp(self.name, other.name)

    def __coerce__(self, other):
        return (self, other)

    def __repr__(self):
        from string import join
        un = ""
        if self.unique: un="UNIQUE "
        innards = join(self.atts, ",\n   ")
        return CREATEINDEXFMT % (un, self.name, self.tablename, innards)

    def relbind(self, db):
        name = self.name
        self.indb = db
        if db.has_datadef(name):
            raise NameError, `name`+": data def exists"
        try:
            self.target = db.get_for_update(self.tablename) #db[self.tablename]
        except:
            raise NameError, `self.tablename`+": no such relation"
        return self

    def eval(self, dyn=None):
        from store import Index
        db = self.indb
        if db is None:
            raise ValueError, "create index unbound or executed"
        self.indb = None
        rel = self.target
        if rel is None:
            raise ValueError, "create index not bound to relation"
        db.touched = 1
        self.the_index = the_index = Index(self.name, self.atts, unique=self.unique)
        rel.add_index(the_index)
        name = self.name
        db.add_datadef(name, self)
        db.add_index(name, the_index)
        log = db.log
        if log is not None:
            log.log(self)

class DropIndex(semantics.SimpleRecursive):
    def __init__(self, name):
        self.name = name
        self.indb = None

    def initargs(self):
        return (self.name,)

    def __repr__(self):
        return "DROP INDEX %s" % (self.name,)

    def relbind(self, db):
        self.indb = db
        if not db.has_datadef(self.name):
            raise NameError, `self.name`+": no such index"
        return self

    def eval(self, dyn=None):
        db = self.indb
        self.indb=None
        if db is None:
            raise ValueError, "drop index executed or unbound"
        db.touched = 1
        indexname = self.name
        createindex = db.datadefs[indexname]
        index = createindex.the_index
        relname = createindex.tablename
        rel = db[relname]
        rel.drop_index(index)
        db.drop_datadef(indexname)
        db.drop_index(indexname)
        log = db.log
        if log is not None:
            log.log(self)

class DropTable(semantics.SimpleRecursive):
    def __init__(self, name):
        self.name = name
        self.indb = None
    def initargs(self):
        return (self.name,)
    def __repr__(self):
        return "DROP TABLE %s" % (self.name,)
    def relbind(self, db):
        self.indb = db
        name = self.name
        if not db.has_relation(name):
            raise NameError, `self.name` + ": cannot delete, no such table/view"
        self.check_kind(name, db)
        return self
    def check_kind(self, name, db):
        if db[name].is_view:
            raise ValueError, "%s is VIEW, can't DROP TABLE" % name
    def eval(self, dyn):
        db = self.indb
        if db is None:
            raise ValueError, "unbound or executed"
        db.touched = 1
        self.indb = None
        self.relbind(db)
        name = self.name
        rel = db[name]
        rel.drop_indices(db)
        db.drop_datadef(name)
        del db[name]
        log = db.log
        if log is not None:
            log.log(self)

class DropView(DropTable):
    """DROP VIEW name"""
    def __repr__(self):
        return "DROP VIEW %s" % self.name
    def check_kind(self, name, db):
        if not db[name].is_view:
            raise ValueError, "%s is TABLE, can't DROP VIEW" % name

COLDEFFMT = "%s %s %s %s"

class ColumnDef(semantics.SimpleRecursive):

    def __init__(self, colid, datatype, defaults, constraints):
        self.colid = colid
        self.datatype = datatype
        self.defaults = defaults
        self.constraints = constraints

    def initargs(self):
        return (self.colid, self.datatype, self.defaults, self.constraints)

    def __repr__(self):
        defaults = self.defaults
        if defaults is None: defaults=""
        constraints = self.constraints
        if constraints is None: constraints = ""
        return COLDEFFMT % (self.colid, self.datatype, defaults, constraints)



def evalcond(cond, eqs, target, dyn, rassns, translate, invtrans):
    """factored out shared op between Update and Delete."""
    if dyn:
            #print "dyn", dyn
        from semantics import dynamic_binding
        dynbind = dynamic_binding(len(dyn), dyn)
        if len(dynbind)>1:
            raise ValueError, "only one dynamic binding allowed for UPDATE"
        dynbind1 = dynbind = dynbind[0]
        if eqs is not None:
            dynbind1 = dynbind.remap(eqs)
        if dynbind1 is None:
            # inconsistent
            return
        dynbind = dynbind1 + dynbind
        if rassns is not None:
            rassns = rassns + invtrans * dynbind
            if rassns.Clean() is None:
                # inconsistent
                return
        else:
            rassns = invtrans * dynbind
        #print "dynbind", dynbind
        #print "rassn", rassns
    else:
        dynbind = None
    # get tuple set, try to use an index
    index = None
    if rassns is not None:
        known = rassns.keys()
        index = target.choose_index(known)
    if index is None:
        (tuples, seqnums) = target.rows(1)
    else:
        #print "using index", index.name
        (tuples, seqnums) = index.matches(rassns)
    ltuples = len(tuples)
    buffer = [0] * ltuples
    rtups = range(ltuples)
    for i in rtups:
        tup = tuples[i]
        #print tup
        ttup = translate * tup
        if dynbind:
            ttup = (ttup + dynbind).Clean()
        if ttup is not None:
            buffer[i] = ttup
    #print "buffer", buffer
    #print "cond", cond
    #for x in buffer:
        #print "before", x
    test = cond(buffer)
    #print "test", test
    return (test, rtups, seqnums, tuples)


UPDFMT = """\
UPDATE %s
SET %s
WHERE %s"""

# optimize to use indices and single call to "cond"
class UpdateOp(semantics.SimpleRecursive):
    def __init__(self, name, assns, condition):
        self.name = name
        self.assns = assns
        self.condition = condition

    def initargs(self):
        return (self.name, self.assns, self.condition)

    def __repr__(self):
        return UPDFMT % (self.name, self.assns, self.condition)

    def relbind(self, db):
        self.indb = db
        name = self.name
        target = self.target = db.get_for_update(name)
        (attb, relb, amb, ambatts) = db.bindings( [ (name, name) ] )
        assns = self.assns = self.assns.relbind(attb, db)
        cond = self.condition = self.condition.relbind(attb, db)
        constraints = cond.constraints
        if constraints is not None:
            eqs = self.eqs = constraints.eqs
            cassns = constraints.assns
        else:
            cassns = eqs = self.eqs = None
        #print constraints, eqs
        # check that atts of assns are atts of target
        #print dir(assns)
        resultatts = assns.attorder
        from semantics import kjbuckets
        kjSet = kjbuckets.kjSet
        kjGraph = kjbuckets.kjGraph
        resultatts = kjSet(resultatts)
        allatts = kjSet(target.attribute_names)
        self.preserved = allatts - resultatts
        huh = resultatts - allatts
        if huh:
            raise NameError, "%s lacks %s attributes" % (name, huh.items())
        # compute projection
        assnsatts = kjGraph(assns.domain().items()).neighbors(name)
        condatts = kjGraph(cond.domain().items()).neighbors(name)
        condatts = condatts+assnsatts
        #print "condatts", condatts
        translate = kjbuckets.kjDict()
        for att in condatts:
            translate[ (name, att) ] = att
        self.translate = translate
        invtrans= self.invtrans = ~translate
        if cassns is not None:
            self.rassns = invtrans * cassns
        else:
            self.rassns = None
        #print "cassns,rassns", cassns, self.rassns
        #print translate
        # compute domain of self.assns
        # (do nothing with it, should add sanity check!)
        assns_domain = self.assns.domain()
        return self

    def eval(self, dyn=None):
        indb = self.indb
        name = self.name
        cond = self.condition
        cond.uncache()
        assns = self.assns
        assns.uncache()
        translate = self.translate
        preserved = self.preserved
        target = self.target
        rassns = self.rassns
        eqs = self.eqs
        invtrans = self.invtrans
        #print "assns", assns, assns.__class__
        #print "cond", cond
        #print "eqs", eqs
        #print "target", target
        #print "dyn", dyn
        #print "rassns", rassns
        #print "translate", translate
        #print "invtrans", invtrans
        (test, rtups, seqnums, tuples) = evalcond(
            cond, eqs, target, dyn, rassns, translate, invtrans)
        # shortcut
        if not test: return
        self.indb.touched = 1
        tt = type
        from types import IntType
        #print test
        (tps, attorder) = assns.map(test)
        count = 0
        newseqs = list(rtups)
        newtups = list(rtups)
        for i in rtups:
            new = tps[i]
            if tt(new) is not IntType and new is not None:
                seqnum = seqnums[i]
                old = tuples[i]
                if preserved:
                    new = new + preserved*old
                newtups[count] = new
                newseqs[count] = seqnum
                count = count + 1
        if count:
            newseqs = newseqs[:count]
            newtups = newtups[:count]
            target.reset_tuples(newtups, newseqs)
            log = indb.log
            if log is not None and not log.is_scratch:
                from semantics import Reset_Tuples
                op = Reset_Tuples(self.name)
                op.set_data(newtups, newseqs, target)
                log.log(op)


class DeleteOp(semantics.SimpleRecursive):

    def __init__(self, name, where):
        self.name = name
        self.condition = where

    def initargs(self):
        return (self.name, self.condition)

    def __repr__(self):
        return "DELETE FROM %s WHERE %s" % (self.name, self.condition)

    def relbind(self, db):
        self.indb = db
        name = self.name
        target = self.target = db.get_for_update(name)
        (attb, relb, amb, ambatts) = db.bindings( [ (name, name) ] )
        cond = self.condition = self.condition.relbind(attb, db)
        # compute domain of cond
        # do nothing with it (should add sanity check)
        cond_domain = cond.domain()
        constraints = cond.constraints
        if constraints is not None:
            cassns = constraints.assns
            self.eqs = constraints.eqs
        else:
            self.eqs = cassns = None
        # compute projection/rename
        from semantics import kjbuckets
        condatts = kjbuckets.kjGraph(cond.domain().items()).neighbors(name)
        translate = kjbuckets.kjDict()
        for att in condatts:
            translate[(name, att)] = att
        self.translate = translate
        invtrans = self.invtrans = ~translate
        if cassns is not None:
            self.rassns = invtrans * cassns
        else:
            self.rassns = None
        return self

    def eval(self, dyn=None):
        # note, very similar to update case...
        indb = self.indb
        name = self.name
        target = self.target
        tuples = target.tuples
        eqs = self.eqs
        rassns = self.rassns
        cond = self.condition
        cond.uncache()
        translate = self.translate
        invtrans = self.invtrans
        (test, rtups, seqnums, tuples) = evalcond(
           cond, eqs, target, dyn, rassns, translate, invtrans)
        # shortcut
        if not test: return
        indb.touched = 1
        tt = type
        from types import IntType
        count = 0
        newseqs = list(rtups)
        #print "rtups", rtups
        for i in rtups:
            new = test[i]
            if tt(new) is not IntType and new is not None:
                seqnum = seqnums[i]
                newseqs[count] = seqnum
                count = count + 1
        #print "newseqs", newseqs
        #print "count", count
        if count:
            newseqs = newseqs[:count]
            target.erase_tuples(newseqs)
            log = indb.log
            if log is not None and not log.is_scratch:
                from semantics import Erase_Tuples
                op = Erase_Tuples(self.name)
                op.set_data(newseqs, target)
                log.log(op)

INSFMT = """\
INSERT INTO %s
%s
%s"""

class InsertOp(semantics.SimpleRecursive):

    def __init__(self, name, optcolids, insertspec):
        self.name = name
        self.optcolids = optcolids
        self.insertspec = insertspec
        self.target = None # target relation
        self.collector = None # name map for attribute translation

    def initargs(self):
        return (self.name, self.optcolids, self.insertspec)

    def __repr__(self):
        return INSFMT % (self.name, self.optcolids, self.insertspec)

    def relbind(self, db):
        self.indb = db
        name = self.name
        # determine target relation
        target = self.target = db.get_for_update(name)
        targetatts = target.attributes()
        from semantics import kjbuckets
        kjSet = kjbuckets.kjSet
        targetset = kjSet(targetatts)
        # check or set colid bindings
        colids = self.optcolids
        if colids is None:
            colids = self.optcolids = target.attributes()
        colset = kjSet(colids)
        ### for now all attributes must be in colset
        cdiff = colset-targetset
        if cdiff:
            raise NameError, "%s: no such attributes in %s" % (cdiff.items(), name)
        cdiff = targetset-colset
        ### temporary!!!
        if cdiff:
            raise NameError, "%s: not set in insert on %s" % (cdiff.items(), name)
        # bind the insertspec
        insertspec = self.insertspec
        self.insertspec = insertspec = insertspec.relbind(db)
        # create a collector for result
        from semantics import TupleCollector
        collector = self.collector = TupleCollector()
        # get ordered list of expressions to eval on bound attributes of insertspec
        resultexps = insertspec.resultexps()
        if len(resultexps)!=len(colset):
            raise ValueError, "result and colset of differing length %s:%s" % (colset,resultexps)
        pairs = map(None, colids, resultexps)
        for (col,exp) in pairs:
            collector.addbinding(col, exp)
        return self

    def eval(self, dyn=None):
        resultbts = self.insertspec.eval(dyn)
        #print "resultbts", resultbts
        # shortcut
        if not resultbts: return
        indb = self.indb
        indb.touched = 1
        (resulttups, resultatts) = self.collector.map(resultbts)
        #print "resulttups", resulttups
        if resulttups:
            target = self.target
            target.add_tuples(resulttups)
            #target.regenerate_indices()
            log = indb.log
            if log is not None and not log.is_scratch:
                from semantics import Add_Tuples
                op = Add_Tuples(self.name)
                op.set_data(resulttups, target)
                log.log(op)


Insert_dummy_arg = [ ( (1,1), 1 ) ]

class InsertValues(semantics.SimpleRecursive):

    def __init__(self, List):
        self.list = List

    def initargs(self):
        return (self.list,)

    def __repr__(self):
        return "VALUES " +` tuple(self.list) `

    def resultexps(self):
        return self.list

    def relbind(self, db):
        l = self.list
        bindings = {}
        for i in xrange(len(self.list)):
            li = l[i]
            l[i] = li.relbind(bindings, db)
            # do nothing with domain, for now
            li_domain = li.domain()
        return self

    def eval(self, dyn=None):
        if dyn:
            from semantics import dynamic_binding
            dynbt = dynamic_binding(len(dyn), dyn)
        else:
            # dummy value to prevent triviality
            from semantics import kjbuckets
            dynbt = [kjbuckets.kjDict(Insert_dummy_arg)]
        #print "bindings", dynbt.assns
        return dynbt # ??

class InsertSubSelect(semantics.SimpleRecursive):

    def __init__(self, subsel):
        self.subsel = subsel

    def initargs(self):
        return (self.subsel,)

    def __repr__(self):
        return "[subsel] %s" % (self.subsel,)

    def resultexps(self):
        # get list of result bindings
        subsel = self.subsel
        atts = self.subsel.attributes()
        # bind each as "result.name"
        exps = []
        from semantics import BoundAttribute
        for a in atts:
            exps.append( BoundAttribute("result", a) )
        return exps # temp

    def relbind(self, db):
        subsel = self.subsel
        self.subsel = subsel.relbind(db)
        # do nothing with domain for now
        #subsel_domain = subsel.domain()
        return self

    def eval(self, dyn=None):
        subsel = self.subsel
        subsel.uncache()
        rel = subsel.eval(dyn)
        tups = rel.rows()
        from semantics import BoundTuple ### temp
        from semantics import kjbuckets
        kjDict = kjbuckets.kjDict
        for i in xrange(len(tups)):
            tupsi = tups[i]
            new = kjDict()
            for k in tupsi.keys():
                new[ ("result", k) ] = tupsi[k]
            tups[i] = new
        return tups

# ordering for archiving datadefs
ddf_order = [CreateTable, CreateIndex, CreateView]

#
# $Log: operations.py,v $
# Revision 1.4  2002/05/11 02:59:05  richard
# Added info into module docstrings.
# Fixed docco of kwParsing to reflect new grammar "marshalling".
# Fixed bug in gadfly.open - most likely introduced during sql loading
# re-work (though looking back at the diff from back then, I can't see how it
# wasn't different before, but it musta been ;)
# A buncha new unit test stuff.
#
# Revision 1.3  2002/05/08 00:49:00  anthonybaxter
# El Grande Grande reindente! Ran reindent.py over the whole thing.
# Gosh, what a lot of checkins. Tests still pass with 2.1 and 2.2.
#
# Revision 1.2  2002/05/07 23:19:02  richard
# Removed circular import (at import time at least)
#
# Revision 1.1.1.1  2002/05/06 07:31:09  richard
#
#
#
