# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Reddit.
#
# The Original Developer is the Initial Developer.  The Initial Developer of the
# Original Code is CondeNet, Inc.
#
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from datetime import datetime
from socket import gethostbyaddr

from pylons import g

from pycassa import ColumnFamily
from pycassa.cassandra.ttypes import ConsistencyLevel, NotFoundException
from pycassa.system_manager import SystemManager, UTF8_TYPE

from r2.lib.utils import tup, Storage
from r2.lib.db.sorts import epoch_seconds
from r2.lib import cache
from uuid import uuid1
from itertools import chain
import cPickle as pickle

cassandra = g.cassandra
thing_cache = g.thing_cache
seeds = g.cassandra_seeds
keyspace = 'reddit'
disallow_db_writes = g.disallow_db_writes
tz = g.tz
log = g.log
read_consistency_level = g.cassandra_rcl
write_consistency_level = g.cassandra_wcl
debug = g.debug
make_lock = g.make_lock
db_create_tables = g.db_create_tables

thing_types = {}

# The available consistency levels
CL = Storage(ANY    = ConsistencyLevel.ANY,
             ONE    = ConsistencyLevel.ONE,
             QUORUM = ConsistencyLevel.QUORUM,
             ALL    = ConsistencyLevel.ALL)

# the greatest number of columns that we're willing to accept over the
# wire for a given row (this should be increased if we start working
# with classes with lots of columns, like Account which has lots of
# karma_ rows, or we should not do that)
max_column_count = 10000

class CassandraException(Exception):
    """Base class for Exceptions in tdb_cassandra"""
    pass

class InvariantException(CassandraException):
    """Exceptions that can only be caused by bugs in tdb_cassandra"""
    pass

class ConfigurationException(CassandraException):
    """Exceptions that are caused by incorrect configuration on the
       Cassandra server"""
    pass

class TdbException(CassandraException):
    """Exceptions caused by bugs in our callers or subclasses"""
    pass

class NotFound(CassandraException, NotFoundException):
    """Someone asked us for an ID that isn't stored in the DB at
       all. This is probably an end-user's fault."""
    pass

def will_write(fn):
    """Decorator to indicate that a given function intends to write
       out to Cassandra"""
    def _fn(*a, **kw):
        if disallow_db_writes:
            raise CassandraException("Not so fast! DB writes have been disabled")
        return fn(*a, **kw)
    return _fn

def get_manager():
    # n.b. does not retry against multiple servers
    server = seeds[0]
    return SystemManager(server)

class ThingMeta(type):
    def __init__(cls, name, bases, dct):
        type.__init__(cls, name, bases, dct)

        if cls._use_db:
            if cls._type_prefix is None:
                # default to the class name
                cls._type_prefix = name

            if '_' in cls._type_prefix:
                raise TdbException("Cannot have _ in type prefix %r (for %r)"
                                   % (cls._type_prefix, name))

            if cls._type_prefix in thing_types:
                raise InvariantException("Redefining type %r?" % (cls._type_prefix))

            # if we weren't given a specific _cf_name, we can use the
            # classes's name
            cf_name = cls._cf_name or name

            thing_types[cls._type_prefix] = cls

            if not getattr(cls, "_read_consistency_level", None):
                cls._read_consistency_level = read_consistency_level
            if not getattr(cls, "_write_consistency_level", None):
                cls._write_consistency_level = write_consistency_level

            try:
                cls._cf = ColumnFamily(cassandra,
                                       cf_name,
                                       read_consistency_level = cls._read_consistency_level,
                                       write_consistency_level = cls._write_consistency_level)
            except NotFoundException:
                if not db_create_tables:
                    raise

                manager = get_manager()

                log.warning("Creating Cassandra Column Family %s" % (cf_name,))
                with make_lock('cassandra_schema'):
                    manager.create_column_family(keyspace, cf_name,
                                                 comparator_type = cls._compare_with)
                log.warning("Created Cassandra Column Family %s" % (cf_name,))

                # try again to look it up
                cls._cf = ColumnFamily(cassandra,
                                       cf_name,
                                       read_consistency_level = cls._read_consistency_level,
                                       write_consistency_level = cls._write_consistency_level)

        cls._kind = name

    def __repr__(cls):
        return '<thing: %s>' % cls.__name__

