Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Databases and arrays

SQLite databases and NumPy arrays are read where they are, a page of rows at a time, and a file of several tables lists them like a directory.

SQLite

make_shop_db.py

import sqlite3

db = sqlite3.connect("shop.db")
db.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT, amount REAL)")
db.execute("CREATE TABLE customers (name TEXT, city TEXT)")
db.executemany("INSERT INTO orders (customer, amount) VALUES (?, ?)", [("ana", 9.5), ("bo", 3.25)])
db.executemany("INSERT INTO customers VALUES (?, ?)", [("ana", "Lima"), ("bo", "Oslo")])
db.commit()
db.close()
python3 make_shop_db.py
datui shop.db --table orders
datui shop.db/orders
cat shop.db | datui --table orders

Continuing from above, a database of several tables opens the home screen inside it:

datui shop.db
Extensions.db, .sqlite, .sqlite3, .db3; any other name by its first bytes, SQLite format 3
Readlazy, in place; downloaded first from a bucket or over HTTP(S)
--tableA table or view by name, or shop.db/orders
Info tabSQLite: page size, schema and user versions, text encoding, and each table’s columns and the rows ANALYZE stored. No table is counted
Not readA compressed database (shop.db.gz): decompress it first
The databaseWhat happens
One table or view of its ownOpens it
SeveralOpens the home screen inside the database: a row per table and view, like a directory of tables. Enter opens one; q comes back to the list
Several, downloaded or piped inRefused with the names of its tables; --table picks one

SQLite’s own tables (sqlite_master, sqlite_sequence, the sqlite_stat tables, a full-text index’s shadow tables) are hidden until Ctrl+A; --table opens them by name. The home screen labels a database with its tables (3 tables).

T at the table lists the database’s tables and views, each with its kind, columns and the rows ANALYZE stored, and opens another; so does Enter on a table of the SQLite tab. Nothing is counted to list them.

A table is read in place; nothing is copied.

How
The rows on screenRead from SQLite a page at a time, by the table’s rowid (or primary key), so the first rows show at once and End costs what the top does
Row countSQLite’s count(*), in the background
Sort and filters from the sidebarRun in SQLite as ORDER BY and WHERE, so an index on the column serves them. Ties keep the table’s order and nulls sort last, as for any other file
A query, analysis, Data Quality, a chart, an exportRead the columns they use from SQLite a batch at a time; what they hold is in memory, as for a JSON or Excel file. A query’s simple comparisons run in SQLite
Leaving the table (Ctrl+O, quit)Stops whatever SQLite is running for it

A view is paged by position and cannot be reversed with r in SQLite (Polars does it). A sort on a column without an index has SQLite sort the rows for each page; SQLite may use temporary files in the temp directory to do so.

Columns are typed by what they declare:

DeclaredColumn
INTEGER, INT, BIGINT, anything with INTi64
REAL, FLOAT, DOUBLEf64
TEXT, VARCHAR(n), CLOBstr
BLOBbinary
nothing, NUMERIC, DECIMAL, BOOLEAN, DATEby the values in the first 1,000 rows: whole numbers i64, numbers f64, blobs binary, text str

SQLite lets a column hold values of any type. A column whose first 1,000 rows hold values of several types is read as text, numbers as SQLite writes them and blobs as X'0A1B'. After the open, one pass over the table checks the rest: a value further on that is not a number, in a number column, reads as null, and the Info panel’s Notes tab says how many. Dates stay text, as SQLite stores them.

The database is only read:

OpenedRead only, with query_only and defensive mode. Extensions cannot be loaded, and reading a table runs no trigger
A WAL database with a -wal fileRead through it, so what another program has committed is seen. SQLite creates the -shm index beside it if it is missing
A WAL database without a -wal fileRead as the file stands (SQLite’s immutable), writing nothing and taking no lock. A program that starts writing it during the read can make the read fail or come out wrong
A -wal without its -shm, in a directory datui cannot write toAn error: read without the WAL, it would lack what was committed there
A -journal left by a program that stopped mid-writeAn error: datui does not roll it back. Opening the database once with the sqlite3 tool does
A program writing the database meanwhiledatui waits up to 2 seconds for its lock. Without WAL, the program cannot commit while datui reads, which is a page at a time except for a whole-table read
Not a SQLite database, or damagedAn error

