Skip to content

Commit 04ecd63

Browse files
committed
sqlite-diffable load command, closes #3
1 parent 4871ed9 commit 04ecd63

3 files changed

Lines changed: 84 additions & 7 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Tools for dumping/loading a SQLite database to diffable directory structure
1010

1111
pip install sqlite-diffable
1212

13-
## Usage
13+
## Dumping a database
1414

1515
Given a SQLite database called `fixtures.db` containing a table `facetable`, the following will dump out that table to the `out/` directory:
1616

@@ -20,6 +20,12 @@ To dump out every table in that database, use `--all`:
2020

2121
sqlite-diffable dump fixtures.db out/ --all
2222

23+
## Loading a database
24+
25+
To load a previously dumped database, run the following:
26+
27+
sqlite-diffable load restored.db out/
28+
2329
## Demo
2430

2531
The repository at [simonw/simonwillisonblog-backup](https://github.com/simonw/simonwillisonblog-backup) contains a backup of the database on my blog, https://simonwillison.net/ - created using this tool.

sqlite_diffable/cli.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,32 @@ def cli():
1313

1414
@cli.command()
1515
@click.argument(
16-
"path", type=click.Path(exists=True, file_okay=True, dir_okay=False), required=True
16+
"dbpath",
17+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
18+
required=True,
1719
)
1820
@click.argument(
1921
"output", type=click.Path(file_okay=False, dir_okay=True), required=True
2022
)
2123
@click.argument("tables", nargs=-1, required=False)
22-
@click.option("--all", is_flag=True)
23-
def dump(path, output, tables, all):
24+
@click.option("--all", is_flag=True, help="Dump all tables")
25+
def dump(dbpath, output, tables, all):
26+
"""
27+
Dump a SQLite database out as flat files in the directory
28+
29+
Usage:
30+
31+
sqlite-diffable dump my.db output/ --all
32+
33+
--all dumps ever table. Or specify tables like this:
34+
35+
sqlite-diffable dump my.db output/ entries tags
36+
"""
2437
if not tables and not all:
2538
raise click.ClickException("You must pass --all or specify some tables")
2639
output = pathlib.Path(output)
2740
output.mkdir(exist_ok=True)
28-
conn = sqlite_utils.Database(path)
41+
conn = sqlite_utils.Database(dbpath)
2942
if all:
3043
tables = conn.table_names()
3144
for table in tables:
@@ -48,3 +61,38 @@ def dump(path, output, tables, all):
4861
indent=4,
4962
)
5063
)
64+
65+
66+
@cli.command()
67+
@click.argument(
68+
"dbpath",
69+
type=click.Path(file_okay=True, allow_dash=False, dir_okay=False),
70+
)
71+
@click.argument(
72+
"directory",
73+
type=click.Path(file_okay=False, dir_okay=True),
74+
)
75+
def load(dbpath, directory):
76+
"""
77+
Load flat files from a directory into a SQLite database
78+
79+
Usage:
80+
81+
sqlite-diffable load my.db output/
82+
"""
83+
db = sqlite_utils.Database(dbpath)
84+
directory = pathlib.Path(directory)
85+
metadatas = directory.glob("*.metadata.json")
86+
for metadata in metadatas:
87+
info = json.loads(metadata.read_text())
88+
columns = info["columns"]
89+
schema = info["schema"]
90+
db.execute(schema)
91+
# Now insert the rows
92+
ndjson = metadata.parent / metadata.stem.replace(".metadata", ".ndjson")
93+
rows = (
94+
dict(zip(columns, json.loads(line)))
95+
for line in ndjson.open()
96+
if line.strip()
97+
)
98+
db[info["name"]].insert_all(rows)

tests/test_dump.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from click.testing import CliRunner
22
from sqlite_diffable import cli
3+
import sqlite_utils
34
import json
45

56

@@ -8,7 +9,7 @@ def test_dump(one_table_db, tmpdir):
89
result = CliRunner().invoke(
910
cli.cli, ["dump", one_table_db, str(output_dir), "one_table"]
1011
)
11-
assert 0 == result.exit_code, result.output
12+
assert result.exit_code == 0, result.output
1213
# out/ should now have a single file in it
1314
ndjson = output_dir / "one_table.ndjson"
1415
metadata = output_dir / "one_table.metadata.json"
@@ -29,8 +30,30 @@ def test_dump_all(two_tables_db, tmpdir):
2930
result = CliRunner().invoke(
3031
cli.cli, ["dump", two_tables_db, str(output_dir), "--all"]
3132
)
32-
assert 0 == result.exit_code, result.output
33+
assert result.exit_code == 0, result.output
3334
assert (output_dir / "one_table.ndjson").exists()
3435
assert (output_dir / "one_table.metadata.json").exists()
3536
assert (output_dir / "second_table.ndjson").exists()
3637
assert (output_dir / "second_table.metadata.json").exists()
38+
39+
40+
def test_load(two_tables_db, tmpdir):
41+
output_dir = tmpdir / "out"
42+
restore_db = tmpdir / "restore.db"
43+
result = CliRunner().invoke(
44+
cli.cli, ["dump", str(two_tables_db), str(output_dir), "--all"]
45+
)
46+
assert result.exit_code == 0, result.output
47+
# Now load it again
48+
result2 = CliRunner().invoke(cli.cli, ["load", str(restore_db), str(output_dir)])
49+
assert result2.exit_code == 0, result2.output
50+
db = sqlite_utils.Database(str(restore_db))
51+
assert set(db.table_names()) == {"second_table", "one_table"}
52+
assert list(db["one_table"].rows) == [
53+
{"id": 1, "name": "Stacey"},
54+
{"id": 2, "name": "Tilda"},
55+
{"id": 3, "name": "Bartek"},
56+
]
57+
assert list(db["second_table"].rows) == [
58+
{"id": 1, "name": "Cleo"},
59+
]

0 commit comments

Comments
 (0)