class ThingBase(object):
    # base class for Thing and Relation

    __metaclass__ = ThingMeta

    _cf_name = None # the name of the ColumnFamily; defaults to the
                    # name of the class

    # subclasses must replace these

    _type_prefix = None # this must be present for classes with _use_db==True

    _use_db = False

    # the Cassandra column-comparator (internally orders column
    # names). In real life you can't change this without some changes
    # to tdb_cassandra to support other attr types
    _compare_with = UTF8_TYPE

    _value_type = None # if set, overrides all of the _props types
                       # below. Used for Views. One of 'int', 'float',
                       # 'bool', 'pickle', 'date', 'bytes', 'str'

    _int_props = ()
    _float_props = () # note that we can lose resolution on these
    _bool_props = ()
    _pickle_props = ()
    _date_props = () # note that we can lose resolution on these
    _bytes_props = ()
    _str_props = () # at present we never actually read out of here
                    # since it's the default if none of the previous
                    # matches

    # the value that we assume a property to have if it is not found
    # in the DB. Note that we don't do type-checking here, so if you
    # want a default to be a boolean and want it to be storable you'll
    # also have to set it in _bool_props
    _defaults = {}

    # The default TTL in seconds to add to all columns. Note: if an
    # entire object is expected to have a TTL, it should be considered
    # immutable! (You don't want to write out an object with an author
    # and date, then go update author or add a new column, then have
    # the original columns expire. Then when you go to look it up, the
    # inherent properties author and/or date will be gone and only the
    # updated columns will be present.) This is an expected convention
    # and is not enforced.
    _ttl = None

    # A per-class dictionary of default TTLs that new columns of this
    # class should have
    _default_ttls = {}

    # A per-instance property defining the TTL of individual columns
    # (that must also appear in self._dirties)
    _column_ttls = {}

    # a timestamp property that will automatically be added to newly
    # created Things (disable by setting to None)
    _timestamp_prop = None

    # a per-instance property indicating that this object was
    # partially loaded: i.e. only some properties were requested from
    # the DB
    _partial = None

    # a per-instance property that specifies that the columns backing
    # these attributes are to be removed on _commit()
    _deletes = set()

    def __init__(self, _id = None, _committed = False, _partial = None, **kw):
        # things that have changed
        self._dirties = kw.copy()

        # what the original properties were when we went to Cassandra to
        # get them
        self._orig = {}

        self._defaults = self._defaults.copy()

        # whether this item has ever been created
        self._committed = _committed

        self._partial = None if _partial is None else frozenset(_partial)

        self._deletes = set()
        self._column_ttls = {}

        # our row key
        self._id = _id

        if not self._use_db:
            raise TdbException("Cannot make instances of %r" % (self.__class__,))

    @classmethod
    def _byID(cls, ids, properties=None):
        ids, is_single = tup(ids, True)

        if properties is not None:
            asked_properties = frozenset(properties)
            willask_properties = set(properties)

        if not len(ids):
            if is_single:
                raise InvariantException("whastis?")
            return {}

        # all keys must be strings or directly convertable to strings
        assert all(isinstance(_id, basestring) and str(_id) for _id in ids)

        def reject_bad_partials(cached, still_need):
            # tell sgm that the match it found in the cache isn't good
            # enough if it's a partial that doesn't include our
            # properties. we still need to look those items up to get
            # the properties that we're after
            stillfind = set()

            for k, v in cached.iteritems():
                if properties is None:
                    if v._partial is not None:
                        # there's a partial in the cache but we're not
                        # looking for partials
                        stillfind.add(k)
                elif v._partial is not None and not asked_properties.issubset(v._partial):
                    # we asked for a partial, and this is a partial,
                    # but it doesn't have all of the properties that
                    # we need
                    stillfind.add(k)

                    # other callers in our request are now expecting
                    # to find the properties that were on that
                    # partial, so we'll have to preserve them
                    for prop in v._partial:
                        willask_properties.add(prop)

            for k in stillfind:
                del cached[k]
                still_need.add(k)

        def lookup(l_ids):
            # TODO: if we get back max_column_count columns for a
            # given row, check a flag on the class as to whether to
            # refetch for more of them. This could be important with
            # large Views, for instance

            if properties is None:
                rows = cls._cf.multiget(l_ids, column_count=max_column_count)
            else:
                rows = cls._cf.multiget(l_ids, columns = willask_properties)

            l_ret = {}
            for t_id, row in rows.iteritems():
                t = cls._from_serialized_columns(t_id, row)
                if properties is not None:
                    # make sure that the item is marked as a _partial
                    t._partial = willask_properties
                l_ret[t._id] = t

            return l_ret

        ret = cache.sgm(thing_cache, ids, lookup, prefix=cls._cache_prefix(),
                        found_fn=reject_bad_partials)

        if is_single and not ret:
            raise NotFound("<%s %r>" % (cls.__name__,
                                        ids[0]))
        elif is_single:
            assert len(ret) == 1
            return ret.values()[0]

        return ret

    @property
    def _fullname(self):
        if self._type_prefix is None:
            raise TdbException("%r has no _type_prefix, so fullnames cannot be generated"
                               % self.__class__)

        return '%s_%s' % (self._type_prefix, self._id)

    @classmethod
    def _by_fullname(cls, fnames):
        ids, is_single = tup(fnames, True)

        by_cls = {}
        for i in ids:
            typ, underscore, _id = i.partition('_')
            assert underscore == '_'

            by_cls.setdefault(thing_types[typ], []).append(_id)

        items = []
        for typ, ids in by_cls.iteritems():
            items.extend(typ._byID(ids).values())

        return items[0] if is_single else dict((x._fullname, x)
                                               for x in items)

    @classmethod
    def _cache_prefix(cls):
        return 'tdbcassandra_' + cls._type_prefix + '_'

    def _cache_key(self):
        if not self._id:
            raise TdbException('no cache key for uncommitted %r' % (self,))

        return self._cache_key_id(self._id)

    @classmethod
    def _cache_key_id(cls, t_id):
        return cls._cache_prefix() + t_id

    @classmethod
    def _wcl(cls, wcl, default = None):
        if wcl is not None:
            return wcl
        elif default is not None:
            return default
        return cls._write_consistency_level

    def _rcl(cls, rcl, default = None):
        if rcl is not None:
            return rcl
        elif default is not None:
            return default
        return cls._read_consistency_level

    @classmethod
    def _deserialize_column(cls, attr, val):
        if attr in cls._int_props or (cls._value_type and cls._value_type == 'int'):
            try:
                return int(val)
            except ValueError:
                return long(val)
        elif attr in cls._float_props or (cls._value_type and cls._value_type == 'float'):
            return float(val)
        elif attr in cls._bool_props or (cls._value_type and cls._value_type == 'bool'):
            # note that only the string "1" is considered true!
            return val == '1'
        elif attr in cls._pickle_props or (cls._value_type and cls._value_type == 'pickle'):
            return pickle.loads(val)
        elif attr in cls._date_props or attr == cls._timestamp_prop or (cls._value_type and cls._value_type == 'date'):
            as_float = float(val)
            return datetime.utcfromtimestamp(as_float).replace(tzinfo = tz)
        elif attr in cls._bytes_props or (cls._value_type and cls._value_type == 'bytes'):
            return val

        # otherwise we'll assume that it's a utf-8 string
        return val.decode('utf-8')

    @classmethod
    def _serialize_column(cls, attr, val):
        if (attr in chain(cls._int_props, cls._float_props) or
            (cls._value_type and cls._value_type in ('float', 'int'))):
            return str(val)
        elif attr in cls._bool_props or (cls._value_type and cls._value_type == 'bool'):
            # n.b. we "truncate" this to a boolean, so truthy but
            # non-boolean values are discarded
            return '1' if val else '0'
        elif attr in cls._pickle_props or (cls._value_type and cls._value_type == 'pickle'):
            return pickle.dumps(val)
        elif (attr in cls._date_props or attr == cls._timestamp_prop or
              (cls._value_type and cls._value_type == 'date')):
            # the _timestamp_prop is handled in _commit(), not here
            return cls._serialize_date(val)
        elif attr in cls._bytes_props or (cls._value_type and cls._value_type == 'bytes'):
            return val

        return unicode(val).encode('utf-8')

    @classmethod
    def _serialize_date(cls, date):
        return str(epoch_seconds(date))

    @classmethod
    def _deserialize_date(cls, val):
        as_float = float(val)
        return datetime.utcfromtimestamp(as_float).replace(tzinfo = tz)

    @classmethod
    def _from_serialized_columns(cls, t_id, columns):
        d_columns = dict((attr, cls._deserialize_column(attr, val))
                         for (attr, val)
                         in columns.iteritems())
        return cls._from_columns(t_id, d_columns)

    @classmethod
    def _from_columns(cls, t_id, columns):
        """Given a dictionary of freshly deserialized columns
           construct an instance of cls"""
        # if modifying this, check Relation._from_columns and see if
        # you should change it as well
        t = cls()
        t._orig = columns
        t._id = t_id
        t._committed = True
        return t

    @property
    def _dirty(self):
        return len(self._dirties) or len(self._deletes) or not self._committed

    @will_write
    def _commit(self, write_consistency_level = None):
        if not self._dirty:
            return

        if self._id is None:
            raise TdbException("Can't commit %r without an ID" % (self,))

        if self._committed and self._ttl:
            log.warning("Using a full-TTL object %r in a mutable fashion"
                        % (self,))

        if not self._committed:
            # if this has never been committed we should also consider
            # the _orig columns as dirty (but "less dirty" than the
            # _dirties)
            upd = self._orig.copy()
            self._orig.clear()
            upd.update(self._dirties)
            self._dirties = upd

        # Cassandra values are untyped byte arrays, so we need to
        # serialize everything while filtering out anything that's
        # been dirtied but doesn't actually differ from what's already
        # in the DB
        updates = dict((attr, self._serialize_column(attr, val))
                       for (attr, val)
                       in self._dirties.iteritems()
                       if (attr not in self._orig or
                           val != self._orig[attr]))

        # n.b. deleted columns are applied *after* the updates. our
        # __setattr__/__delitem__ tries to make sure that this always
        # works

        if not self._committed and self._timestamp_prop and self._timestamp_prop not in updates:
            # auto-create timestamps on classes that request them

            # this serialize/deserialize is a bit funny: the process
            # of storing and retrieving causes us to lose some
            # resolution because of the floating-point representation,
            # so this is just to make sure that we have the same value
            # that the DB does after writing it out. Note that this is
            # the only property munged this way: other timestamp and
            # floating point properties may lose resolution
            s_now = self._serialize_date(datetime.now(tz))
            now = self._deserialize_date(s_now)

            updates[self._timestamp_prop] = s_now
            self._dirties[self._timestamp_prop] = now

        if not updates and not self._deletes:
            return

        # actually write out the changes to the CF
        wcl = self._wcl(write_consistency_level)
        with self._cf.batch(write_consistency_level = wcl) as b:
            if updates:
                for k, v in updates.iteritems():
                    b.insert(self._id,
                             {k: v},
                             ttl=self._column_ttls.get(k, self._ttl))
            if self._deletes:
                b.remove(self._id, self._deletes)

        self._orig.update(self._dirties)
        self._column_ttls.clear()
        self._dirties.clear()
        for k in self._deletes:
            try:
                del self._orig[k]
            except KeyError:
                pass
        self._deletes.clear()

        if not self._committed:
            self._on_create()

        self._committed = True

        thing_cache.set(self._cache_key(), self)

    def _revert(self):
        if not self._committed:
            raise TdbException("Revert to what?")

        self._dirties.clear()
        self._deletes.clear()
        self._column_ttls.clear()

    def __getattr__(self, attr):
        if attr.startswith('_'):
            try:
                return self.__dict__[attr]
            except KeyError:
                raise AttributeError, attr

        if attr in self._deletes:
            raise AttributeError("%r has no %r because you deleted it", (self, attr))
        elif attr in self._dirties:
            return self._dirties[attr]
        elif attr in self._orig:
            return self._orig[attr]
        elif attr in self._defaults:
            return self._defaults[attr]
        elif self._partial is not None and attr not in self._partial:
            raise AttributeError("%r has no %r but you didn't request it" % (self, attr))
        else:
            raise AttributeError('%r has no %r' % (self, attr))

    def __setattr__(self, attr, val):
        if attr == '_id' and self._committed:
            raise ValueError('cannot change _id on a committed %r' % (self.__class__))

        if attr.startswith('_'):
            return object.__setattr__(self, attr, val)

        try:
            self._deletes.remove(attr)
        except KeyError:
            pass
        self._dirties[attr] = val
        if attr in self._default_ttls:
            self._column_ttls[attr] = self._default_ttls[attr]

    def __eq__(self, other):
        if self.__class__ != other.__class__:
            return False

        if self._partial or other._partial and self._partial != other._partial:
            raise ValueError("Can't compare incompatible partials")

        return self._id == other._id and self._t == other._t

    def __ne__(self, other):
        return not (self == other)

    @property
    def _t(self):
        """Emulate the _t property from tdb_sql: a dictionary of all
           values that are or will be stored in the database, (not
           including _defaults or unrequested properties on
           partials)"""
        ret = self._orig.copy()
        ret.update(self._dirties)
        for k in self._deletes:
            try:
                del ret[k]
            except KeyError:
                pass
        return ret

    # allow the dictionary mutation syntax; it makes working some some
    # keys a bit easier. Go through our regular
    # __getattr__/__setattr__ functions where all of the appropriate
    # work is done
    def __getitem__(self, key):
        return self.__getattr__(key)

    def __setitem__(self, key, value):
        return self.__setattr__(key, value)

    def _get(self, key, default = None):
        try:
            return self.__getattr__(key)
        except AttributeError:
            if self._partial is not None and key not in self._partial:
                raise AttributeError("_get on unrequested key from partial")
            return default

    def _set_ttl(self, key, ttl):
        assert key in self._dirties
        assert isinstance(ttl, (long, int))
        self._column_ttls[key] = ttl

    def _on_create(self):
        """A hook executed on creation, good for creation of static
           Views. Subclasses should call their parents' hook(s) as
           well"""
        pass

    @classmethod
    def _all(cls):
        # returns a query object yielding every single item in a
        # column family. it probably shouldn't be used except in
        # debugging
        return Query(cls, limit=None)

    def __repr__(self):
        # it's safe for subclasses to override this to e.g. put a Link
        # title or Account name in the repr(), but they must be
        # careful to check hasattr for the properties that they read
        # out, as __getattr__ itself may call __repr__ in constructing
        # its error messages
        id_str = self._id
        comm_str = '' if self._committed else ' (uncommitted)'
        part_str = '' if self._partial is None else ' (partial)'
        return "<%s %r%s%s>" % (self.__class__.__name__,
                              id_str,
                              comm_str, part_str)

    if debug:
        # we only want this with g.debug because overriding __del__
        # can play hell with memory leaks
        def __del__(self):
                if not self._committed:
                    # normally we'd log this with g.log or something,
                    # but we can't guarantee that the thread
                    # destructing us has access to g
                    print "Warning: discarding uncomitted %r; this is usually a bug" % (self,)
                elif self._dirty:
                    print ("Warning: discarding dirty %r; this is usually a bug (_dirties=%r, _deletes=%r)"
                           % (self,self._dirties,self._deletes))

