@@ -9,7 +9,7 @@
99import json
1010import pathlib
1111import uuid
12-from collections.abc import Callable, Mapping, Sequence
12+from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
1313from contextlib import contextmanager
1414from dataclasses import asdict, is_dataclass
1515from typing import Any, TextIO, TypeVar, cast
@@ -199,8 +199,10 @@ def _decode_binary_value(value: Any) -> Any:
199199200200def _read_records(
201201filename: str, *, nl: bool, csv: bool, tsv: bool
202-) -> tuple[list[dict[str, Any]], bool]:
202+) -> tuple[Iterable[dict[str, Any]], bool]:
203203input_format = _selected_input_format(filename, nl=nl, csv=csv, tsv=tsv)
204+if input_format in ("csv", "tsv"):
205+return _read_delimited_records(filename, input_format=input_format), False
204206with click.open_file(filename, mode="r", encoding="utf-8-sig") as file:
205207stream = cast(TextIO, file)
206208if input_format == "json":
@@ -218,12 +220,20 @@ def _read_records(
218220for line in stream
219221if line.strip()
220222 ], False
223+224+225+def _read_delimited_records(
226+filename: str, *, input_format: str
227+) -> Iterator[dict[str, Any]]:
228+with click.open_file(filename, mode="r", encoding="utf-8-sig") as file:
229+stream = cast(TextIO, file)
221230reader = csv_stdlib.DictReader(
222231stream, dialect="excel-tab" if input_format == "tsv" else "excel"
223232 )
224233if reader.fieldnames is None:
225234raise click.ClickException("CSV/TSV input must include a header row")
226-return [dict(row) for row in reader], False
235+for row in reader:
236+yield dict(row)
227237228238229239def _coerce_value(
@@ -287,25 +297,25 @@ def _python_type_for_name(type_name: str) -> type[Any]:
287297288298289299def _coerce_records(
290-records: list[dict[str, Any]],
300+records: Iterable[dict[str, Any]],
291301reflected_types: Mapping[str, type[Any]],
292302explicit_types: Mapping[str, str],
293303*,
294304strict: bool,
295-) -> list[dict[str, Any]]:
305+) -> Iterator[dict[str, Any]]:
296306types = dict(reflected_types)
297307types.update(
298308 {name: _python_type_for_name(type_name) for name, type_name in explicit_types.items()}
299309 )
300-return [
310+return (
301311 {
302312name: _coerce_value(
303313value, types.get(name, str), column_name=name, strict=strict
304314 )
305315for name, value in record.items()
306316 }
307317for record in records
308-]
318+)
309319310320311321def _serializable_columns(table: Any) -> list[dict[str, Any]]:
@@ -431,6 +441,7 @@ def _write_options(function: F) -> F:
431441click.option("--nl", is_flag=True, help="Read newline-delimited JSON."),
432442click.option("--csv", is_flag=True, help="Read CSV with a header row."),
433443click.option("--tsv", is_flag=True, help="Read TSV with a header row."),
444+click.option("--batch-size", type=click.IntRange(min=1), default=100, show_default=True, help="Number of records to insert per batch."),
434445click.option("types", "--type", multiple=True, type=(str, click.Choice(VALID_COLUMN_TYPES, case_sensitive=False)), help="Column and type to use when creating the table."),
435446click.option("--alter", is_flag=True, help="Add nullable columns missing from an existing table."),
436447click.option("not_null", "--not-null", multiple=True, help="Column to make NOT NULL when creating the table."),
@@ -450,6 +461,7 @@ def _perform_write(
450461nl: bool,
451462csv: bool,
452463tsv: bool,
464+batch_size: int,
453465types: tuple[tuple[str, str], ...],
454466alter: bool,
455467not_null: tuple[str, ...],
@@ -461,6 +473,7 @@ def _perform_write(
461473) -> None:
462474if ignore and replace:
463475raise click.ClickException("Use either --ignore or --replace, not both")
476+input_format = _selected_input_format(file, nl=nl, csv=csv, tsv=tsv)
464477records, single = _read_records(file, nl=nl, csv=csv, tsv=tsv)
465478type_overrides = {name: type_name.upper() for name, type_name in types}
466479pk: str | tuple[str, ...] | None = None
@@ -485,15 +498,21 @@ def _perform_write(
485498"not_null": not_null,
486499"defaults": _parse_defaults(defaults),
487500"columns": type_overrides,
501+"batch_size": batch_size,
502+"stream": input_format in ("csv", "tsv"),
488503 }
489504if upsert:
490505if single:
491-table.upsert(records[0], **kwargs)
506+table.upsert(next(iter(records)), **kwargs)
492507else:
493508table.upsert_all(records, **kwargs)
494509elif single:
495510table.insert(
496-records[0], ignore=ignore, replace=replace, truncate=truncate, **kwargs
511+next(iter(records)),
512+ignore=ignore,
513+replace=replace,
514+truncate=truncate,
515+**kwargs,
497516 )
498517else:
499518table.insert_all(
@@ -536,9 +555,9 @@ def update(
536555raise click.ClickException("update input must be one JSON object")
537556with _database(database) as db:
538557table = db[table_name]
539-updates = _coerce_records(
540-records, table.columns_dict, {}, strict=table.exists()
541- )[0]
558+updates = next(
559+_coerce_records(records, table.columns_dict, {}, strict=table.exists())
560+ )
542561table.update(_primary_key_value(table, pk_value), updates, alter=alter)
543562544563