Skip to content

Commit 975b6c6

Browse files
committed
Support tuples as well as lists
Refs #672
1 parent 632f4f5 commit 975b6c6

2 files changed

Lines changed: 106 additions & 8 deletions

File tree

sqlite_utils/db.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3033,14 +3033,18 @@ def build_insert_queries_and_params(
30333033
# Pad short records with None, truncate long ones
30343034
record_len = len(record)
30353035
if record_len < num_columns:
3036-
record_values = [jsonify_if_needed(v) for v in record] + [None] * (num_columns - record_len)
3036+
record_values = [jsonify_if_needed(v) for v in record] + [None] * (
3037+
num_columns - record_len
3038+
)
30373039
else:
30383040
record_values = [jsonify_if_needed(v) for v in record[:num_columns]]
30393041
# Only process extracts if there are any
30403042
if has_extracts:
30413043
for i, key in enumerate(all_columns):
30423044
if key in extracts:
3043-
record_values[i] = self.db[extracts[key]].lookup({"value": record_values[i]})
3045+
record_values[i] = self.db[extracts[key]].lookup(
3046+
{"value": record_values[i]}
3047+
)
30443048
values.append(record_values)
30453049
else:
30463050
# Dict mode: original logic
@@ -3393,21 +3397,25 @@ def insert_all(
33933397
return self # It was an empty list
33943398

33953399
# Check if this is list mode or dict mode
3396-
if isinstance(first_record, list):
3397-
# List mode: first record should be column names
3400+
if isinstance(first_record, (list, tuple)):
3401+
# List/tuple mode: first record should be column names
33983402
list_mode = True
33993403
if not all(isinstance(col, str) for col in first_record):
3400-
raise ValueError("When using list-based iteration, the first yielded value must be a list of column name strings")
3401-
column_names = first_record
3404+
raise ValueError(
3405+
"When using list-based iteration, the first yielded value must be a list of column name strings"
3406+
)
3407+
column_names = list(first_record)
34023408
all_columns = column_names
34033409
num_columns = len(column_names)
34043410
# Get the actual first data record
34053411
try:
34063412
first_record = next(records_iter)
34073413
except StopIteration:
34083414
return self # Only headers, no data
3409-
if not isinstance(first_record, list):
3410-
raise ValueError("After column names list, all subsequent records must also be lists")
3415+
if not isinstance(first_record, (list, tuple)):
3416+
raise ValueError(
3417+
"After column names list, all subsequent records must also be lists"
3418+
)
34113419
else:
34123420
# Dict mode: traditional behavior
34133421
records_iter = itertools.chain([first_record], records_iter)

tests/test_list_mode.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Tests for list-based iteration in insert_all and upsert_all
33
"""
4+
45
import pytest
56
from sqlite_utils import Database
67

@@ -173,3 +174,92 @@ def test_backwards_compatibility_dict_mode():
173174
rows = list(db["people"].rows)
174175
assert len(rows) == 2
175176
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
177+
178+
179+
def test_insert_all_tuple_mode_basic():
180+
"""Test basic insert_all with tuple-based iteration"""
181+
db = Database(memory=True)
182+
183+
def data_generator():
184+
# First yield column names as tuple
185+
yield ("id", "name", "age")
186+
# Then yield data rows as tuples
187+
yield (1, "Alice", 30)
188+
yield (2, "Bob", 25)
189+
yield (3, "Charlie", 35)
190+
191+
db["people"].insert_all(data_generator())
192+
193+
rows = list(db["people"].rows)
194+
assert len(rows) == 3
195+
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
196+
assert rows[1] == {"id": 2, "name": "Bob", "age": 25}
197+
assert rows[2] == {"id": 3, "name": "Charlie", "age": 35}
198+
199+
200+
def test_insert_all_mixed_list_tuple():
201+
"""Test insert_all with mixed lists and tuples for data rows"""
202+
db = Database(memory=True)
203+
204+
def data_generator():
205+
# Column names as list
206+
yield ["id", "name", "age"]
207+
# Mix of list and tuple data rows
208+
yield [1, "Alice", 30]
209+
yield (2, "Bob", 25)
210+
yield [3, "Charlie", 35]
211+
yield (4, "Diana", 40)
212+
213+
db["people"].insert_all(data_generator())
214+
215+
rows = list(db["people"].rows)
216+
assert len(rows) == 4
217+
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
218+
assert rows[1] == {"id": 2, "name": "Bob", "age": 25}
219+
assert rows[2] == {"id": 3, "name": "Charlie", "age": 35}
220+
assert rows[3] == {"id": 4, "name": "Diana", "age": 40}
221+
222+
223+
def test_upsert_all_tuple_mode():
224+
"""Test upsert_all with tuple-based iteration"""
225+
db = Database(memory=True)
226+
227+
# Initial insert with tuples
228+
def initial_data():
229+
yield ("id", "name", "value")
230+
yield (1, "Alice", 100)
231+
yield (2, "Bob", 200)
232+
233+
db["data"].insert_all(initial_data(), pk="id")
234+
235+
# Upsert with tuples
236+
def upsert_data():
237+
yield ("id", "name", "value")
238+
yield (1, "Alice", 150) # Update existing
239+
yield (3, "Charlie", 300) # Insert new
240+
241+
db["data"].upsert_all(upsert_data(), pk="id")
242+
243+
rows = list(db["data"].rows_where(order_by="id"))
244+
assert len(rows) == 3
245+
assert rows[0] == {"id": 1, "name": "Alice", "value": 150}
246+
assert rows[1] == {"id": 2, "name": "Bob", "value": 200}
247+
assert rows[2] == {"id": 3, "name": "Charlie", "value": 300}
248+
249+
250+
def test_tuple_mode_shorter_rows():
251+
"""Test that tuple rows shorter than column list get NULL values"""
252+
db = Database(memory=True)
253+
254+
def data_generator():
255+
yield "id", "name", "age", "city"
256+
yield 1, "Alice", 30, "NYC"
257+
yield 2, "Bob" # Missing age and city
258+
yield 3, "Charlie", 35 # Missing city
259+
260+
db["people"].insert_all(data_generator())
261+
262+
rows = list(db["people"].rows_where(order_by="id"))
263+
assert rows[0] == {"id": 1, "name": "Alice", "age": 30, "city": "NYC"}
264+
assert rows[1] == {"id": 2, "name": "Bob", "age": None, "city": None}
265+
assert rows[2] == {"id": 3, "name": "Charlie", "age": 35, "city": None}

0 commit comments

Comments
 (0)