class Thing(ThingBase):
    _timestamp_prop = 'date'

class Relation(ThingBase):
    _timestamp_prop = 'date'

    def __init__(self, thing1_id, thing2_id, **kw):
        # NB! When storing relations between postgres-backed Thing
        # objects, these IDs are actually ID36s
        ThingBase.__init__(self,
                           _id = self._rowkey(thing1_id, thing2_id),
                           **kw)
        self._orig['thing1_id'] = thing1_id
        self._orig['thing2_id'] = thing2_id

    @will_write
    def _destroy(self, write_consistency_level = None):
        # only implemented on relations right now, but at present
        # there's no technical reason for this
        self._cf.remove(self._id,
                        write_consistency_level = self._wcl(write_consistency_level))
        self._on_destroy()
        thing_cache.delete(self._cache_key())

    def _on_destroy(self):
        """Called *after* the destruction of the Thing on the
           destroyed Thing's mortal shell"""
        # only implemented on relations right now, but at present
        # there's no technical reason for this
        pass

    @classmethod
    def _fast_query(cls, thing1_ids, thing2_ids, properties = None, **kw):
        """Find all of the relations of this class between all of the
           members of thing1_ids and thing2_ids"""
        thing1_ids, thing1s_is_single = tup(thing1_ids, True)
        thing2_ids, thing2s_is_single = tup(thing2_ids, True)

        if not thing1_ids or not thing2_ids:
            # nothing to permute
            return {}

        if properties is not None:
            properties = set(properties)

            # all relations must load these properties, even if
            # unrequested
            properties.add('thing1_id')
            properties.add('thing2_id')

        # permute all of the pairs
        ids = set(cls._rowkey(x, y)
                  for x in thing1_ids
                  for y in thing2_ids)

        rels = cls._byID(ids, properties = properties).values()

        if thing1s_is_single and thing2s_is_single:
            if rels:
                assert len(rels) == 1
                return rels[0]
            else:
                raise NotFound("<%s %r>" % (cls.__name__,
                                            cls._rowkey(thing1_ids[0],
                                                        thing2_ids[0])))

        return dict(((rel.thing1_id, rel.thing2_id), rel)
                    for rel in rels)

    @classmethod
    def _from_columns(cls, t_id, columns):
        # we deserialize relations a little specially so that we can
        # throw our toys on the floor if they don't have thing1_id and
        # thing2_id
        if not ('thing1_id' in columns and 'thing2_id' in columns
                and t_id == cls._rowkey(columns['thing1_id'], columns['thing2_id'])):
            raise InvariantException("Looked up %r with unmatched IDs (%r)"
                                     % (cls, t_id))

        # if modifying this, check ThingBase._from_columns and see if
        # you should change it as well
        thing1_id, thing2_id = columns['thing1_id'], columns['thing2_id']
        t = cls(thing1_id = thing1_id, thing2_id = thing2_id)
        assert t._id == t_id
        t._orig = columns
        t._committed = True
        return t

    @staticmethod
    def _rowkey(thing1_id36, thing2_id36):
        assert isinstance(thing1_id36, basestring) and isinstance(thing2_id36, basestring)
        return '%s_%s' % (thing1_id36, thing2_id36)

    def _commit(self, *a, **kw):
        assert self._id == self._rowkey(self.thing1_id, self.thing2_id)

        return ThingBase._commit(self, *a, **kw)

    @classmethod
    def _rel(cls, thing1_cls, thing2_cls):
        # should be implemented by abstract relations, like Vote
        raise NotImplementedError

    @classmethod
    def _datekey(cls, date):
        # ick
        return str(long(cls._serialize_date(date)))