NumPy

make_arrays.py writes prices.npy and run.npz:

import ctypes
import zipfile


class Preamble(ctypes.LittleEndianStructure):
    _layout_ = "ms"
    _pack_ = 1  # no padding between fields
    _fields_ = [
        ("magic", ctypes.c_char * 6),
        ("major", ctypes.c_uint8),
        ("minor", ctypes.c_uint8),
        ("header_len", ctypes.c_uint16),
    ]


def npy(descr, shape, values):
    """An .npy file: the preamble, a header padded to 64 bytes, then the values."""
    header = repr({"descr": descr, "fortran_order": False, "shape": shape}).encode()
    header += b" " * (63 - (10 + len(header)) % 64) + b"\n"
    return bytes(Preamble(b"\x93NUMPY", 1, 0, len(header))) + header + bytes(values)


def f8(*values):  # little-endian float64, as "<f8" says
    return (ctypes.c_double.__ctype_le__ * len(values))(*values)


def f4(*values):  # little-endian float32, "<f4"
    return (ctypes.c_float.__ctype_le__ * len(values))(*values)


with open("prices.npy", "wb") as f:
    f.write(npy("<f8", (3,), f8(1.5, 2.5, 4.0)))
with zipfile.ZipFile("run.npz", "w") as z:
    z.writestr("weights.npy", npy("<f4", (2, 2), f4(1, 2, 3, 4)))
    z.writestr("bias.npy", npy("<f4", (2,), f4(0.5, -0.5)))
python3 make_arrays.py
datui prices.npy
datui run.npz --table weights
datui run.npz/weights
Extensions.npy, .npz; any other name by its first bytes, \x93NUMPY
Readlazy, from a map of the file. An array saved with np.savez_compressed is decompressed to the temp directory first, and removed when the dataset closes
--tableAn array of an .npz archive by name, or run.npz/weights
Several arraysdatui run.npz opens the home screen inside the archive, a row per array in the order saved. Downloaded or piped in, it is refused with the arrays’ names
Info tabNumPy: shape, type, order and format version, and each field’s type and offset
The arrayColumns
1-DOne, named for the file (prices.npy is prices) or the archive’s array
2-DOne per index, 0 to n-1; more than 1,024 make one Array column, values
Structured ([('ts', '<u8'), ('px', '<f8')])One per field; a nested field is outer.inner, a subarray field an Array column
0-DOne row
3-D or moreAn error that gives the shape
dtypeColumn
b1bool
i1 to i8, u1 to u8The integer of that width
f2, f4f32
f8f64
c8, c16An Array of two floats: real, imaginary
Sstr, NUL padding trimmed
Ustr, from UTF-32
Vbinary
M8[ns], M8[us], M8[ms]datetime in that unit
M8[s], M8[m], M8[h]datetime[ms]
M8[D]date
m8[...]duration, by the same units
M8 and m8 in months, years or finer than nsi64, the count as stored
OAn error: Python objects are pickled, and datui does not unpickle
  • NaT is null.

  • Big-endian (>i4) and little-endian fields mix in one array.

  • Fortran (column-major) order reads the same as C order.

  • Padding fields (align=True) are left out, and fields at offsets (offsets, itemsize) are read where they are.

  • A file shorter than its shape says shows the rows it holds; the Notes tab says so.

  • A file named without .npy is known by its first bytes, \x93NUMPY.

  • NaT is null.

  • Big-endian (>i4) and little-endian fields mix in one array.

  • Fortran (column-major) order reads the same as C order.

  • Padding fields (align=True) are left out, and fields at offsets (offsets, itemsize) are read where they are.

  • A file shorter than its shape says shows the rows it holds; the Notes tab says so.