insert-files: add --sqlar flag and content_sqlar column for sqlar-compatible compressed payloads

Adds a --sqlar option that defaults insert-files to the name/mode/mtime/sz/data
schema used by SQLite's own sqlar archive format, plus a content_sqlar column
type that zlib-compresses content the same way sqlar_compress() does (only
when compression actually shrinks the data). Reuses the existing -c coldef
parsing added for fixed-literal metadata columns, no changes needed there.
Existing BLOB/TEXT content/content_text imports are untouched.

Closes #141
This commit is contained in:
Parker Gurney 2026-08-05 14:11:30 -07:00
commit daf2ebbbaf
3 changed files with 91 additions and 2 deletions

View file

@ -11,6 +11,7 @@ import pathlib
import pdb # noqa: T100
import sys
import textwrap
import zlib
from datetime import datetime, timezone
from runpy import run_module
from typing import Any
@ -2913,6 +2914,11 @@ def extract(
@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("--text", is_flag=True, help="Store file content as TEXT, not BLOB")
@click.option(
"--sqlar",
is_flag=True,
help="Store file content zlib-compressed, compatible with SQLite's sqlar format",
)
@click.option(
"--encoding",
help="Character encoding for input, defaults to utf-8",
@ -2930,6 +2936,7 @@ def insert_files(
upsert,
name,
text,
sqlar,
encoding,
silent,
load_extension,
@ -2949,13 +2956,23 @@ def insert_files(
-c size:size \\
--pk name
"""
if text and sqlar:
raise click.ClickException("Cannot use --text and --sqlar together")
if not column:
if text:
column = ["path:path", "content_text:content_text", "size:size"]
elif sqlar:
column = [
"name:name",
"mode:mode",
"mtime:mtime_int",
"sz:size",
"data:content_sqlar",
]
else:
column = ["path:path", "content:content", "size:size"]
if not pks:
pks = ["path"]
pks = ["name"] if sqlar else ["path"]
def yield_paths_and_relative_paths():
for f_or_d in file_or_dir:
@ -2997,6 +3014,9 @@ def insert_files(
"content_text": lambda p, data=stdin_data: data.decode(
encoding or "utf-8"
),
"content_sqlar": lambda p, data=stdin_data: sqlar_compress(
data
),
"sha256": lambda p, data=stdin_data: hashlib.sha256(
data
).hexdigest(),
@ -3688,6 +3708,14 @@ class UnicodeDecodeErrorForPath(Exception):
self.path = path
def sqlar_compress(data):
# Matches SQLite's sqlar_compress(): use the zlib-compressed blob only
# if it is actually smaller than the original, otherwise store as-is.
# https://sqlite.org/sqlar.html
compressed = zlib.compress(data)
return compressed if len(compressed) < len(data) else data
FILE_COLUMNS = {
"name": lambda p: p.name,
"path": lambda p: str(p),
@ -3696,6 +3724,7 @@ FILE_COLUMNS = {
"md5": lambda p: hashlib.md5(p.resolve().read_bytes()).hexdigest(),
"mode": lambda p: p.stat().st_mode,
"content": lambda p: p.resolve().read_bytes(),
"content_sqlar": lambda p: sqlar_compress(p.resolve().read_bytes()),
"mtime": lambda p: p.stat().st_mtime,
"ctime": lambda p: p.stat().st_ctime,
"mtime_int": lambda p: int(p.stat().st_mtime),