class Query(object):
    """A query across a CF. Note that while you can query rows from a
       CF that has a RandomPartitioner, you won't get them in any sort
       of order"""
    def __init__(self, cls, after=None, properties=None, limit=100,
                 chunk_size=100, _max_column_count = max_column_count):
        self.cls = cls
        self.after = after
        self.properties = properties
        self.limit = limit
        self.chunk_size = chunk_size
        self.max_column_count = _max_column_count

    def __copy__(self):
        return Query(self.cls, after=self.after,
                     properties = self.properties,
                     limit=self.limit,
                     chunk_size=self.chunk_size,
                     _max_column_count = self.max_column_count)
    copy = __copy__

    def _dump(self):
        q = self.copy()
        q.after = q.limit = None

        for row in q:
            print row
            for col, val in row._t.iteritems():
                print '\t%s: %r' % (col, val)

    @will_write
    def _delete_all(self, write_consistency_level = None):
        # uncomment to use on purpose
        raise InvariantException("Nice try, FBI")

        # TODO: this could use cf.truncate instead and be *way*
        # faster, but it wouldn't flush the thing_cache at the same
        # time that way

        q = self.copy()
        q.after = q.limit = None

        # since we're just deleting it, we only need enough columns to
        # avoid reading ghost rows
        q.max_column_count = 1

        # I'm going to guess that if they are trying to flush out an
        # entire CF, they aren't that worried about how long it takes
        # to become consistent. So we'll default to the fastest way
        wcl = q.cls._wcl(write_consistency_level, CL.ONE)

        for row in q:
            print row

            # n.b. we're not calling _on_destroy!
            q.cls._cf.remove(row._id, write_consistency_level = wcl)
            thing_cache.delete(q.cls._cache_key_id(row._id))

    def __iter__(self):
        # n.b.: we aren't caching objects that we find this way in the
        # LocalCache. This may will need to be changed if we ever
        # start using OPP in Cassandra (since otherwise these types of
        # queries aren't useful for anything but debugging anyway)
        after = '' if self.after is None else self.after._id
        limit = self.limit

        if self.properties is None:
            r = self.cls._cf.get_range(start=after, row_count=limit,
                                       column_count = self.max_column_count)
        else:
            r = self.cls._cf.get_range(start=after, row_count=limit,
                                       columns = self.properties)

        for t_id, columns in r:
            if not columns:
                # a ghost row
                continue

            t = self.cls._from_serialized_columns(t_id, columns)
            yield t

