Skip to content

insert --csv drops values past the header, rows_from_file raises for the same input #890

Description

@feiiiiii5

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?

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions