# Simple in-memory database

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()

-
Do you want to add thumbtack.com/challenges/software-engineer#simple-db to the question as a definition of the program's specification? – ChrisW Feb 18 '14 at 2:30
If you want to edit the code, put your edited code after (not replacing) the existing code; otherwise that invalidates existing answers: see Can I edit my own question to include suggested changes from answers? for further details. – ChrisW Feb 18 '14 at 12:03

## 1 Answer

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.

-
I was thinking just that. I've re-written the code such that no changes are made until the user sends the commit command. – Joel Feb 18 '14 at 6:49
You might want to post the revised code as a second question, referencing this one. – 200_success Feb 18 '14 at 6:55
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) – Andrew Hill Jul 8 '15 at 7:06
@AndrewHill No matter how the transaction is implemented, it should still be atomic. – 200_success Jul 8 '15 at 7:53
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? – Andrew Hill Jul 8 '15 at 23:47