class View(ThingBase):
    # Views are Things like any other, but may have special key
    # characteristics

    # these default to not having a timestamp column
    _timestamp_prop = None

    _value_type = 'str'

    def _values(self):
        """Retrieve the entire contents of the view"""
        # TODO: at present this only grabs max_column_count columns
        return self._t

    @staticmethod
    def _gen_uuid():
        """Convenience method for generating UUIDs for view
           keys. Generates time-based UUIDs, safe for use as TimeUUID
           indices in Cassandra"""
        
        return uuid1()

    @classmethod
    @will_write
    def _set_values(cls, row_key, col_values,
                    write_consistency_level = None,
                    ttl=None):
        """Set a set of column values in a row of a View without
           looking up the whole row first"""
        # col_values =:= dict(col_name -> col_value)

        updates = dict((col_name, cls._serialize_column(col_name, col_val))
                       for (col_name, col_val) in col_values.iteritems())

        # if they didn't give us a TTL, use the default TTL for the
        # class. This will be further overwritten below per-column
        # based on the _default_ttls class dict. Note! There is no way
        # to use this API to express that you don't want a TTL if
        # there is a default set on either the row or the column
        default_ttl = None if ttl is None else self._ttl

        with cls._cf.batch(write_consistency_level = cls._wcl(write_consistency_level)) as b:
            # with some quick tweaks we could have a version that
            # operates across multiple row keys, but this is not it
            for k, v in updates.iteritems():
                ttl = cls._default_ttls.get(k, default_ttl)
                b.insert(row_key,
                         {k: v},
                         ttl = cls._default_ttls.get(k, default_ttl))

        # can we be smarter here?
        thing_cache.delete(cls._cache_key_id(row_key))

    def __delitem__(self, key):
        # only implemented on Views right now, but at present there's
        # no technical reason for this
        try:
            del self._dirties[key]
        except KeyError:
            pass
        try:
            del self._column_ttls[key]
        except KeyError:
            pass
        self._deletes.add(key)

