3
\$\begingroup\$

How can I improve this code (elegance, best practices, etc)?

""" Simple in-memory database as a response to the Thumbtack coding challenge. """


class SimpleDb(object):

    """ Simple in-memory database as a response to the Thumbtack coding challenge. """

    def __init__(self):
        """ Initialize SimpleDb instance. """
        self.__db = {}
        self.__num_equal_to_cache = {}
        self.__transactions = []  # Stores previous values to allow rollback

    def assign(self, name, value):
        """ Sets value of name to value. Inserts name into database if it doesn't already exist. """
        current_value = self.get(name)
        if current_value == value:
            return
        self.__update_num_equal_to(current_value, value)
        self.__update_current_transaction(name, current_value)
        self.__db[name] = value

    def get(self, name):
        """ Returns value of name if it exists in the database, otherwise returns None. """
        return self.__db[name] if name in self.__db else None

    def get_num_equal_to(self, value):
        """ Returns number of entries in the database that have the specified value. """
        return self.__num_equal_to_cache[value] if value in self.__num_equal_to_cache else 0

    def unset(self, name):
        """ Removes name from database if it's present. """
        current_value = self.__db.pop(name, None)
        if current_value is None:
            return
        self.__update_num_equal_to(current_value)
        self.__update_current_transaction(name, current_value)

    def begin(self):
        """ Opens transaction block. """
        self.__transactions += [{}]

    def rollback(self):
        """
        Reverts database to its state before most current transaction.
        Returns True on success, returns False if there aren't any open transactions.
        """
        if not self.__transactions:
            return False
        for name, value in self.__transactions.pop().iteritems():
            current_value = self.get(name)
            if current_value == value:
                continue
            self.__update_num_equal_to(current_value, value)
            if value is None:
                del self.__db[name]
            else:
                self.__db[name] = value
        return True

    def commit(self):
        """
        Commits all transactions to database. Returns True on success,
        returns False if there aren't any open transactions.
        """
        if not self.__transactions:
            return False
        self.__transactions = []
        return True

    def __update_num_equal_to(self, current_value, new_value=None):
        """
        Decrements by one the number items present with current_value (if current_value
        is not equal to None) and increments by one the number present with new_value
        (if new_value is not equal to None).
        """
        for amount_to_add, value in [(-1, current_value), (1, new_value)]:
            if value is not None:
                self.__num_equal_to_cache.setdefault(value, 0)
                self.__num_equal_to_cache[value] += amount_to_add

    def __update_current_transaction(self, name, value):
        """
        Stores current value of name if not already stored to most recent transaction
        (if any transactions open) to enable restoration of previous state on rollback.
        """
        if self.__transactions and name not in self.__transactions[-1]:
            self.__transactions[-1][name] = value


def display(value, default=None):
    """
    Prints value to stdout. If value is None and a default value is
    specified (and not None), then the default value is printed instead. Otherwise
    the None value is printed.
    """
    if value is None and default is not None:
        value = default
    print value


OPS = {
        'GET':        (2, lambda db, name:  display(db.get(name), "NULL")),
        'NUMEQUALTO': (2, lambda db, value: display(db.get_num_equal_to(value))),
        'UNSET':      (2, lambda db, name:  db.unset(name)),
        'BEGIN':      (1, lambda db:        db.begin()),
        'ROLLBACK':   (1, lambda db:        db.rollback() or display("NO TRANSACTION")),
        'COMMIT':     (1, lambda db:        db.commit() or display("NO TRANSACTION")),
        'END':        (1, lambda db:        False),
        'SET':        (3, lambda db, name, value: db.assign(name, value)),
}


def process_command(simpleDb, command):
    """
    Parses string commands and applies them to the database.
    Returning False indicates that no more commands should be passed in.
    """
    command = command.split()
    opcode = command.pop(0).upper() if len(command) > 0 else None
    if opcode is None or opcode not in OPS or len(command) != (OPS[opcode][0] - 1):
        print "INVALID COMMAND"
    elif 'END' == opcode:
        return False
    else:
        OPS[opcode][1](simpleDb, *command)
    return True


def run():
    """ Reads database command from the command line and passes it through for processing. """
    # BEGIN \n SET a 30 \n BEGIN \n SET a 40 \n COMMIT \n GET a \n ROLLBACK \n END
    simpleDb = SimpleDb()
    while process_command(simpleDb, raw_input()):
        pass

run()
\$\endgroup\$
2

1 Answer 1

6
\$\begingroup\$

I find your treatment of transactions odd. I would expect that when a transaction is in progress, data modifications commands get buffered; queries would consult the state stored in the current transaction for any relevant data, then any stacked transactions, then finally consulting the "real" database. Instead, you immediately "auto-commit" each command, and add an entry to the undo list; a commit simply discards the undo list. That's not a realistic model of a database: one would expect that a commit either succeeds atomically or has no effect at all on the database.

\$\endgroup\$
5
  • \$\begingroup\$ I was thinking just that. I've re-written the code such that no changes are made until the user sends the commit command. \$\endgroup\$
    – Joel
    Feb 18, 2014 at 6:49
  • 1
    \$\begingroup\$ You might want to post the revised code as a second question, referencing this one. \$\endgroup\$ Feb 18, 2014 at 6:55
  • \$\begingroup\$ some databases do indeed work like this -- optimistically committing, and writing a transaction log at the same time. it makes rollback more expensive, but commit much cheaper as you don't need to re-read the destination data to check that it's changed while the the transaction was running, before finally committing (and writing the rows). (eg: microsoft sql server works this way, as does oracle) \$\endgroup\$ Jul 8, 2015 at 7:06
  • \$\begingroup\$ @AndrewHill No matter how the transaction is implemented, it should still be atomic. \$\endgroup\$ Jul 8, 2015 at 7:53
  • \$\begingroup\$ yes, row/page locks in sql server and temp storage of old versions for rollback purposes in oracle both prevent partially complete transactions from becoming visible; these dbms's are atomic, they just rely on more sophisticated ; how should the database behave when when 2 transactions try to modify the same row / or read a row written but not commited? \$\endgroup\$ Jul 8, 2015 at 23:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.