Skip to content

Commit 2b20957

Browse files
committed
Better validation for upsert --pk, closes #390
1 parent 4c60234 commit 2b20957

File tree

3 files changed

+79
-59
lines changed

3 files changed

+79
-59
lines changed

docs/cli-reference.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,6 @@ See :ref:`cli_upsert`.
253253
incoming record has a primary key that matches an existing record the existing
254254
record will be updated.
255255

256-
The --pk option is required.
257-
258256
Example:
259257

260258
echo '[
@@ -264,6 +262,7 @@ See :ref:`cli_upsert`.
264262

265263
Options:
266264
--pk TEXT Columns to use as the primary key, e.g. id
265+
[required]
267266
--flatten Flatten nested JSON objects, so {"a": {"b": 1}}
268267
becomes {"a_b": 1}
269268
--nl Expect newline-delimited JSON

sqlite_utils/cli.py

Lines changed: 61 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -813,59 +813,65 @@ def import_options(fn):
813813
return fn
814814

815815

816-
def insert_upsert_options(fn):
817-
for decorator in reversed(
818-
(
819-
click.argument(
820-
"path",
821-
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
822-
required=True,
823-
),
824-
click.argument("table"),
825-
click.argument("file", type=click.File("rb"), required=True),
826-
click.option(
827-
"--pk", help="Columns to use as the primary key, e.g. id", multiple=True
828-
),
829-
)
830-
+ _import_options
831-
+ (
832-
click.option(
833-
"--batch-size", type=int, default=100, help="Commit every X records"
834-
),
835-
click.option(
836-
"--alter",
837-
is_flag=True,
838-
help="Alter existing table to add any missing columns",
839-
),
840-
click.option(
841-
"--not-null",
842-
multiple=True,
843-
help="Columns that should be created as NOT NULL",
844-
),
845-
click.option(
846-
"--default",
847-
multiple=True,
848-
type=(str, str),
849-
help="Default value that should be set for a column",
850-
),
851-
click.option(
852-
"-d",
853-
"--detect-types",
854-
is_flag=True,
855-
envvar="SQLITE_UTILS_DETECT_TYPES",
856-
help="Detect types for columns in CSV/TSV data",
857-
),
858-
click.option(
859-
"--analyze",
860-
is_flag=True,
861-
help="Run ANALYZE at the end of this operation",
862-
),
863-
load_extension_option,
864-
click.option("--silent", is_flag=True, help="Do not show progress bar"),
865-
)
866-
):
867-
fn = decorator(fn)
868-
return fn
816+
def insert_upsert_options(*, require_pk=False):
817+
def inner(fn):
818+
for decorator in reversed(
819+
(
820+
click.argument(
821+
"path",
822+
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
823+
required=True,
824+
),
825+
click.argument("table"),
826+
click.argument("file", type=click.File("rb"), required=True),
827+
click.option(
828+
"--pk",
829+
help="Columns to use as the primary key, e.g. id",
830+
multiple=True,
831+
required=require_pk,
832+
),
833+
)
834+
+ _import_options
835+
+ (
836+
click.option(
837+
"--batch-size", type=int, default=100, help="Commit every X records"
838+
),
839+
click.option(
840+
"--alter",
841+
is_flag=True,
842+
help="Alter existing table to add any missing columns",
843+
),
844+
click.option(
845+
"--not-null",
846+
multiple=True,
847+
help="Columns that should be created as NOT NULL",
848+
),
849+
click.option(
850+
"--default",
851+
multiple=True,
852+
type=(str, str),
853+
help="Default value that should be set for a column",
854+
),
855+
click.option(
856+
"-d",
857+
"--detect-types",
858+
is_flag=True,
859+
envvar="SQLITE_UTILS_DETECT_TYPES",
860+
help="Detect types for columns in CSV/TSV data",
861+
),
862+
click.option(
863+
"--analyze",
864+
is_flag=True,
865+
help="Run ANALYZE at the end of this operation",
866+
),
867+
load_extension_option,
868+
click.option("--silent", is_flag=True, help="Do not show progress bar"),
869+
)
870+
):
871+
fn = decorator(fn)
872+
return fn
873+
874+
return inner
869875

870876

871877
def insert_upsert_implementation(
@@ -1060,7 +1066,7 @@ def _find_variables(tb, vars):
10601066

10611067

10621068
@cli.command()
1063-
@insert_upsert_options
1069+
@insert_upsert_options()
10641070
@click.option(
10651071
"--ignore", is_flag=True, default=False, help="Ignore records if pk already exists"
10661072
)
@@ -1168,7 +1174,7 @@ def insert(
11681174

11691175

11701176
@cli.command()
1171-
@insert_upsert_options
1177+
@insert_upsert_options(require_pk=True)
11721178
def upsert(
11731179
path,
11741180
table,
@@ -1201,8 +1207,6 @@ def upsert(
12011207
an incoming record has a primary key that matches an existing record
12021208
the existing record will be updated.
12031209
1204-
The --pk option is required.
1205-
12061210
Example:
12071211
12081212
\b

tests/test_cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,23 @@ def test_upsert(db_path, tmpdir):
922922
]
923923

924924

925+
def test_upsert_pk_required(db_path, tmpdir):
926+
json_path = str(tmpdir / "dogs.json")
927+
db = Database(db_path)
928+
insert_dogs = [
929+
{"id": 1, "name": "Cleo", "age": 4},
930+
{"id": 2, "name": "Nixie", "age": 4},
931+
]
932+
open(json_path, "w").write(json.dumps(insert_dogs))
933+
result = CliRunner().invoke(
934+
cli.cli,
935+
["upsert", db_path, "dogs", json_path],
936+
catch_exceptions=False,
937+
)
938+
assert result.exit_code == 2
939+
assert "Error: Missing option '--pk'" in result.output
940+
941+
925942
def test_upsert_analyze(db_path, tmpdir):
926943
db = Database(db_path)
927944
db["rows"].insert({"id": 1, "foo": "x", "n": 3}, pk="id")

0 commit comments

Comments
 (0)