def schema_report():
    manager = get_manager()
    print manager.describe_keyspace(keyspace)

def ring_report():
    # uses a silly algorithm to pick natural endpoints that requires N>=RF+1

    sizes = {}
    nodes = {} # token -> node

    manager = get_manager()

    ring = manager.describe_ring(keyspace)
    ring.sort(key=lambda tr: long(tr.start_token))

    for x, tr in enumerate(ring):
        next = ring[x+1] if x < len(ring)-1 else ring[0]

        # tr = ring1[x]
        # next = ring1[x+1]

        s = long(tr.start_token)
        e = long(tr.end_token)

        if e > s:
            # a regular range
            l = e-s
        else:
            # range that overlaps 0
            l = e+2**127-s

        for ep in tr.endpoints:
            sizes.setdefault(ep, []).append(float(l)/2**127)

        natural = set(tr.endpoints) - set(next.endpoints)
        assert len(natural) == 1
        natural = natural.pop()

        nodes[e] = natural

    totalsize = sum(map(sum, sizes.values()))
    for token, ip in sorted(nodes.items(),
                              key = lambda x: x[0]):
        size = sizes[ip]
        name = gethostbyaddr(ip)[0]

        fmt_perc = lambda num: '%s%2.2f%%' % (' ' if num < 10 else '',
                                              num)
        maxtoklen = len(str(2**127))

        print '%16s\t%s\t%s\t%s' % (ip, name,
                                    fmt_perc(sum(size)/totalsize * 100),
                                    str(token).rjust(maxtoklen))
