mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-10-23 09:47:06 +02:00
Compare commits
6 commits
main
...
insert-fil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c18ae2faca | ||
|
|
db2dd6d9f3 | ||
|
|
96bc06db03 | ||
|
|
d5dab71d24 | ||
|
|
873877111c | ||
|
|
0c796cd945 |
5 changed files with 110 additions and 17 deletions
|
|
@ -2,6 +2,14 @@
|
||||||
Changelog
|
Changelog
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
.. _v3_17:
|
||||||
|
|
||||||
|
3.17 (2021-08-24)
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
- The :ref:`sqlite-utils memory <cli_memory>` command has a new ``--analyze`` option, which runs the equivalent of the :ref:`analyze-tables <cli_analyze_tables>` command directly against the in-memory database created from the incoming CSV or JSON data. (:issue:`320`)
|
||||||
|
- :ref:`sqlite-utils insert-files <cli_insert_files>` now has the ability to insert file contents in to ``TEXT`` columns in addition to the default ``BLOB``. Pass the ``--text`` option or use ``content_text`` as a column specifier. (:issue:`319`)
|
||||||
|
|
||||||
.. _v3_16:
|
.. _v3_16:
|
||||||
|
|
||||||
3.16 (2021-08-18)
|
3.16 (2021-08-18)
|
||||||
|
|
|
||||||
18
docs/cli.rst
18
docs/cli.rst
|
|
@ -331,7 +331,7 @@ The CSV data that was piped into the script is available in the ``stdin`` table,
|
||||||
\-\-schema, \-\-analyze, \-\-dump and \-\-save
|
\-\-schema, \-\-analyze, \-\-dump and \-\-save
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
To see the in-memory datbase schema that would be used for a file or for multiple files, use ``--schema``::
|
To see the in-memory database schema that would be used for a file or for multiple files, use ``--schema``::
|
||||||
|
|
||||||
% sqlite-utils memory dogs.csv --schema
|
% sqlite-utils memory dogs.csv --schema
|
||||||
CREATE TABLE [dogs] (
|
CREATE TABLE [dogs] (
|
||||||
|
|
@ -909,12 +909,10 @@ The command will fail if you reference columns that do not exist on the table. T
|
||||||
|
|
||||||
.. _cli_insert_files:
|
.. _cli_insert_files:
|
||||||
|
|
||||||
Inserting binary data from files
|
Inserting data from files
|
||||||
================================
|
=========================
|
||||||
|
|
||||||
SQLite ``BLOB`` columns can be used to store binary content. It can be useful to insert the contents of files into a SQLite table.
|
The ``insert-files`` command can be used to insert the content of files, along with their metadata, into a SQLite table.
|
||||||
|
|
||||||
The ``insert-files`` command can be used to insert the content of files, along with their metadata.
|
|
||||||
|
|
||||||
Here's an example that inserts all of the GIF files in the current directory into a ``gifs.db`` database, placing the file contents in an ``images`` table::
|
Here's an example that inserts all of the GIF files in the current directory into a ``gifs.db`` database, placing the file contents in an ``images`` table::
|
||||||
|
|
||||||
|
|
@ -932,6 +930,8 @@ By default this command will create a table with the following schema::
|
||||||
[size] INTEGER
|
[size] INTEGER
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Content will be treated as binary by default and stored in a ``BLOB`` column. You can use the ``--text`` option to store that content in a ``TEXT`` column instead.
|
||||||
|
|
||||||
You can customize the schema using one or more ``-c`` options. For a table schema that includes just the path, MD5 hash and last modification time of the file, you would use this::
|
You can customize the schema using one or more ``-c`` options. For a table schema that includes just the path, MD5 hash and last modification time of the file, you would use this::
|
||||||
|
|
||||||
$ sqlite-utils insert-files gifs.db images *.gif -c path -c md5 -c mtime --pk=path
|
$ sqlite-utils insert-files gifs.db images *.gif -c path -c md5 -c mtime --pk=path
|
||||||
|
|
@ -944,6 +944,8 @@ This will result in the following schema::
|
||||||
[mtime] FLOAT
|
[mtime] FLOAT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Note that there's no ``content`` column here at all - if you specify custom columns using ``-c`` you need to include ``-c content`` to create that column.
|
||||||
|
|
||||||
You can change the name of one of these columns using a ``-c colname:coldef`` parameter. To rename the ``mtime`` column to ``last_modified`` you would use this::
|
You can change the name of one of these columns using a ``-c colname:coldef`` parameter. To rename the ``mtime`` column to ``last_modified`` you would use this::
|
||||||
|
|
||||||
$ sqlite-utils insert-files gifs.db images *.gif \
|
$ sqlite-utils insert-files gifs.db images *.gif \
|
||||||
|
|
@ -967,6 +969,8 @@ The full list of column definitions you can use is as follows:
|
||||||
The permission bits of the file, as an integer - you may want to convert this to octal
|
The permission bits of the file, as an integer - you may want to convert this to octal
|
||||||
``content``
|
``content``
|
||||||
The binary file contents, which will be stored as a BLOB
|
The binary file contents, which will be stored as a BLOB
|
||||||
|
``content_text``
|
||||||
|
The text file contents, which will be stored as TEXT
|
||||||
``mtime``
|
``mtime``
|
||||||
The modification time of the file, as floating point seconds since the Unix epoch
|
The modification time of the file, as floating point seconds since the Unix epoch
|
||||||
``ctime``
|
``ctime``
|
||||||
|
|
@ -988,7 +992,7 @@ You can insert data piped from standard input like this::
|
||||||
|
|
||||||
The ``-`` argument indicates data should be read from standard input. The string passed using the ``--name`` option will be used for the file name and path values.
|
The ``-`` argument indicates data should be read from standard input. The string passed using the ``--name`` option will be used for the file name and path values.
|
||||||
|
|
||||||
When inserting data from standard input only the following column definitions are supported: ``name``, ``path``, ``content``, ``sha256``, ``md5`` and ``size``.
|
When inserting data from standard input only the following column definitions are supported: ``name``, ``path``, ``content``, ``content_text``, ``sha256``, ``md5`` and ``size``.
|
||||||
|
|
||||||
.. _cli_convert:
|
.. _cli_convert:
|
||||||
|
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "3.16"
|
VERSION = "3.17"
|
||||||
|
|
||||||
|
|
||||||
def get_long_description():
|
def get_long_description():
|
||||||
|
|
|
||||||
|
|
@ -1811,6 +1811,11 @@ def extract(
|
||||||
@click.option("--replace", is_flag=True, help="Replace files with matching primary key")
|
@click.option("--replace", is_flag=True, help="Replace files with matching primary key")
|
||||||
@click.option("--upsert", is_flag=True, help="Upsert files with matching primary key")
|
@click.option("--upsert", is_flag=True, help="Upsert files with matching primary key")
|
||||||
@click.option("--name", type=str, help="File name to use")
|
@click.option("--name", type=str, help="File name to use")
|
||||||
|
@click.option("--text", is_flag=True, help="Store file content as TEXT, not BLOB")
|
||||||
|
@click.option(
|
||||||
|
"--encoding",
|
||||||
|
help="Character encoding for input, defaults to utf-8",
|
||||||
|
)
|
||||||
@click.option("-s", "--silent", is_flag=True, help="Don't show a progress bar")
|
@click.option("-s", "--silent", is_flag=True, help="Don't show a progress bar")
|
||||||
@load_extension_option
|
@load_extension_option
|
||||||
def insert_files(
|
def insert_files(
|
||||||
|
|
@ -1823,6 +1828,8 @@ def insert_files(
|
||||||
replace,
|
replace,
|
||||||
upsert,
|
upsert,
|
||||||
name,
|
name,
|
||||||
|
text,
|
||||||
|
encoding,
|
||||||
silent,
|
silent,
|
||||||
load_extension,
|
load_extension,
|
||||||
):
|
):
|
||||||
|
|
@ -1842,7 +1849,10 @@ def insert_files(
|
||||||
--pk name
|
--pk name
|
||||||
"""
|
"""
|
||||||
if not column:
|
if not column:
|
||||||
column = ["path:path", "content:content", "size:size"]
|
if text:
|
||||||
|
column = ["path:path", "content_text:content_text", "size:size"]
|
||||||
|
else:
|
||||||
|
column = ["path:path", "content:content", "size:size"]
|
||||||
if not pk:
|
if not pk:
|
||||||
pk = "path"
|
pk = "path"
|
||||||
|
|
||||||
|
|
@ -1866,7 +1876,16 @@ def insert_files(
|
||||||
def to_insert():
|
def to_insert():
|
||||||
for path, relative_path in bar:
|
for path, relative_path in bar:
|
||||||
row = {}
|
row = {}
|
||||||
lookups = FILE_COLUMNS
|
# content_text is special case as it considers 'encoding'
|
||||||
|
|
||||||
|
def _content_text(p):
|
||||||
|
resolved = p.resolve()
|
||||||
|
try:
|
||||||
|
return resolved.read_text(encoding=encoding)
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
raise UnicodeDecodeErrorForPath(e, resolved)
|
||||||
|
|
||||||
|
lookups = dict(FILE_COLUMNS, content_text=_content_text)
|
||||||
if path == "-":
|
if path == "-":
|
||||||
stdin_data = sys.stdin.buffer.read()
|
stdin_data = sys.stdin.buffer.read()
|
||||||
# We only support a subset of columns for this case
|
# We only support a subset of columns for this case
|
||||||
|
|
@ -1874,6 +1893,9 @@ def insert_files(
|
||||||
"name": lambda p: name or "-",
|
"name": lambda p: name or "-",
|
||||||
"path": lambda p: name or "-",
|
"path": lambda p: name or "-",
|
||||||
"content": lambda p: stdin_data,
|
"content": lambda p: stdin_data,
|
||||||
|
"content_text": lambda p: stdin_data.decode(
|
||||||
|
encoding or "utf-8"
|
||||||
|
),
|
||||||
"sha256": lambda p: hashlib.sha256(stdin_data).hexdigest(),
|
"sha256": lambda p: hashlib.sha256(stdin_data).hexdigest(),
|
||||||
"md5": lambda p: hashlib.md5(stdin_data).hexdigest(),
|
"md5": lambda p: hashlib.md5(stdin_data).hexdigest(),
|
||||||
"size": lambda p: len(stdin_data),
|
"size": lambda p: len(stdin_data),
|
||||||
|
|
@ -1899,9 +1921,16 @@ def insert_files(
|
||||||
|
|
||||||
db = sqlite_utils.Database(path)
|
db = sqlite_utils.Database(path)
|
||||||
_load_extensions(db, load_extension)
|
_load_extensions(db, load_extension)
|
||||||
with db.conn:
|
try:
|
||||||
db[table].insert_all(
|
with db.conn:
|
||||||
to_insert(), pk=pk, alter=alter, replace=replace, upsert=upsert
|
db[table].insert_all(
|
||||||
|
to_insert(), pk=pk, alter=alter, replace=replace, upsert=upsert
|
||||||
|
)
|
||||||
|
except UnicodeDecodeErrorForPath as e:
|
||||||
|
raise click.ClickException(
|
||||||
|
UNICODE_ERROR.format(
|
||||||
|
"Could not read file '{}' as text\n\n{}".format(e.path, e.exception)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2149,6 +2178,12 @@ def _render_common(title, values):
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
class UnicodeDecodeErrorForPath(Exception):
|
||||||
|
def __init__(self, exception, path):
|
||||||
|
self.exception = exception
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
|
||||||
FILE_COLUMNS = {
|
FILE_COLUMNS = {
|
||||||
"name": lambda p: p.name,
|
"name": lambda p: p.name,
|
||||||
"path": lambda p: str(p),
|
"path": lambda p: str(p),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from click.testing import CliRunner
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import pytest
|
import pytest
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("silent", (False, True))
|
@pytest.mark.parametrize("silent", (False, True))
|
||||||
|
|
@ -23,6 +24,7 @@ def test_insert_files(silent):
|
||||||
"md5",
|
"md5",
|
||||||
"mode",
|
"mode",
|
||||||
"content",
|
"content",
|
||||||
|
"content_text",
|
||||||
"mtime",
|
"mtime",
|
||||||
"ctime",
|
"ctime",
|
||||||
"mtime_int",
|
"mtime_int",
|
||||||
|
|
@ -52,6 +54,7 @@ def test_insert_files(silent):
|
||||||
)
|
)
|
||||||
assert {
|
assert {
|
||||||
"content": b"This is file one",
|
"content": b"This is file one",
|
||||||
|
"content_text": "This is file one",
|
||||||
"md5": "556dfb57fce9ca301f914e2273adf354",
|
"md5": "556dfb57fce9ca301f914e2273adf354",
|
||||||
"name": "one.txt",
|
"name": "one.txt",
|
||||||
"path": "one.txt",
|
"path": "one.txt",
|
||||||
|
|
@ -60,6 +63,7 @@ def test_insert_files(silent):
|
||||||
}.items() <= one.items()
|
}.items() <= one.items()
|
||||||
assert {
|
assert {
|
||||||
"content": b"Two is shorter",
|
"content": b"Two is shorter",
|
||||||
|
"content_text": "Two is shorter",
|
||||||
"md5": "f86f067b083af1911043eb215e74ac70",
|
"md5": "f86f067b083af1911043eb215e74ac70",
|
||||||
"name": "two.txt",
|
"name": "two.txt",
|
||||||
"path": "two.txt",
|
"path": "two.txt",
|
||||||
|
|
@ -68,6 +72,7 @@ def test_insert_files(silent):
|
||||||
}.items() <= two.items()
|
}.items() <= two.items()
|
||||||
assert {
|
assert {
|
||||||
"content": b"Three is nested",
|
"content": b"Three is nested",
|
||||||
|
"content_text": "Three is nested",
|
||||||
"md5": "12580f341781f5a5b589164d3cd39523",
|
"md5": "12580f341781f5a5b589164d3cd39523",
|
||||||
"name": "three.txt",
|
"name": "three.txt",
|
||||||
"path": os.path.join("nested", "three.txt"),
|
"path": os.path.join("nested", "three.txt"),
|
||||||
|
|
@ -84,24 +89,65 @@ def test_insert_files(silent):
|
||||||
"mtime_iso": str,
|
"mtime_iso": str,
|
||||||
"mode": int,
|
"mode": int,
|
||||||
"fullpath": str,
|
"fullpath": str,
|
||||||
|
"content": bytes,
|
||||||
|
"content_text": str,
|
||||||
}
|
}
|
||||||
for colname, expected_type in expected_types.items():
|
for colname, expected_type in expected_types.items():
|
||||||
for row in (one, two, three):
|
for row in (one, two, three):
|
||||||
assert isinstance(row[colname], expected_type)
|
assert isinstance(row[colname], expected_type)
|
||||||
|
|
||||||
|
|
||||||
def test_insert_files_stdin():
|
@pytest.mark.parametrize(
|
||||||
|
"use_text,encoding,input,expected",
|
||||||
|
(
|
||||||
|
(False, None, "hello world", b"hello world"),
|
||||||
|
(True, None, "hello world", "hello world"),
|
||||||
|
(False, None, b"S\xe3o Paulo", b"S\xe3o Paulo"),
|
||||||
|
(True, "latin-1", b"S\xe3o Paulo", "S\xe3o Paulo"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_insert_files_stdin(use_text, encoding, input, expected):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
with runner.isolated_filesystem():
|
with runner.isolated_filesystem():
|
||||||
tmpdir = pathlib.Path(".")
|
tmpdir = pathlib.Path(".")
|
||||||
db_path = str(tmpdir / "files.db")
|
db_path = str(tmpdir / "files.db")
|
||||||
|
args = ["insert-files", db_path, "files", "-", "--name", "stdin-name"]
|
||||||
|
if use_text:
|
||||||
|
args += ["--text"]
|
||||||
|
if encoding is not None:
|
||||||
|
args += ["--encoding", encoding]
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["insert-files", db_path, "files", "-", "--name", "stdin-name"],
|
args,
|
||||||
catch_exceptions=False,
|
catch_exceptions=False,
|
||||||
input="hello world",
|
input=input,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.stdout
|
assert result.exit_code == 0, result.stdout
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
row = list(db["files"].rows)[0]
|
row = list(db["files"].rows)[0]
|
||||||
assert {"path": "stdin-name", "content": b"hello world", "size": 11} == row
|
key = "content"
|
||||||
|
if use_text:
|
||||||
|
key = "content_text"
|
||||||
|
assert {"path": "stdin-name", key: expected}.items() <= row.items()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.platform.startswith("win"),
|
||||||
|
reason="Windows has a different way of handling default encodings",
|
||||||
|
)
|
||||||
|
def test_insert_files_bad_text_encoding_error():
|
||||||
|
runner = CliRunner()
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
tmpdir = pathlib.Path(".")
|
||||||
|
latin = tmpdir / "latin.txt"
|
||||||
|
latin.write_bytes(b"S\xe3o Paulo")
|
||||||
|
db_path = str(tmpdir / "files.db")
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert-files", db_path, "files", str(latin), "--text"],
|
||||||
|
catch_exceptions=False,
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1, result.output
|
||||||
|
assert result.output.strip().startswith(
|
||||||
|
"Error: Could not read file '{}' as text".format(str(latin.resolve()))
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue