TSVZ 5.0__tar.gz → 5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 5.0
3
+ Version: 5.2
4
4
  Summary: Key-value store kept in a TSV / CSV / NSV / PSV file, with tsvz-spec-v1 support
5
5
  Home-page: https://github.com/tsvz-org/TSVZ
6
6
  Author: Yufei Pan
@@ -39,7 +39,7 @@ key-value store in a delimiter-separated text file**. The first column of each
39
39
  row is the key; the file behaves like an ordered dictionary that is persisted to
40
40
  disk as you change it.
41
41
 
42
- **Version 5.0**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
42
+ **Version 5.1**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
43
43
  next to your code and `import TSVZ`, or install it.
44
44
 
45
45
  ```bash
@@ -163,6 +163,20 @@ named like an operation then needs a path prefix (`./read`).
163
163
  with a warning.
164
164
  - **TSVZ extensions:**
165
165
  - `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
166
+ - `--x-header H` makes line 1 of a `.tsv`-family file its header for every operation
167
+ when line 1 is H: the same number of cells, each equal to H's once the whitespace
168
+ around it is taken off (a trailing empty cell counts: `id\tv\t` is not `id\tv`; a UTF-8
169
+ byte order mark before line 1, as Excel writes one, is skipped). It
170
+ is then never a key, row or count, `scrub` and `clear` keep it, and
171
+ `read --format table` shows it above the rows. When line 1 differs, the file has no
172
+ header and line 1 is a row, with 3.39's "Header mismatch" warning (`--x-strict`: an
173
+ error, exit 1, nothing written). A missing or empty file gets H first. Without
174
+ `--x-header`, line 1 is a row, as in 3.39. (The library's own header check, a
175
+ `header=` given to `readTabularFile`, `appendTabularFile`, `clearTabularFile`,
176
+ `scrubTabularFile` or `TSVZed`, is 3.39's, looser one: each cell of H within the
177
+ cell of line 1 under it, so `idaho\tvalue` passes for `id\tv`.) A rewrite without
178
+ `--x-header` can put a row equal to H on line 1
179
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
166
180
  - `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
167
181
 
168
182
  ## Serving a store
@@ -193,7 +207,13 @@ status stay the same.
193
207
  and `fsync`s everything and removes `STORE.serve`.
194
208
  - **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
195
209
  lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
196
- than the server's, use the files too.
210
+ than the server's, use the files too. A server started with `--x-header H`
211
+ answers every request by the `--x-header` rule above, and one started with
212
+ `--x-strict` too answers as a direct `--x-strict` command does: a line 1 that is
213
+ not H fails the request (status 1, nothing written), and reads and scrubs drop
214
+ the rows of another width. It does not start when line 1 is not H then. The
215
+ table of a routed `read --format table` shows no H line: its first row is set
216
+ as the table's header, as with no `--x-header`.
197
217
 
198
218
  ```python
199
219
  import TSVZ
@@ -261,7 +281,9 @@ microseconds as a write starts is followed as a rotation instead); while that re
261
281
  fails or is refused, the rows are appended to a new file there, and memory keeps all of
262
282
  them for the rewrite that gets through. A compressed file another writer left damaged
263
283
  is made again too (L5: every read stops at the damage, so rows
264
- appended after it would be read by no one); the bytes after the damage go. While that
284
+ appended after it would be read by no one): what the read did not get goes, the bytes after
285
+ the damage and also the lines of the damaged member before it that the codec never handed
286
+ over (Python's gzip gives none of a first member cut short, where `zcat` shows them). While that
265
287
  rewrite fails, the rows are appended after the damage and memory keeps them for the
266
288
  next rewrite that gets through; while it is refused (a row changed in place to text
267
289
  the encoding cannot hold), they are appended after it as in 4.9, and the next read
@@ -301,12 +323,184 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
301
323
  memory's. An explicit `reload()` while the file is damaged replaces memory with the
302
324
  damaged file, as asked, and warns.
303
325
 
326
+ ## Upgrading to 5.2 from 5.1
327
+
328
+ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
329
+ 5.2 writes.
330
+
331
+ - **A scrub of a `.tsv` whose rows were all deleted shrinks it.** `scrubTabularFile`,
332
+ `tsvz scrub` (also through a `tsvz serve` handler) of a `.tsv`-family file (`.tsv`,
333
+ `.csv`, `.nsv`, `.psv` and their compressed forms) in which every key has been deleted
334
+ leaves the file as an empty store: the header line it has, and its `#_defaults_#`
335
+ lines as a scrub of a file with rows keeps them (an empty one after the header, the
336
+ last non-empty one after it, both as wide as the header, or as the column count the
337
+ read took, which caller defaults set when given). A file with no header line keeps its
338
+ empty first line, or becomes 0 bytes when its first line was a row; a first line that
339
+ is no row (a comment, say) becomes an empty line. A defaults line it keeps follows an
340
+ empty first line, which the scrub then writes. Comment lines go, as a scrub of a file
341
+ with rows already drops them. A scrub that is given
342
+ a header (and checks it, the default) keeps the file's first line as its header, an
343
+ empty one included, with a "Header mismatch" warning when it differs; the header it is
344
+ given is written only with `verifyHeader=False`, or when the file's first line cannot be
345
+ decoded (plain text named `.gz`); `tsvz scrub --x-header` follows the `--x-header` rule
346
+ below instead. A scrub of a path that is not there
347
+ makes no file and returns `{}`, as in 5.1, unless `createIfNotExist=True`. A compressed
348
+ file whose read decodes no row (plain text named `.gz`, a first member cut short) is
349
+ made again as an empty store, as a scrub of a damaged file with rows already was in
350
+ 5.1: what the read did not get goes, the bytes after the damage and also the lines of the
351
+ damaged member before it that the codec never handed over (Python's gzip gives none of
352
+ a first member cut short, where `zcat` shows them) (see the damaged-stream notes above), and
353
+ `tsvz clear --x-header H --x-strict` (`clearTabularFile(verifyHeader=True)`) now clears
354
+ such a file, where 5.1 exited 1 with the codec's error.
355
+ 5.1 and 5.0 left such a file as it was, deletion lines included, so a writer that
356
+ appended keys, deleted them all and scrubbed, over and over, saw the file grow without
357
+ limit (205, 405, 605 bytes, ... for 20 keys a cycle); now it is the same size after
358
+ every scrub. A file with no byte (or a compressed one with only empty streams) is
359
+ still left alone, and so is a file whose strict scrub (`strict=True`, `--x-strict`) read
360
+ dropped a line for its width and found no row: that line is not a deletion, as 5.1 left
361
+ it. A scrub that finds the file changed since its read still reads
362
+ again and writes nothing if it keeps changing, as for a file with rows.
363
+ - **`get_file_obj('ab')` on a compressed store writes its trailer before the lock is let
364
+ go.** A program that calls it on a `.tsv.gz`, `.bz2`, `.xz` or `.zst` store and writes
365
+ through the file now has all of it, the gzip trailer and the bz2, xz and zstd data included,
366
+ in the file when `release_file_obj()` unlocks it. 5.1 and earlier wrote them after the
367
+ unlock, where another writer could append in between and corrupt the stream.
368
+ - **A `TSVZed` no longer lets a file grow with every session of sets and deletes.** It
369
+ rewrites a `.tsv`-family file at open when the file holds a key's row and the deletion
370
+ of it, as 3.39 did only for a key with two values, so a program that sets keys and
371
+ deletes them in each session (default settings, or `rewrite_interval`) keeps the file
372
+ the same size (205, 405, 605 bytes, ... for 20 keys a session before). Every session
373
+ that opens such a file now rewrites it once. With `rewrite_on_load=False` the open
374
+ does not, and the next exit or periodic rewrite does it (`rewrite_on_exit=True`,
375
+ `rewrite_interval`). What such a rewrite does, it now does at a plain open:
376
+ - it cuts a row wider than the header, and the cut stays
377
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)):
378
+ `id\tv\na\t1\t2\nb\t1\nb\t\n` becomes `id\tv\na\t1 \n`, and
379
+ `id\tv\na\t\t7\nb\t1\nb\t\n` becomes `id\tv\na\t \n`, the 7 lost;
380
+ - it writes over the file in place and pads with spaces (`a\t1 ` above): TSVZ readers
381
+ strip them, tools that read the raw bytes (`awk`, `cut`) see them;
382
+ - on a file it cannot write (read-only), it prints `Failed to open file … Permission
383
+ denied` twice and leaves the file as it is, as 5.1 did for a key with two values.
384
+ - **`--x-header H` makes line 1 the header for every `tsvz` operation.** On a
385
+ `.tsv`-family file whose line 1 is H (an exact match: the same cells, each without the
386
+ whitespace around it, a trailing empty cell included), line 1 is the header: `keys`,
387
+ `len`, `read`, `get`, `has`, `pop`, `popitem` and `delete` never list, count, print,
388
+ pop or delete it, and `scrub` and `clear` keep it. When line 1 differs, the file has no
389
+ header and line 1 is a row, with 3.39's "Header mismatch" warning, for the reads and
390
+ the scrub alike, so a scrub keeps the rows a read shows (but for the column-width cases
391
+ below), and in such a file it never writes a row that is H as line 1: it puts an empty
392
+ line first (`a\t1\nid\tv\na\t\n` becomes `\nid\tv\n`), so the reads still take that row
393
+ for a row. With `--x-strict` a line 1 that is not H is an error (exit 1, nothing
394
+ written) for every operation, through a handler too. A missing or empty file gets H
395
+ first at its first write, as in 5.1. A `tsvz serve STORE --x-header H` handler applies
396
+ the rule to every request it answers, routed `tsvz` commands included; with
397
+ `--x-strict` it is strict for them as above (5.1 ignored `--x-strict` at `serve`): such
398
+ a handler refuses to start on a file whose line 1 is not H, its routed `.tsv` reads and
399
+ scrubs drop lines of another column count, and its `.tsvz` reads are strict, as for a
400
+ direct `--x-strict` command. The library's own header check (`header=` to
401
+ `readTabularFile`, `appendTabularFile`, `clearTabularFile`, `scrubTabularFile`,
402
+ `TSVZed`) is unchanged, 3.39's containment check. The CLI's writes check line 1 more
403
+ strictly than 5.1's did: 5.1's `set`, `append`, `delete` and `clear` with `--x-header`
404
+ silently took a line 1 that merely held H's cells (`id\tvalue` for `id\tv`) for the
405
+ header and fitted rows to H's width; now they warn and write the row at its own width,
406
+ and with `--x-strict` exit 1. A rewrite without `--x-header` (a scrub without it,
407
+ `scrubTabularFile`, a `TSVZed` rewrite) can still put a row equal to H on line 1 of a
408
+ header-less file
409
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
410
+ In 5.1 only `set`, `append`, `delete` and `clear` used `--x-header`: the reads and
411
+ `scrub` took line 1 as a row (`tsvz keys`
412
+ printed `id a b` for two keys, `len` said 3, `popitem first` popped the header), and
413
+ so did every request to a handler. Without `--x-header` nothing changed: line 1 is a
414
+ row, as in 5.1 and 3.39. In a `.tsvz` store `pop`, `popitem` and `setdefault` now check
415
+ `--x-header` as `set` does; nothing else changed there.
416
+ - **Known, mostly as in 5.1: a scrub, a rewrite or a read takes one column count for every row,
417
+ and can lose cells or keys.** A scrub given defaults narrower than the rows, a row wider
418
+ than the header, and a `.tsv` that begins with a bare `#_defaults_#` line (read without
419
+ defaults) are examples; the entry lists each, and which is new in 5.2
420
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)).
421
+ A strict scrub of a file whose read dropped a line for its width and found no row leaves it,
422
+ so it grows again over append, delete-all and scrub cycles
423
+ ([limitations](docs/limitations.md#a-strict-scrub-leaves-a-file-whose-read-dropped-a-line-for-its-width)).
424
+ Both wait for the column-width design after 5.2.
425
+
426
+ ## Upgrading to 5.1 from 5.0
427
+
428
+ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
429
+ 5.1 writes. Most of the changes are in scrub and clear.
430
+
431
+ - **Scrub and clear of a `.tsv` write over the file.** `scrubTabularFile`,
432
+ `clearTabularFile`, `tsvz scrub` and `tsvz clear` (on the command line or through a
433
+ `tsvz serve` handler), `TSVZClient.clear()` and `TSVZedLite.clear_file()` of a
434
+ `.tsv`-family file write the new content over the file and then cut it, as
435
+ `TSVZed`'s rewrites do, so a reader that takes no lock never meets the file emptied.
436
+ They take the file's lock, so they wait while another writer holds it. 5.0 emptied
437
+ the file first, with no lock (`TSVZedLite` truncated its own descriptor).
438
+ - **A scrub keeps another process's rows.** A scrub of a `.tsv` writes only if the
439
+ file is still what its read saw. Otherwise it reads again, up to three times, and if
440
+ the file kept changing it writes nothing and warns: `tsvz scrub` exits 1 (`scrub
441
+ refused; nothing was written`), as for a `.tsvz` (C6), and a handler answers refused.
442
+ `scrubTabularFile` still returns the rows it read. 5.0 wrote over a row appended
443
+ after its read.
444
+ - **`scrubTabularFile(lastLineOnly=True)` is a read.** It returns the last row and
445
+ writes nothing, as for a `.tsvz`. 5.0 and 3.39 wrote that row back as deletion lines
446
+ (`id\tv\na\t\n3\t\n`), and the store was gone.
447
+ - **A scrub keeps the file's `#_defaults_#` lines.** The defaults in effect at the end
448
+ of the read are written after the rows: the last non-empty defaults line, its empty
449
+ cells filled from the line before it as a reader fills them. An empty defaults line
450
+ that was read (it turns defaults off) is written before the rows, padded to their
451
+ width. Every row reads as it did to a reader given the scrub's defaults and
452
+ `strict`. Defaults a caller passes still fill the rows' empty cells, as in 5.0, but
453
+ are not written as a defaults line, and the caller's list is not changed. 5.0 and
454
+ 3.39 dropped the lines, so rows appended later got no defaults.
455
+ - **The header stays on line 1.** A scrub given no header (`tsvz scrub` of a `.tsv`,
456
+ `scrubTabularFile(path)`) and a `TSVZed` given none, of a file
457
+ that has a header line, keep that line first, with a `TSVZed`'s defaults line right
458
+ after it. 5.0 and 3.39 put an empty line in front of it, so a reader given the header
459
+ read the header as a row. A header-less file keeps the empty first line it begins
460
+ with, and new files and clears given no header still get one, as in 5.0.
461
+ - **Interrupts.** A scrub or clear of either dialect keeps a copy of only the bytes it
462
+ overwrites, to put back if it is interrupted before the file is cut to its new
463
+ length; once the cut is made the rewrite is done and the new content stays. 5.0 kept
464
+ a copy of the whole `.tsvz` part and put it all back. (A scrub still reads the whole
465
+ store, and a `.tsvz` clear reads it too, to keep its markers.)
466
+ - **A compressed store cleared to nothing is one empty stream.** A `.tsvz.gz` (or
467
+ `.bz2`, `.xz`, `.zst`) cleared or scrubbed to nothing holds one empty compressed
468
+ stream, which `bzcat`, `xzcat` and Python's `bz2` and `lzma` can read; 5.0 left 0
469
+ bytes.
470
+ - **A header-checking clear of an empty file writes the header it was given.** A clear
471
+ that checks the header (`verifyHeader=True`, `tsvz clear --x-header H --x-strict`) of a
472
+ 0-byte `.tsv`, or of a compressed file with no byte or only empty streams (another
473
+ tool's), takes it for an empty store with that header, as every reader does (L9), and
474
+ writes H without a "Header mismatch" warning. 5.0 wrote an empty line and warned of a
475
+ mismatch, so a strict `TSVZed` given H then refused the file, and raised on a 0-byte
476
+ `.bz2`, `.xz` or `.zst`.
477
+ - **`tsvz --help`** says what `clear` keeps: a `.tsvz` its header and markers, a
478
+ `.tsv` the `--x-header` it is given. (With `--x-strict` too, a `.tsv` clear keeps the
479
+ file's own header line.)
480
+ `TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
481
+ a program asks for.
482
+ - **Known in 5.1, fixed in 5.2: a scrub of a `.tsv` whose rows were all deleted did not
483
+ compact it** when the scrub was given the file's header (`scrubTabularFile(path, header=H)`)
484
+ or the file had no header line: it left the file as it was, deletion lines included;
485
+ reads were right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file read the header
486
+ line as a row, so it did compact it.)
487
+ - **Known: a scrub run with other defaults than the store's program can change how
488
+ early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
489
+ cells before any defaults line, the reset line the scrub writes before the rows keeps
490
+ those cells empty for a program that opens the store with its own defaults; run the
491
+ scrub with the defaults that program uses
492
+ ([limitations](docs/limitations.md#a-scrub-run-with-other-defaults-than-the-stores-program-can-change-how-early-rows-read)).
493
+ - **Running programs keep 5.0** until they are restarted: long-running programs that
494
+ import TSVZ, and running `tsvz serve` handlers.
495
+
304
496
  ## Upgrading to 5.0 from 4.9
305
497
 
306
498
  - **Writes refuse text they cannot encode.** A key, value, header, defaults line or
307
499
  marker the file's encoding cannot hold (UTF-8 for `.tsvz`; `encoding=` for a
308
500
  `.tsv`), such as the lone surrogate `os.fsdecode` gives for a non-UTF-8 file name,
309
- raises `TSVZEncodeError` (a `ValueError`) and writes nothing; 4.9 stored `?`.
501
+ raises `TSVZEncodeError` (a `ValueError`) and writes nothing; 4.9 stored `?` (its
502
+ `tsvz` stored U+FFFD for a non-UTF-8 argument, and its `.tsvz` file functions raised
503
+ `UnicodeEncodeError`).
310
504
  `errors='replace'` brings `?` back. Under a non-UTF-8 `encoding=`, a key read as
311
505
  U+FFFD cannot be deleted or updated. `tsvz` exits 1 with the message. See Fault
312
506
  tolerance.
@@ -324,8 +518,7 @@ damaged file, as asked, and warns.
324
518
  merged into it (Fault tolerance).
325
519
  - **`.tsvz` stores** see another process's append made right after their load, a
326
520
  commit or a clear (4.9 missed it until the next change), keep the rows set while
327
- they reload, refuse `#_snapshot_#` (spec §19.10.2), and raise a strict header
328
- mismatch at the first read.
521
+ they reload, and raise a strict header mismatch at the first read.
329
522
  - **`.tsv` files:** an append to an existing empty file writes the header first, and
330
523
  reading one gives an empty store; see L8 to L14 for the rest.
331
524
  - **`tsvz`** runs every operation through the engine `tsvz serve` uses, with the same
@@ -334,8 +527,15 @@ damaged file, as asked, and warns.
334
527
  file. The 3.39 warning that a file name does not end in `.tsv` is printed only
335
528
  when a command creates such a file, and a write that cannot be made reports the
336
529
  operating system's error.
337
- - **macOS and other systems that lock with `lockf`:** 4.9's close gate is gone, so
338
- a read in another thread of the process can drop a write's lock
530
+ - **Running programs keep 4.9.** Long-running programs that import TSVZ, and running
531
+ `tsvz serve` handlers, keep 4.9 until they are restarted. 5.0's `tsvz stop` cannot
532
+ stop a 4.9 handler of a `.tsv` (or other tab- or NUL-delimited) file: 4.9 wrote the
533
+ delimiter into the pointer file's `x-delimiter` as `<sep>` (a NUL as `<NUL>`), which
534
+ 5.0 decodes to the TAB or NUL itself and refuses as a control character. Send that
535
+ handler SIGTERM instead: its pid is on the `pid` line of its pointer file
536
+ (`STORE.serve`), and it finishes its writes and exits.
537
+ - **macOS and other systems that lock with `lockf`:** as in 4.9, a read in another
538
+ thread of the process can still drop a write's lock
339
539
  ([docs/limitations.md](docs/limitations.md)).
340
540
 
341
541
  ## Breaking changes from 3.39
@@ -358,6 +558,11 @@ damaged file, as asked, and warns.
358
558
  | L12 | A rewrite of a file whose header line has no newline writes the rows on lines of their own; 3.39 wrote the first one onto the header line. |
359
559
  | L13 | A compressed file is rewritten in members of at most 1 MiB, as appends already were: the bytes differ, the rows read are the same, and 3.39 reads them. |
360
560
  | L14 | `TSVZed` opening a compressed file in a codec this Python lacks (zstd before Python 3.14) raises `ImportError` instead of reading the compressed bytes as text. |
561
+ | L15 | `scrubTabularFile`, `clearTabularFile`, `tsvz scrub`, `tsvz clear` and `TSVZedLite.clear_file()` write the new content over the file and then cut it, under the file's lock; 3.39 emptied the file first, with no lock, so a reader could meet it empty. |
562
+ | L16 | A scrub writes only if the file is still what its read saw: it reads again up to three times, then writes nothing, with a warning (`tsvz scrub` exits 1, C6); 3.39 wrote over rows appended after its read. |
563
+ | L17 | `scrubTabularFile(lastLineOnly=True)` returns the last row and writes nothing; 3.39 wrote that row back as deletion lines, and the store was gone. |
564
+ | L18 | A scrub keeps the file's `#_defaults_#` lines (an empty one before the rows, the defaults in effect after them), so every row reads as it did; 3.39 dropped them, so rows appended later got no defaults. |
565
+ | L19 | A scrub given no header, and a `TSVZed` given none, keep a file's header line first; 3.39 put an empty line in front of it, and a reader given the header then read the header as a row. |
361
566
 
362
567
  ### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
363
568
 
@@ -7,7 +7,7 @@ key-value store in a delimiter-separated text file**. The first column of each
7
7
  row is the key; the file behaves like an ordered dictionary that is persisted to
8
8
  disk as you change it.
9
9
 
10
- **Version 5.0**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
10
+ **Version 5.1**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
11
11
  next to your code and `import TSVZ`, or install it.
12
12
 
13
13
  ```bash
@@ -131,6 +131,20 @@ named like an operation then needs a path prefix (`./read`).
131
131
  with a warning.
132
132
  - **TSVZ extensions:**
133
133
  - `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
134
+ - `--x-header H` makes line 1 of a `.tsv`-family file its header for every operation
135
+ when line 1 is H: the same number of cells, each equal to H's once the whitespace
136
+ around it is taken off (a trailing empty cell counts: `id\tv\t` is not `id\tv`; a UTF-8
137
+ byte order mark before line 1, as Excel writes one, is skipped). It
138
+ is then never a key, row or count, `scrub` and `clear` keep it, and
139
+ `read --format table` shows it above the rows. When line 1 differs, the file has no
140
+ header and line 1 is a row, with 3.39's "Header mismatch" warning (`--x-strict`: an
141
+ error, exit 1, nothing written). A missing or empty file gets H first. Without
142
+ `--x-header`, line 1 is a row, as in 3.39. (The library's own header check, a
143
+ `header=` given to `readTabularFile`, `appendTabularFile`, `clearTabularFile`,
144
+ `scrubTabularFile` or `TSVZed`, is 3.39's, looser one: each cell of H within the
145
+ cell of line 1 under it, so `idaho\tvalue` passes for `id\tv`.) A rewrite without
146
+ `--x-header` can put a row equal to H on line 1
147
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
134
148
  - `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
135
149
 
136
150
  ## Serving a store
@@ -161,7 +175,13 @@ status stay the same.
161
175
  and `fsync`s everything and removes `STORE.serve`.
162
176
  - **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
163
177
  lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
164
- than the server's, use the files too.
178
+ than the server's, use the files too. A server started with `--x-header H`
179
+ answers every request by the `--x-header` rule above, and one started with
180
+ `--x-strict` too answers as a direct `--x-strict` command does: a line 1 that is
181
+ not H fails the request (status 1, nothing written), and reads and scrubs drop
182
+ the rows of another width. It does not start when line 1 is not H then. The
183
+ table of a routed `read --format table` shows no H line: its first row is set
184
+ as the table's header, as with no `--x-header`.
165
185
 
166
186
  ```python
167
187
  import TSVZ
@@ -229,7 +249,9 @@ microseconds as a write starts is followed as a rotation instead); while that re
229
249
  fails or is refused, the rows are appended to a new file there, and memory keeps all of
230
250
  them for the rewrite that gets through. A compressed file another writer left damaged
231
251
  is made again too (L5: every read stops at the damage, so rows
232
- appended after it would be read by no one); the bytes after the damage go. While that
252
+ appended after it would be read by no one): what the read did not get goes, the bytes after
253
+ the damage and also the lines of the damaged member before it that the codec never handed
254
+ over (Python's gzip gives none of a first member cut short, where `zcat` shows them). While that
233
255
  rewrite fails, the rows are appended after the damage and memory keeps them for the
234
256
  next rewrite that gets through; while it is refused (a row changed in place to text
235
257
  the encoding cannot hold), they are appended after it as in 4.9, and the next read
@@ -269,12 +291,184 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
269
291
  memory's. An explicit `reload()` while the file is damaged replaces memory with the
270
292
  damaged file, as asked, and warns.
271
293
 
294
+ ## Upgrading to 5.2 from 5.1
295
+
296
+ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
297
+ 5.2 writes.
298
+
299
+ - **A scrub of a `.tsv` whose rows were all deleted shrinks it.** `scrubTabularFile`,
300
+ `tsvz scrub` (also through a `tsvz serve` handler) of a `.tsv`-family file (`.tsv`,
301
+ `.csv`, `.nsv`, `.psv` and their compressed forms) in which every key has been deleted
302
+ leaves the file as an empty store: the header line it has, and its `#_defaults_#`
303
+ lines as a scrub of a file with rows keeps them (an empty one after the header, the
304
+ last non-empty one after it, both as wide as the header, or as the column count the
305
+ read took, which caller defaults set when given). A file with no header line keeps its
306
+ empty first line, or becomes 0 bytes when its first line was a row; a first line that
307
+ is no row (a comment, say) becomes an empty line. A defaults line it keeps follows an
308
+ empty first line, which the scrub then writes. Comment lines go, as a scrub of a file
309
+ with rows already drops them. A scrub that is given
310
+ a header (and checks it, the default) keeps the file's first line as its header, an
311
+ empty one included, with a "Header mismatch" warning when it differs; the header it is
312
+ given is written only with `verifyHeader=False`, or when the file's first line cannot be
313
+ decoded (plain text named `.gz`); `tsvz scrub --x-header` follows the `--x-header` rule
314
+ below instead. A scrub of a path that is not there
315
+ makes no file and returns `{}`, as in 5.1, unless `createIfNotExist=True`. A compressed
316
+ file whose read decodes no row (plain text named `.gz`, a first member cut short) is
317
+ made again as an empty store, as a scrub of a damaged file with rows already was in
318
+ 5.1: what the read did not get goes, the bytes after the damage and also the lines of the
319
+ damaged member before it that the codec never handed over (Python's gzip gives none of
320
+ a first member cut short, where `zcat` shows them) (see the damaged-stream notes above), and
321
+ `tsvz clear --x-header H --x-strict` (`clearTabularFile(verifyHeader=True)`) now clears
322
+ such a file, where 5.1 exited 1 with the codec's error.
323
+ 5.1 and 5.0 left such a file as it was, deletion lines included, so a writer that
324
+ appended keys, deleted them all and scrubbed, over and over, saw the file grow without
325
+ limit (205, 405, 605 bytes, ... for 20 keys a cycle); now it is the same size after
326
+ every scrub. A file with no byte (or a compressed one with only empty streams) is
327
+ still left alone, and so is a file whose strict scrub (`strict=True`, `--x-strict`) read
328
+ dropped a line for its width and found no row: that line is not a deletion, as 5.1 left
329
+ it. A scrub that finds the file changed since its read still reads
330
+ again and writes nothing if it keeps changing, as for a file with rows.
331
+ - **`get_file_obj('ab')` on a compressed store writes its trailer before the lock is let
332
+ go.** A program that calls it on a `.tsv.gz`, `.bz2`, `.xz` or `.zst` store and writes
333
+ through the file now has all of it, the gzip trailer and the bz2, xz and zstd data included,
334
+ in the file when `release_file_obj()` unlocks it. 5.1 and earlier wrote them after the
335
+ unlock, where another writer could append in between and corrupt the stream.
336
+ - **A `TSVZed` no longer lets a file grow with every session of sets and deletes.** It
337
+ rewrites a `.tsv`-family file at open when the file holds a key's row and the deletion
338
+ of it, as 3.39 did only for a key with two values, so a program that sets keys and
339
+ deletes them in each session (default settings, or `rewrite_interval`) keeps the file
340
+ the same size (205, 405, 605 bytes, ... for 20 keys a session before). Every session
341
+ that opens such a file now rewrites it once. With `rewrite_on_load=False` the open
342
+ does not, and the next exit or periodic rewrite does it (`rewrite_on_exit=True`,
343
+ `rewrite_interval`). What such a rewrite does, it now does at a plain open:
344
+ - it cuts a row wider than the header, and the cut stays
345
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)):
346
+ `id\tv\na\t1\t2\nb\t1\nb\t\n` becomes `id\tv\na\t1 \n`, and
347
+ `id\tv\na\t\t7\nb\t1\nb\t\n` becomes `id\tv\na\t \n`, the 7 lost;
348
+ - it writes over the file in place and pads with spaces (`a\t1 ` above): TSVZ readers
349
+ strip them, tools that read the raw bytes (`awk`, `cut`) see them;
350
+ - on a file it cannot write (read-only), it prints `Failed to open file … Permission
351
+ denied` twice and leaves the file as it is, as 5.1 did for a key with two values.
352
+ - **`--x-header H` makes line 1 the header for every `tsvz` operation.** On a
353
+ `.tsv`-family file whose line 1 is H (an exact match: the same cells, each without the
354
+ whitespace around it, a trailing empty cell included), line 1 is the header: `keys`,
355
+ `len`, `read`, `get`, `has`, `pop`, `popitem` and `delete` never list, count, print,
356
+ pop or delete it, and `scrub` and `clear` keep it. When line 1 differs, the file has no
357
+ header and line 1 is a row, with 3.39's "Header mismatch" warning, for the reads and
358
+ the scrub alike, so a scrub keeps the rows a read shows (but for the column-width cases
359
+ below), and in such a file it never writes a row that is H as line 1: it puts an empty
360
+ line first (`a\t1\nid\tv\na\t\n` becomes `\nid\tv\n`), so the reads still take that row
361
+ for a row. With `--x-strict` a line 1 that is not H is an error (exit 1, nothing
362
+ written) for every operation, through a handler too. A missing or empty file gets H
363
+ first at its first write, as in 5.1. A `tsvz serve STORE --x-header H` handler applies
364
+ the rule to every request it answers, routed `tsvz` commands included; with
365
+ `--x-strict` it is strict for them as above (5.1 ignored `--x-strict` at `serve`): such
366
+ a handler refuses to start on a file whose line 1 is not H, its routed `.tsv` reads and
367
+ scrubs drop lines of another column count, and its `.tsvz` reads are strict, as for a
368
+ direct `--x-strict` command. The library's own header check (`header=` to
369
+ `readTabularFile`, `appendTabularFile`, `clearTabularFile`, `scrubTabularFile`,
370
+ `TSVZed`) is unchanged, 3.39's containment check. The CLI's writes check line 1 more
371
+ strictly than 5.1's did: 5.1's `set`, `append`, `delete` and `clear` with `--x-header`
372
+ silently took a line 1 that merely held H's cells (`id\tvalue` for `id\tv`) for the
373
+ header and fitted rows to H's width; now they warn and write the row at its own width,
374
+ and with `--x-strict` exit 1. A rewrite without `--x-header` (a scrub without it,
375
+ `scrubTabularFile`, a `TSVZed` rewrite) can still put a row equal to H on line 1 of a
376
+ header-less file
377
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
378
+ In 5.1 only `set`, `append`, `delete` and `clear` used `--x-header`: the reads and
379
+ `scrub` took line 1 as a row (`tsvz keys`
380
+ printed `id a b` for two keys, `len` said 3, `popitem first` popped the header), and
381
+ so did every request to a handler. Without `--x-header` nothing changed: line 1 is a
382
+ row, as in 5.1 and 3.39. In a `.tsvz` store `pop`, `popitem` and `setdefault` now check
383
+ `--x-header` as `set` does; nothing else changed there.
384
+ - **Known, mostly as in 5.1: a scrub, a rewrite or a read takes one column count for every row,
385
+ and can lose cells or keys.** A scrub given defaults narrower than the rows, a row wider
386
+ than the header, and a `.tsv` that begins with a bare `#_defaults_#` line (read without
387
+ defaults) are examples; the entry lists each, and which is new in 5.2
388
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)).
389
+ A strict scrub of a file whose read dropped a line for its width and found no row leaves it,
390
+ so it grows again over append, delete-all and scrub cycles
391
+ ([limitations](docs/limitations.md#a-strict-scrub-leaves-a-file-whose-read-dropped-a-line-for-its-width)).
392
+ Both wait for the column-width design after 5.2.
393
+
394
+ ## Upgrading to 5.1 from 5.0
395
+
396
+ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
397
+ 5.1 writes. Most of the changes are in scrub and clear.
398
+
399
+ - **Scrub and clear of a `.tsv` write over the file.** `scrubTabularFile`,
400
+ `clearTabularFile`, `tsvz scrub` and `tsvz clear` (on the command line or through a
401
+ `tsvz serve` handler), `TSVZClient.clear()` and `TSVZedLite.clear_file()` of a
402
+ `.tsv`-family file write the new content over the file and then cut it, as
403
+ `TSVZed`'s rewrites do, so a reader that takes no lock never meets the file emptied.
404
+ They take the file's lock, so they wait while another writer holds it. 5.0 emptied
405
+ the file first, with no lock (`TSVZedLite` truncated its own descriptor).
406
+ - **A scrub keeps another process's rows.** A scrub of a `.tsv` writes only if the
407
+ file is still what its read saw. Otherwise it reads again, up to three times, and if
408
+ the file kept changing it writes nothing and warns: `tsvz scrub` exits 1 (`scrub
409
+ refused; nothing was written`), as for a `.tsvz` (C6), and a handler answers refused.
410
+ `scrubTabularFile` still returns the rows it read. 5.0 wrote over a row appended
411
+ after its read.
412
+ - **`scrubTabularFile(lastLineOnly=True)` is a read.** It returns the last row and
413
+ writes nothing, as for a `.tsvz`. 5.0 and 3.39 wrote that row back as deletion lines
414
+ (`id\tv\na\t\n3\t\n`), and the store was gone.
415
+ - **A scrub keeps the file's `#_defaults_#` lines.** The defaults in effect at the end
416
+ of the read are written after the rows: the last non-empty defaults line, its empty
417
+ cells filled from the line before it as a reader fills them. An empty defaults line
418
+ that was read (it turns defaults off) is written before the rows, padded to their
419
+ width. Every row reads as it did to a reader given the scrub's defaults and
420
+ `strict`. Defaults a caller passes still fill the rows' empty cells, as in 5.0, but
421
+ are not written as a defaults line, and the caller's list is not changed. 5.0 and
422
+ 3.39 dropped the lines, so rows appended later got no defaults.
423
+ - **The header stays on line 1.** A scrub given no header (`tsvz scrub` of a `.tsv`,
424
+ `scrubTabularFile(path)`) and a `TSVZed` given none, of a file
425
+ that has a header line, keep that line first, with a `TSVZed`'s defaults line right
426
+ after it. 5.0 and 3.39 put an empty line in front of it, so a reader given the header
427
+ read the header as a row. A header-less file keeps the empty first line it begins
428
+ with, and new files and clears given no header still get one, as in 5.0.
429
+ - **Interrupts.** A scrub or clear of either dialect keeps a copy of only the bytes it
430
+ overwrites, to put back if it is interrupted before the file is cut to its new
431
+ length; once the cut is made the rewrite is done and the new content stays. 5.0 kept
432
+ a copy of the whole `.tsvz` part and put it all back. (A scrub still reads the whole
433
+ store, and a `.tsvz` clear reads it too, to keep its markers.)
434
+ - **A compressed store cleared to nothing is one empty stream.** A `.tsvz.gz` (or
435
+ `.bz2`, `.xz`, `.zst`) cleared or scrubbed to nothing holds one empty compressed
436
+ stream, which `bzcat`, `xzcat` and Python's `bz2` and `lzma` can read; 5.0 left 0
437
+ bytes.
438
+ - **A header-checking clear of an empty file writes the header it was given.** A clear
439
+ that checks the header (`verifyHeader=True`, `tsvz clear --x-header H --x-strict`) of a
440
+ 0-byte `.tsv`, or of a compressed file with no byte or only empty streams (another
441
+ tool's), takes it for an empty store with that header, as every reader does (L9), and
442
+ writes H without a "Header mismatch" warning. 5.0 wrote an empty line and warned of a
443
+ mismatch, so a strict `TSVZed` given H then refused the file, and raised on a 0-byte
444
+ `.bz2`, `.xz` or `.zst`.
445
+ - **`tsvz --help`** says what `clear` keeps: a `.tsvz` its header and markers, a
446
+ `.tsv` the `--x-header` it is given. (With `--x-strict` too, a `.tsv` clear keeps the
447
+ file's own header line.)
448
+ `TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
449
+ a program asks for.
450
+ - **Known in 5.1, fixed in 5.2: a scrub of a `.tsv` whose rows were all deleted did not
451
+ compact it** when the scrub was given the file's header (`scrubTabularFile(path, header=H)`)
452
+ or the file had no header line: it left the file as it was, deletion lines included;
453
+ reads were right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file read the header
454
+ line as a row, so it did compact it.)
455
+ - **Known: a scrub run with other defaults than the store's program can change how
456
+ early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
457
+ cells before any defaults line, the reset line the scrub writes before the rows keeps
458
+ those cells empty for a program that opens the store with its own defaults; run the
459
+ scrub with the defaults that program uses
460
+ ([limitations](docs/limitations.md#a-scrub-run-with-other-defaults-than-the-stores-program-can-change-how-early-rows-read)).
461
+ - **Running programs keep 5.0** until they are restarted: long-running programs that
462
+ import TSVZ, and running `tsvz serve` handlers.
463
+
272
464
  ## Upgrading to 5.0 from 4.9
273
465
 
274
466
  - **Writes refuse text they cannot encode.** A key, value, header, defaults line or
275
467
  marker the file's encoding cannot hold (UTF-8 for `.tsvz`; `encoding=` for a
276
468
  `.tsv`), such as the lone surrogate `os.fsdecode` gives for a non-UTF-8 file name,
277
- raises `TSVZEncodeError` (a `ValueError`) and writes nothing; 4.9 stored `?`.
469
+ raises `TSVZEncodeError` (a `ValueError`) and writes nothing; 4.9 stored `?` (its
470
+ `tsvz` stored U+FFFD for a non-UTF-8 argument, and its `.tsvz` file functions raised
471
+ `UnicodeEncodeError`).
278
472
  `errors='replace'` brings `?` back. Under a non-UTF-8 `encoding=`, a key read as
279
473
  U+FFFD cannot be deleted or updated. `tsvz` exits 1 with the message. See Fault
280
474
  tolerance.
@@ -292,8 +486,7 @@ damaged file, as asked, and warns.
292
486
  merged into it (Fault tolerance).
293
487
  - **`.tsvz` stores** see another process's append made right after their load, a
294
488
  commit or a clear (4.9 missed it until the next change), keep the rows set while
295
- they reload, refuse `#_snapshot_#` (spec §19.10.2), and raise a strict header
296
- mismatch at the first read.
489
+ they reload, and raise a strict header mismatch at the first read.
297
490
  - **`.tsv` files:** an append to an existing empty file writes the header first, and
298
491
  reading one gives an empty store; see L8 to L14 for the rest.
299
492
  - **`tsvz`** runs every operation through the engine `tsvz serve` uses, with the same
@@ -302,8 +495,15 @@ damaged file, as asked, and warns.
302
495
  file. The 3.39 warning that a file name does not end in `.tsv` is printed only
303
496
  when a command creates such a file, and a write that cannot be made reports the
304
497
  operating system's error.
305
- - **macOS and other systems that lock with `lockf`:** 4.9's close gate is gone, so
306
- a read in another thread of the process can drop a write's lock
498
+ - **Running programs keep 4.9.** Long-running programs that import TSVZ, and running
499
+ `tsvz serve` handlers, keep 4.9 until they are restarted. 5.0's `tsvz stop` cannot
500
+ stop a 4.9 handler of a `.tsv` (or other tab- or NUL-delimited) file: 4.9 wrote the
501
+ delimiter into the pointer file's `x-delimiter` as `<sep>` (a NUL as `<NUL>`), which
502
+ 5.0 decodes to the TAB or NUL itself and refuses as a control character. Send that
503
+ handler SIGTERM instead: its pid is on the `pid` line of its pointer file
504
+ (`STORE.serve`), and it finishes its writes and exits.
505
+ - **macOS and other systems that lock with `lockf`:** as in 4.9, a read in another
506
+ thread of the process can still drop a write's lock
307
507
  ([docs/limitations.md](docs/limitations.md)).
308
508
 
309
509
  ## Breaking changes from 3.39
@@ -326,6 +526,11 @@ damaged file, as asked, and warns.
326
526
  | L12 | A rewrite of a file whose header line has no newline writes the rows on lines of their own; 3.39 wrote the first one onto the header line. |
327
527
  | L13 | A compressed file is rewritten in members of at most 1 MiB, as appends already were: the bytes differ, the rows read are the same, and 3.39 reads them. |
328
528
  | L14 | `TSVZed` opening a compressed file in a codec this Python lacks (zstd before Python 3.14) raises `ImportError` instead of reading the compressed bytes as text. |
529
+ | L15 | `scrubTabularFile`, `clearTabularFile`, `tsvz scrub`, `tsvz clear` and `TSVZedLite.clear_file()` write the new content over the file and then cut it, under the file's lock; 3.39 emptied the file first, with no lock, so a reader could meet it empty. |
530
+ | L16 | A scrub writes only if the file is still what its read saw: it reads again up to three times, then writes nothing, with a warning (`tsvz scrub` exits 1, C6); 3.39 wrote over rows appended after its read. |
531
+ | L17 | `scrubTabularFile(lastLineOnly=True)` returns the last row and writes nothing; 3.39 wrote that row back as deletion lines, and the store was gone. |
532
+ | L18 | A scrub keeps the file's `#_defaults_#` lines (an empty one before the rows, the defaults in effect after them), so every row reads as it did; 3.39 dropped them, so rows appended later got no defaults. |
533
+ | L19 | A scrub given no header, and a `TSVZed` given none, keep a file's header line first; 3.39 put an empty line in front of it, and a reader given the header then read the header as a row. |
329
534
 
330
535
  ### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
331
536