Skip to content

Commit

Permalink
Add support for inserting multiple rows at once
Browse files Browse the repository at this point in the history
  • Loading branch information
a-hurst committed Sep 28, 2023
1 parent b18cd94 commit adec912
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 20 deletions.
5 changes: 5 additions & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ New Features:
a friendlier, more readable way of adding events to the trial sequencer.
* Added a convenience function :func:`~klibs.KLTime.time_msec` for getting
timestamps in milliseconds.
* The :meth:`~klibs.KLDatabase.Database.insert` method for the
:class:`~klibs.KLDatabase.Database` class now supports inserting multiple rows
at once via a list of dicts (one for each row). When inserting many rows of
data, this can offer substatial speedup over calling ``insert`` on each row
individually.


Runtime Changes:
Expand Down
68 changes: 49 additions & 19 deletions klibs/KLDatabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,36 +424,66 @@ def exists(self, table, column, value):


def insert(self, data, table=None):
"""Inserts a row of data into a table in the database.
"""Inserts one or more rows of data into a table in the database.
Each row of data is represented as a dict in ``{'column': value}``
format, with keys for each required column in the destination table
and values specifying the corresponding value to insert for each column.
For example, to insert a single row of data, you could do::
dat = {'participant_id': P.p_id, 'rating': resp, 'rt': resp_rt}
self.db.insert(dat, table='ratings')
Alternatively, you can insert multiple rows of data at once by providing
a list of dicts::
rows = []
for timestamp, stick_x, stick_y in axis_data:
rows.append({
'participant_id': P.participant_id,
'block_num': P.block_number,
'trial_num': P.trial_number,
'time': timestamp,
'stick_x': stick_x,
'stick_y': stick_y,
})
self.db.insert(rows, table='gamepad')
Args:
data (:obj:`dict`): A dictionary in the format ``{'column': value}``
specifying the values to insert for each column in the row. The
column names must match the columns of the table.
data (:obj:`dict` or :obj:`list`): A dictionary (or list of dicts)
containing the data to insert into the database. The column
names must match the columns of the destination table.
table (str): The name of the table to insert the data into.
Returns:
int: The row id of the last row inserted into the table.
"""
if isinstance(data, EntryTemplate):
if not table:
table = data.table
data = data._values
elif isinstance(data, dict):
elif isinstance(data, dict) or isinstance(data, list):
if not table:
raise ValueError("A table must be specified when inserting a dict.")
raise ValueError("A table must be specified when inserting data.")
else:
raise TypeError("Argument 'data' must be either an EntryTemplate or a dict.")

cols, values = self._gather_insert_values(data, table)
cols_str = u", ".join(cols)
qmark_str = u", ".join(["?"] * len(values))
q = u"INSERT INTO `{0}` ({1}) VALUES({2})".format(table, cols_str, qmark_str)
try:
self.cursor.execute(q, values)
except sqlite3.OperationalError as e:
err = "\n\n\nTried to match the following:\n\n{0}\n\nwith\n\n{1}"
print(full_trace())
print(err.format(self.table_schemas[table], q))
raise e
raise TypeError("Argument 'data' must be either a dict or list of dicts.")

if not isinstance(data, list):
data = [data]
for row in data:
cols, values = self._gather_insert_values(row, table)
col_str = u", ".join(cols)
qmark_str = u", ".join(["?"] * len(values))
q = u"INSERT INTO `{0}` ({1}) VALUES({2})".format(table, col_str, qmark_str)
try:
self.cursor.execute(q, values)
except sqlite3.OperationalError as e:
err = "\n\n\nTried to match the following:\n\n{0}\n\nwith\n\n{1}"
print(full_trace())
print(err.format(self.table_schemas[table], q))
raise e
self.db.commit()
return self.cursor.lastrowid

Expand Down
12 changes: 11 additions & 1 deletion klibs/tests/test_KLDatabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,10 @@ def test_insert(self, db):
last_row = db.last_row_id('participants')
assert last_row == None
data = generate_id_row()
db.insert(data, table='participants')
row_id = db.insert(data, table='participants')
last_row = db.last_row_id('participants')
assert last_row == 1
assert last_row == row_id
# Test exception on non-existant table
with pytest.raises(ValueError):
db.insert(data, table='nope')
Expand All @@ -113,6 +114,15 @@ def test_insert(self, db):
data = runtime_info_init()
db.insert(data, table='session_info')
assert db.last_row_id('session_info') == 1
# Test inserting multiple rows of data
rows = [generate_data_row(trial=i+1) for i in range(3)]
db.insert(rows, table='trials')
assert db.last_row_id('trials') == 3
retrieved = db.select('trials')
assert len(retrieved) == 3
# Test inserting an empty list of rows
db.insert([], table='trials')
assert db.last_row_id('trials') == 3
# Test exception when unable to coerce value to column type
data = generate_id_row(uid=3)
data["age"] = "hello"
Expand Down

0 comments on commit adec912

Please sign in to comment.