2018-07-28 06:43:18 -07:00
|
|
|
import click
|
2019-01-24 19:30:47 -08:00
|
|
|
import sqlite_utils
|
2019-01-26 10:58:45 -08:00
|
|
|
import itertools
|
2019-01-29 07:37:01 -08:00
|
|
|
import json
|
2019-01-25 07:50:20 -08:00
|
|
|
import sys
|
2019-01-29 07:37:01 -08:00
|
|
|
import csv
|
2019-01-25 18:06:29 -08:00
|
|
|
import sqlite3
|
2018-07-28 06:43:18 -07:00
|
|
|
|
|
|
|
|
|
2019-01-24 19:30:47 -08:00
|
|
|
@click.group()
|
|
|
|
|
@click.version_option()
|
|
|
|
|
def cli():
|
|
|
|
|
"Commands for interacting with a SQLite database"
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
2019-01-24 19:57:04 -08:00
|
|
|
@click.option(
|
|
|
|
|
"--fts4", help="Just show FTS4 enabled tables", default=False, is_flag=True
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--fts5", help="Just show FTS5 enabled tables", default=False, is_flag=True
|
|
|
|
|
)
|
2019-01-24 23:04:52 -08:00
|
|
|
def tables(path, fts4, fts5):
|
2019-01-24 19:30:47 -08:00
|
|
|
"""List the tables in the database"""
|
|
|
|
|
db = sqlite_utils.Database(path)
|
2019-01-24 19:57:04 -08:00
|
|
|
for name in db.table_names(fts4=fts4, fts5=fts5):
|
2019-01-24 19:30:47 -08:00
|
|
|
print(name)
|
2019-01-24 19:39:04 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
def vacuum(path):
|
|
|
|
|
"""Run VACUUM against the database"""
|
|
|
|
|
sqlite_utils.Database(path).vacuum()
|
2019-01-24 20:35:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.option("--no-vacuum", help="Don't run VACUUM", default=False, is_flag=True)
|
|
|
|
|
def optimize(path, no_vacuum):
|
|
|
|
|
"""Optimize all FTS tables and then run VACUUM - should shrink the database file"""
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
tables = db.table_names(fts4=True) + db.table_names(fts5=True)
|
|
|
|
|
with db.conn:
|
|
|
|
|
for table in tables:
|
|
|
|
|
db[table].optimize()
|
|
|
|
|
if not no_vacuum:
|
|
|
|
|
db.vacuum()
|
2019-01-24 21:06:41 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
@click.argument("json_file", type=click.File(), required=True)
|
|
|
|
|
@click.option("--pk", help="Column to use as the primary key, e.g. id")
|
2019-01-27 18:17:38 -08:00
|
|
|
@click.option("--nl", is_flag=True, help="Expect newline-delimited JSON")
|
2019-01-29 07:28:12 -08:00
|
|
|
@click.option("--csv", is_flag=True, help="Expect CSV")
|
2019-01-27 22:26:45 -08:00
|
|
|
@click.option("--batch-size", type=int, default=100, help="Commit every X records")
|
2019-01-29 07:28:12 -08:00
|
|
|
def insert(path, table, json_file, pk, nl, csv, batch_size):
|
2019-01-24 21:20:10 -08:00
|
|
|
"Insert records from JSON file into the table, create table if it is missing"
|
2019-01-24 21:06:41 -08:00
|
|
|
db = sqlite_utils.Database(path)
|
2019-01-29 07:28:12 -08:00
|
|
|
if nl and csv:
|
|
|
|
|
click.echo("Use just one of --nl and --csv", err=True)
|
|
|
|
|
return
|
|
|
|
|
if csv:
|
2019-01-29 07:37:01 -08:00
|
|
|
reader = csv.reader(json_file)
|
2019-01-29 07:28:12 -08:00
|
|
|
headers = next(reader)
|
|
|
|
|
docs = (dict(zip(headers, row)) for row in reader)
|
|
|
|
|
elif nl:
|
2019-01-29 07:37:01 -08:00
|
|
|
docs = (json.loads(line) for line in json_file)
|
2019-01-27 18:17:38 -08:00
|
|
|
else:
|
2019-01-29 07:37:01 -08:00
|
|
|
docs = json.load(json_file)
|
2019-01-27 18:17:38 -08:00
|
|
|
if isinstance(docs, dict):
|
|
|
|
|
docs = [docs]
|
2019-01-27 22:26:45 -08:00
|
|
|
db[table].insert_all(docs, pk=pk, batch_size=batch_size)
|
2019-01-24 21:20:10 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
@click.argument("json_file", type=click.File(), required=True)
|
|
|
|
|
@click.option("--pk", help="Column to use as the primary key, e.g. id")
|
|
|
|
|
def upsert(path, table, json_file, pk):
|
|
|
|
|
"Upsert records based on their primary key"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
2019-01-29 07:37:01 -08:00
|
|
|
docs = json.load(json_file)
|
2019-01-24 21:20:10 -08:00
|
|
|
if isinstance(docs, dict):
|
|
|
|
|
docs = [docs]
|
|
|
|
|
db[table].upsert_all(docs, pk=pk)
|
2019-01-25 07:50:20 -08:00
|
|
|
|
|
|
|
|
|
2019-01-29 07:37:01 -08:00
|
|
|
@cli.command(name="csv")
|
2019-01-25 07:50:20 -08:00
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("sql")
|
|
|
|
|
@click.option(
|
|
|
|
|
"--no-headers", help="Exclude headers from CSV output", is_flag=True, default=False
|
|
|
|
|
)
|
2019-01-29 07:37:01 -08:00
|
|
|
def csv_cmd(path, sql, no_headers):
|
2019-01-25 07:50:20 -08:00
|
|
|
"Execute SQL query and return the results as CSV"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
cursor = db.conn.execute(sql)
|
2019-01-29 07:37:01 -08:00
|
|
|
writer = csv.writer(sys.stdout)
|
2019-01-25 07:50:20 -08:00
|
|
|
if not no_headers:
|
|
|
|
|
writer.writerow([c[0] for c in cursor.description])
|
|
|
|
|
for row in cursor:
|
|
|
|
|
writer.writerow(row)
|
2019-01-25 18:06:29 -08:00
|
|
|
|
|
|
|
|
|
2019-01-29 07:37:01 -08:00
|
|
|
@cli.command(name="json")
|
2019-01-25 18:06:29 -08:00
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("sql")
|
|
|
|
|
@click.option("--nl", help="Output newline-delimited JSON", is_flag=True, default=False)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--arrays",
|
|
|
|
|
help="Output rows as arrays instead of objects",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
default=False,
|
|
|
|
|
)
|
2019-01-29 07:37:01 -08:00
|
|
|
def json_cmd(path, sql, nl, arrays):
|
2019-01-25 18:06:29 -08:00
|
|
|
"Execute SQL query and return the results as JSON"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
cursor = iter(db.conn.execute(sql))
|
|
|
|
|
# We have to iterate two-at-a-time so we can know if we
|
|
|
|
|
# should output a trailing comma or if we have reached
|
|
|
|
|
# the last row.
|
2019-01-26 10:58:45 -08:00
|
|
|
current_iter, next_iter = itertools.tee(cursor, 2)
|
|
|
|
|
next(next_iter, None)
|
2019-01-25 18:06:29 -08:00
|
|
|
first = True
|
|
|
|
|
headers = [c[0] for c in cursor.description]
|
2019-01-26 10:58:45 -08:00
|
|
|
for row, next_row in itertools.zip_longest(current_iter, next_iter):
|
|
|
|
|
is_last = next_row is None
|
2019-01-25 18:06:29 -08:00
|
|
|
data = row
|
|
|
|
|
if not arrays:
|
|
|
|
|
data = dict(zip(headers, row))
|
|
|
|
|
line = "{firstchar}{serialized}{maybecomma}{lastchar}".format(
|
|
|
|
|
firstchar=("[" if first else " ") if not nl else "",
|
2019-01-29 07:37:01 -08:00
|
|
|
serialized=json.dumps(data),
|
2019-01-25 18:06:29 -08:00
|
|
|
maybecomma="," if (not nl and not is_last) else "",
|
|
|
|
|
lastchar="]" if (is_last and not nl) else "",
|
|
|
|
|
)
|
|
|
|
|
click.echo(line)
|
|
|
|
|
first = False
|