Bad rows¶
Your load failed. This page says why, and what to do about it. It covers both readers — .csv.read and
.j.read — because they answer the same way.
The default posture is strict: a bad row aborts the load. Tolerance is opt-in, one option at a time, and the levers carry DuckDB's own names and defaults.
The one law¶
A lever governs whether the load survives a bad row, never whether it is counted.
Whichever lever let a bad row through, it still leaves a reject record and still bumps the summary's rejected.
No lever quietly stops counting, and none silently repairs a row without saying so.
With no lever set the load aborts instead, so there is no summary and no stored reject table — the error is the whole report. The reject record is what you get when you have asked the load to continue.
The error classes¶
| Class | Reader | Means |
|---|---|---|
'csv |
CSV | a malformed file, a frozen-type miss, a zero-byte file, or a ragged row with no lever set |
'parse |
JSON | a malformed document, a source carrying no document, or a framing the file does not have |
'type |
both | an argument of the wrong kind, and in JSON also a document that is not a table shape (see below) |
'option |
both | an unknown option key, a recognised-but-unimplemented one, or an unknown format specifier |
'domain |
both | an option value out of range, or a name that addresses nothing (see below) |
'length |
both | a types string that does not cover every column |
'rank |
CSV | a lambda target that is not rank 3 |
'dup |
JSON | a key repeated within one record |
'nyi |
JSON | a target other than :: |
An error leaves you in q's error handler, at the q)) prompt; \ returns to q).
'type covers a source that is neither a resource symbol nor text, an option value of the wrong type, a types
char outside the roster, and a CSV target that is not ::, a symbol or a lambda. In JSON it also covers a document
that is not a table shape, a key beyond the frozen schema, a frozen-type miss, and a path step of the wrong kind
— a symbol meeting an array, or a long meeting an object.
'domain covers a two-character comment, a delimiter colliding with the quote, a negative skip, a types dict
key naming no column, and a path step this document cannot take.
The frozen-type miss¶
This is the common one, so it is worth recognising on sight. The reader infers types from the first sample_size
records and then freezes them. A later cell that does not fit its column's frozen type is an error — never a
silent null, and never a mid-load promotion of the column to something wider, because a type that can change
halfway through a file is not a schema.
The sample saw 1, decided long, and then met x. There are two fixes, and which one is right depends on
whether the sample was unrepresentative or the file is genuinely mixed.
Read further before deciding. If the file really is one type and the sample just missed it, raise sample_size:
q).csv.read[`:f.csv;::;::;(enlist `sample_size)!enlist 100]
| a |
| |
|-----|
| "1" |
| "2" |
| "x" |
Or stop guessing and say what you want. Naming the column in types replaces the inference for it outright, and
does so without reading any further:
"*" is the useful answer when a column is genuinely heterogeneous: it keeps the raw text and lets you sort it out
in q. Remember that an explicit type is a promise, not a hint — a cell that will not parse under it is a frozen-type
miss in its own right.
In JSON the same law also covers keys: the columns are the union of the keys the sampled records carry, so a
key first seen after the sample is not a column, and signals 'type.
A failed load into a table leaves a partial table. The rows that had already been inserted stay inserted:
insert is not transactional, and a reader streaming a file larger than memory has nothing to roll back to. Check
count on the target after a failure, and prefer loading into a fresh name when you need all-or-nothing.
One kind of failure never reaches this page at all. A bad option — a mistyped key, or an xcol naming a column
the file does not have — is an argument error: it stops the load before a row is read, and no lever below turns it
into a reject record. The levers govern bad rows; an option is not a row.
The tolerance levers¶
All default off. Each one lets a specific kind of bad row through; none of them suppresses the record.
| Option | Reader | Survives |
|---|---|---|
ignore_errors |
both | skip the bad row (CSV), or drop the record (JSON — a record lands whole or not at all) |
null_padding |
CSV | pad a short row with nulls and keep it. It never pads a long row: too many fields stays an error |
strict_mode:0b |
CSV | read a field whose quotes will not parse as literal bytes, up to the next delimiter |
store_rejects |
both | continue, and keep the reject records in a global table |
rejects_table |
both | name that table (default `reject_errors), and imply store_rejects |
null_padding and strict_mode are salvage levers, not row-skip levers: they repair one specific defect and
keep the row. A cast miss under padding alone still aborts.
Accepted for parity and doing nothing: null_padding on the JSON side, where the key-union law already null-fills
an omitted key.
The reject record¶
A count alone does not make a bad file diagnosable, so every rejected row leaves a row of its own — line, column, class and the raw text. It is an audit trail, not an error count.
q).csv.read[`:f.csv;::;::;`sample_size`ignore_errors`store_rejects!(2;1b;1b)]
| a |
| long |
|------|
| 1 |
| 2 |
q)reject_errors
| line | column | error | csvLine |
| long | symbol | symbol | |
|------|--------|--------|---------|
| 4 | a | cast | "x" |
The JSON reader fills the same table, with record — the offending record, re-serialized — in place of csvLine:
q).j.read["[{\"a\":1},{\"a\":\"x\"}]";::;::;`sample_size`ignore_errors`store_rejects!(1;1b;1b)]
| a |
| long |
|------|
| 1 |
q)reject_errors
| line | column | error | record |
| long | symbol | symbol | |
|------|--------|--------|-----------------|
| 2 | a | cast | "{\"a\":\"x\"}" |
The columns:
line— where the row sits. In CSV that is the physical line, comments and blank lines included:skiprenumbers nothing. In JSON it is the physical line under newline-delimited framing, and the record's 1-based ordinal under array framing.column— the column to blame, where one column is to blame.error— the class, from the table below.csvLine/record— the raw text of what was rejected.
The stored table is one load's audit: it is replaced on every load, and is empty after a clean one.
| Reject class | Reader | Raised by |
|---|---|---|
cast |
both | a cell that will not parse as its frozen type |
toofewcolumns |
CSV | a short row |
toomanycolumns |
CSV | a long row |
unquotedvalue |
CSV | a quote appearing where the dialect does not allow one |
unterminatedquote |
CSV | a quoted region the file never closes |
padded |
CSV | a short row that null_padding repaired — kept, and still distinguishable from a skipped one |
unknownkey |
JSON | a key beyond the frozen schema |
dupkey |
JSON | a key repeated within one record |
parse |
JSON | a line that will not parse as one whole in-line value |
Rejects while streaming¶
With a lambda target, .csv.read hands each batch its own reject records as the second argument, so a streaming
load can react to them as they arrive rather than waiting for a table at the end:
errData is a table with the same four columns, and is the empty table — schema intact — on a clean batch. See
Reading CSV.