Source

django-mongodb / django_mongodb / base.py

Full commit
#-*- coding:utf-8 -*-

# Copyright (c) 2009, Kirill Mavreshko (kimavr@gmail.com)
# All rights reserved.

# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions are met:

#    * Redistributions of source code must retain the above copyright notice, 
#      this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#      this list of conditions and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#    * The name of the author may not be used to endorse or promote products
#      derived from this software without specific prior written permission.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
# POSSIBILITY OF SUCH DAMAGE.

"""
django_mongodb.base -- Django to MongoDB (http://www.mongodb.org) database backend.
"""

import sys, time

from django.core.exceptions import ImproperlyConfigured
from django.db.backends import *
from django.db.backends.creation import BaseDatabaseCreation, TEST_DATABASE_PREFIX

def complain(*args, **kwargs):
    raise ImproperlyConfigured("django_mongodb is not supporting all Django ORM abilities.")

def ignore(*args, **kwargs):
    pass

class DatabaseError(Exception):
    pass

class IntegrityError(DatabaseError):
    pass

class MongoDatabaseOperations(BaseDatabaseOperations):
    quote_name = complain

    def query_class(self, DefaultQueryClass):
        from django_mongodb.query import MongoDbQuery
        return MongoDbQuery

    def quote_name(self, name):
        return name

    def value_to_db_datetime(self, value):
        return value

    def sql_flush(self, *args, **kwargs):
        return []


class MongoDatabaseCreation(BaseDatabaseCreation):
    sql_for_inline_foreign_key_references = ignore
    sql_for_many_to_many = ignore
    sql_for_many_to_many_field = ignore
    sql_for_inline_many_to_many_references = ignore
    sql_remove_table_constraints = ignore
    sql_destroy_many_to_many = ignore
    

    sql_indexes_for_model = ignore
    sql_indexes_for_field = ignore
    sql_destroy_model = ignore

    _rollback_works = lambda self: False
    
    def _create_test_db(self, verbosity, autoclobber):
        "See django.db.backends.creation.BaseDatabaseCreation._create_test_db"
        from django.conf import settings
        suffix = self.sql_table_creation_suffix()

        if settings.TEST_DATABASE_NAME:
            test_database_name = settings.TEST_DATABASE_NAME
        else:
            test_database_name = TEST_DATABASE_PREFIX + settings.DATABASE_NAME

        mongodb_connection = self.connection.connection
        
        if test_database_name in mongodb_connection.database_names():
            sys.stderr.write("Test database already exists: %s\n" % test_database_name)
            if not autoclobber:
                confirm = raw_input("Type 'yes' if you would like to try deleting the test database '%s', or 'no' to cancel: " % test_database_name)
            if autoclobber or confirm == 'yes':
                try:
                    if verbosity >= 1:
                        print "Destroying old test database..."
                    mongodb_connection.drop_database(test_database_name)
                    if verbosity >= 1:
                        print "Creating test database..."
                    mongodb_connection['test']
                except Exception, e:
                    sys.stderr.write("Got an error recreating the test database: %s\n" % e)
                    sys.exit(2)
            else:
                print "Tests cancelled."
                sys.exit(1)

        return test_database_name


    def _destroy_test_db(self, test_database_name, verbosity):
        "See django.db.backends.creation.BaseDatabaseCreation._destroy_test_db"
        mongodb_connection = self.connection.connection
        time.sleep(1)
        mongodb_connection.drop_database(test_database_name)
        self.connection.close()

    def sql_create_model(self, *args, **kwargs): 
        return [],{}

    def sql_for_pending_references(self, *args, **kwargs):
        return []


class MongoCollectionDebugWrapper(object):
    """
    Wrapper around pymongo.collection.Collection for Django debug mode query logging.
    Used automatically in Django debug mode (DEBUG=True).
    """

    MONGO_LOGGED_CALLS = ("find", "remove", "update", "save")

    def __init__(self, database, queries):
        self._wrapped_mongo_collection = database
        self._debug_queries_list = queries

    def _mongo_request_args_to_sql(self, method, *args, **kwargs):
        "Format call of method and append result message to django.db.connection.queries"
        formatted_args = u", ".join([repr(a) for a in args])
        formatted_kwargs = u", ".join([u"%s=%s" % (k,repr(v)) for k,v in kwargs.items()])
        col_name = self._wrapped_mongo_collection.name()
        return u"db.%s.%s(%r)" % (col_name, method, u", ".join([formatted_args, formatted_kwargs]))

    def _call_logger(self, func):
        "Logging decorator for all calls of MONGO_LOGGED_CALLS methods."
        def logger(*args, **kwargs):
            start = time.time()
            try:
                return func(*args, **kwargs)
            finally:
                stop = time.time()
                sql = self._mongo_request_args_to_sql(func.func_name, *args, **kwargs)
                self._debug_queries_list.append({
                        'sql': sql,
                        'time': "%.6f" % (stop - start),
                        })
                #print self._debug_queries_list
        return logger

    def __getattr__(self, attr):
        if attr in self.__dict__:
            return self.__dict__[attr]
        else:
            rval = getattr(self._wrapped_mongo_collection, attr)
            if attr in self.MONGO_LOGGED_CALLS:
                rval = self._call_logger(rval)
            return rval


class MongoDatabaseDebugWrapper(object):
    """
    Wrapper around pymongo.collection.Database for Django debug mode query logging.
    Used automatically in Django debug mode (DEBUG=True).
    """

    MONGO_LOGGED_CALLS = ("find", "remove", "update", "save")

    def __init__(self, database, queries):
        self._wrapped_mongo_database = database
        self._debug_queries_list = queries

    def __getattr__(self, attr):
        from pymongo.collection import Collection
        rval = getattr(self._wrapped_mongo_database, attr)
        if isinstance(rval, Collection):
            rval = MongoCollectionDebugWrapper(rval, self._debug_queries_list)
        return rval

    def __getitem__(self, name):
        return self.__getattr__(name)


class DatabaseClient(BaseDatabaseClient):
    runshell = complain


class MongoDatabaseIntrospection(BaseDatabaseIntrospection):
    get_table_list = complain
    get_table_description = complain
    get_relations = complain
    get_indexes = complain

    def table_names(self):
        return self.connection.cursor().collection_names()


class MongoDatabaseFeatures(BaseDatabaseFeatures):
    uses_custom_query_class = True

    allows_group_by_pk = False
    # True if django.db.backend.utils.typecast_timestamp is used on values
    # returned from dates() calls.
    needs_datetime_string_cast = True
    
    empty_fetchmany_value = []
    update_can_self_select = True
    interprets_empty_strings_as_nulls = False
    can_use_chunked_reads = False
    can_return_id_from_insert = True
    uses_autocommit = True
    uses_savepoints = False
    # If True, don't use integer foreign keys referring to, e.g., positive
    # integer primary keys.
    related_fields_match_type = False


class DatabaseWrapper(BaseDatabaseWrapper):
    _commit = ignore
    _rollback = ignore
    operators = {}

    def __init__(self, settings_dict):
        self.features = MongoDatabaseFeatures()
        self.ops = MongoDatabaseOperations()
        self.client = DatabaseClient(self)
        self.creation = MongoDatabaseCreation(self)
        self.introspection = MongoDatabaseIntrospection(self)
        self.validation = BaseDatabaseValidation()

        self._connection = None
        self.queries = []
        self.settings_dict = settings_dict

    def close(self):
        if self._connection is not None:
            del self._connection
            self._connection = None

    def make_debug_cursor(self, cursor):
        return MongoDatabaseDebugWrapper(cursor, self.queries)

    def _cursor(self):
        return getattr(self.connection, self.settings_dict['DATABASE_NAME'])

    @property
    def connection(self):
        if self._connection is None:
            from pymongo.connection import Connection
            self._connection = Connection(self.settings_dict['DATABASE_HOST'], int(self.settings_dict['DATABASE_PORT']))
        return self._connection