insert --csv silently drops values past the end of the header row, and sqlite_utils.utils.rows_from_file() raises RowError for the same input. I started to fix this and then found that an existing test pins the truncating behaviour, so I would like to know which one you want before changing anything.
The two readers disagree
sqlite_utils/cli.py builds rows for insert --csv / --tsv with:
docs = (dict(zip(headers, row)) for row in reader)
dict(zip(headers, row)) stops at len(headers), so anything beyond is discarded with no warning and exit code 0:
$ printf 'id,name\n1,Cleo,dog\n' > wide.csv
$ sqlite-utils insert d.db t wide.csv --csv
$ sqlite-utils d.db "select * from t"
[{"id": 1, "name": "Cleo"}] # "dog" gone
rows_from_file takes the opposite position on identical input, and says so in its docstring:
>>> rows, _ = rows_from_file(io.BytesIO(b'id,name\n1,Cleo,dog\n'), format=Format.CSV)
>>> list(rows)
RowError: Row {'id': '1', 'name': 'Cleo'} contained these extra values: ['dog']
insert and upsert expose no extras_key or ignore_extras option, so a user of insert --csv has no way to notice the loss or to choose the other behaviour.
What stopped me: a test pins the truncation
tests/test_cli_insert.py::test_insert_csv_empty_null feeds a row that is one value wider than the header:
input="foo,bar,baz\n1,,cat,dog",
...
assert result.exit_code == 0
assert [r for r in db.table("data").rows] == [
{"foo": "1", "bar": None if empty_null else "", "baz": "cat"}
]
Comparing the row dict for equality does assert that no fourth key appears, so dog being dropped is currently asserted behaviour rather than an oversight. That test is about --empty-null, so the fourth field may well have been incidental — but I did not want to change an asserted output on my own judgement about what its author meant.
The question
Which of these should insert --csv do?
- Raise, like
rows_from_file. One helper, same message, no silent loss. Requires changing test_insert_csv_empty_null's input to foo,bar,baz\n1,,cat so it keeps testing --empty-null without an extra field. This is the fix I had written.
- Keep truncating, and accept that the two readers disagree. In which case I would suggest either documenting the truncation for
insert --csv or routing it through rows_from_file with an explicit extras_key, so the behaviour is a choice rather than a side effect of dict(zip(...)).
A short row is not affected either way — fewer values than headers is a short row, not an error, and that stays as it is.
Happy to take whichever you prefer, or to write the docs-only version if you would rather not change behaviour at all.
insert --csvsilently drops values past the end of the header row, andsqlite_utils.utils.rows_from_file()raisesRowErrorfor the same input. I started to fix this and then found that an existing test pins the truncating behaviour, so I would like to know which one you want before changing anything.The two readers disagree
sqlite_utils/cli.pybuilds rows forinsert --csv/--tsvwith:dict(zip(headers, row))stops atlen(headers), so anything beyond is discarded with no warning and exit code 0:rows_from_filetakes the opposite position on identical input, and says so in its docstring:insertandupsertexpose noextras_keyorignore_extrasoption, so a user ofinsert --csvhas no way to notice the loss or to choose the other behaviour.What stopped me: a test pins the truncation
tests/test_cli_insert.py::test_insert_csv_empty_nullfeeds a row that is one value wider than the header:Comparing the row dict for equality does assert that no fourth key appears, so
dogbeing dropped is currently asserted behaviour rather than an oversight. That test is about--empty-null, so the fourth field may well have been incidental — but I did not want to change an asserted output on my own judgement about what its author meant.The question
Which of these should
insert --csvdo?rows_from_file. One helper, same message, no silent loss. Requires changingtest_insert_csv_empty_null's input tofoo,bar,baz\n1,,catso it keeps testing--empty-nullwithout an extra field. This is the fix I had written.insert --csvor routing it throughrows_from_filewith an explicitextras_key, so the behaviour is a choice rather than a side effect ofdict(zip(...)).A short row is not affected either way — fewer values than headers is a short row, not an error, and that stays as it is.
Happy to take whichever you prefer, or to write the docs-only version if you would rather not change behaviour at all.