chtypes 0.3.0__tar.gz → 0.3.1__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.
- {chtypes-0.3.0 → chtypes-0.3.1}/CHANGELOG.md +10 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/PKG-INFO +1 -1
- {chtypes-0.3.0 → chtypes-0.3.1}/pyproject.toml +1 -1
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/fetch.py +131 -54
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/registry.py +4 -5
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/results.py +2 -2
- chtypes-0.3.1/tests/test_csv_reader.py +445 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_errors.py +1 -1
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_fetch.py +154 -0
- chtypes-0.3.1/tests/test_formats_withnames.py +458 -0
- chtypes-0.3.1/tests/test_golden.py +262 -0
- chtypes-0.3.1/tests/test_quoting_boundaries.py +235 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/uv.lock +1 -1
- chtypes-0.3.0/tests/test_golden.py +0 -153
- {chtypes-0.3.0 → chtypes-0.3.1}/.gitignore +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/LICENSE +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/README.md +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/__init__.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/__main__.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/_document.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/_ed25519.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/_manifest.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/_native.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/_rawjson.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/discover.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/errors.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/py.typed +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/src/chtypes/transform.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/conftest.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_abi_revision.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_cli.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_discover_reconstruct.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_ed25519.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_lazy.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_parity.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_rawjson.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_registry.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_results_rules.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_rows_export_with.py +0 -0
- {chtypes-0.3.0 → chtypes-0.3.1}/tests/test_transform.py +0 -0
|
@@ -4,6 +4,16 @@ All notable changes to the python binding. The format is [Keep a Changelog](http
|
|
|
4
4
|
|
|
5
5
|
The four bindings in this repository are released together and give one answer, so an entry here has a counterpart in the other three.
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.1] — 2026-09-26
|
|
10
|
+
|
|
11
|
+
Speaks ABI revision 5, unchanged from 0.3.0: every artifact 0.3.0 loads, this release loads.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **The publish-window retry (`docs/guides/fetch.md` §3a) now also covers a release-level file whose hash disagrees with its `SHA256SUMS` row** — `sdk-goldens.json`, checked after the requested artifact has already installed, since it is best-effort and never blocks that install — and the retry budget widened from 3 attempts ~10s apart to 5 attempts with delays doubling from 4s (4/8/16/32s, ~60s of sleep, ~70s of wall time with network latency). The artifacts host's edge cache can hold a stale pairing of `SHA256SUMS`, its signature, `index.json` or `sdk-goldens.json` for up to its measured 60-second `Cache-Control: max-age`, wider than the original ~10s budget covered. Every retry now re-reads the WHOLE consistent set fresh — never one freshly re-fetched object checked against another attempt's stale one. A release-level file mismatch that never heals still leaves the golden tests skipping loudly, exactly as before, and never fails the fetch that asked for the artifact (issue #223).
|
|
16
|
+
|
|
7
17
|
## [0.3.0] — 2026-09-22
|
|
8
18
|
|
|
9
19
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: chtypes
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI
|
|
5
5
|
Project-URL: Homepage, https://github.com/wave-rf/chtypes
|
|
6
6
|
Project-URL: Repository, https://github.com/wave-rf/chtypes
|
|
@@ -40,7 +40,7 @@ import warnings
|
|
|
40
40
|
from collections.abc import Callable, Iterable, Sequence
|
|
41
41
|
from dataclasses import dataclass
|
|
42
42
|
from pathlib import Path, PurePosixPath
|
|
43
|
-
from typing import Any, Final
|
|
43
|
+
from typing import Any, Final, TypeVar
|
|
44
44
|
|
|
45
45
|
from ._ed25519 import verify as _ed25519_verify
|
|
46
46
|
from ._manifest import (
|
|
@@ -96,9 +96,13 @@ ENV_AUTOFETCH: Final = "CHTYPES_AUTOFETCH"
|
|
|
96
96
|
ENV_REGISTRY: Final = "CHTYPES_REGISTRY"
|
|
97
97
|
|
|
98
98
|
#: How many times `Fetcher.release` reads an ``http(s)`` release before giving
|
|
99
|
-
#: up, and the
|
|
100
|
-
#:
|
|
101
|
-
|
|
99
|
+
#: up, and the BASE delay before the first retry — each later retry doubles it,
|
|
100
|
+
#: so the default 5 attempts sleep 4+8+16+32 = 60s (~70s wall with network
|
|
101
|
+
#: time). That is chosen to outlast the artifacts host's edge cache (observed
|
|
102
|
+
#: ``Cache-Control: max-age=60`` on the mutable release objects, so a stale
|
|
103
|
+
#: pairing of any of them can persist up to 60s), while a genuine few-second
|
|
104
|
+
#: mid-publish window still clears on the second attempt.
|
|
105
|
+
RELEASE_LOAD_ATTEMPTS: Final = 5
|
|
102
106
|
RELEASE_RETRY_DELAY: Final = 4
|
|
103
107
|
|
|
104
108
|
#: The served golden set: a release-level file, and a row in the signed
|
|
@@ -128,6 +132,8 @@ DEFAULT_LOCK_FILE: Final = "chtypes.lock"
|
|
|
128
132
|
_USER_AGENT = "chtypes-python (+https://github.com/wave-rf/chtypes)"
|
|
129
133
|
_HTTP_ATTEMPTS = 3
|
|
130
134
|
_HTTP_TIMEOUT = 60.0
|
|
135
|
+
|
|
136
|
+
_T = TypeVar("_T")
|
|
131
137
|
_CHUNK = 1 << 20
|
|
132
138
|
|
|
133
139
|
log = logging.getLogger("chtypes.fetch")
|
|
@@ -752,45 +758,86 @@ class Fetcher:
|
|
|
752
758
|
|
|
753
759
|
# ------------------------------------------------------------ the release
|
|
754
760
|
|
|
755
|
-
def
|
|
756
|
-
"""
|
|
761
|
+
def _retry_publish_window(self, load: Callable[[], _T]) -> _T:
|
|
762
|
+
"""Runs ``load`` through the publish-window retry: on every attempt but
|
|
763
|
+
the last, an `ArtifactUntrustedError` or `ArtifactCorruptError` is
|
|
764
|
+
logged and slept off before trying again; any other exception, or the
|
|
765
|
+
last attempt's, propagates immediately.
|
|
757
766
|
|
|
758
767
|
A publish into the rolling release is three objects — ``SHA256SUMS``,
|
|
759
|
-
``SHA256SUMS.sig``, ``index.json`` —
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
768
|
+
``SHA256SUMS.sig``, ``index.json`` — plus, when ``load`` also reads it,
|
|
769
|
+
the served golden set — and object storage cannot swap them atomically.
|
|
770
|
+
The artifacts host's edge cache widens that window from "between two
|
|
771
|
+
uploads" to "as long as any one object can still be served stale from
|
|
772
|
+
cache" (observed ``Cache-Control: max-age=60`` on all four).
|
|
773
|
+
|
|
774
|
+
Three symptoms of reading inside it are retried: a signature under no
|
|
775
|
+
trusted key, an index that disagrees with the sums, and a release-level
|
|
776
|
+
file whose hash disagrees with its own SHA256SUMS row (or that the sums
|
|
777
|
+
list but the source does not yet serve). Each retry calls ``load``
|
|
778
|
+
again from scratch — the whole consistent set is re-read together,
|
|
779
|
+
never one freshly re-fetched object checked against another attempt's
|
|
780
|
+
stale one. Nothing else is retried, and neither are these three once
|
|
781
|
+
the attempts run out: the same exception surfaces, with the same exit
|
|
782
|
+
code, as it did before. A tarball whose hash is wrong is never retried;
|
|
783
|
+
that is the release lying about a byte, not a half-finished upload.
|
|
771
784
|
|
|
772
785
|
Only an ``http(s)`` source can be mid-publish, so a ``file://`` or
|
|
773
|
-
directory source
|
|
774
|
-
if self._release is not None:
|
|
775
|
-
return self._release
|
|
786
|
+
directory source runs ``load`` exactly once."""
|
|
776
787
|
attempts = RELEASE_LOAD_ATTEMPTS if self.source.kind == "http" else 1
|
|
788
|
+
delay = RELEASE_RETRY_DELAY
|
|
777
789
|
for attempt in range(1, attempts + 1):
|
|
778
790
|
try:
|
|
779
|
-
|
|
780
|
-
return self._release
|
|
791
|
+
return load()
|
|
781
792
|
except (ArtifactUntrustedError, ArtifactCorruptError) as exc:
|
|
782
793
|
if attempt >= attempts:
|
|
783
794
|
raise
|
|
784
795
|
self._say(
|
|
785
796
|
f"{exc} (attempt {attempt}/{attempts}) — this is what a release being "
|
|
786
|
-
f"published looks like from outside; retrying in {
|
|
797
|
+
f"published looks like from outside; retrying in {delay}s"
|
|
787
798
|
)
|
|
788
|
-
time.sleep(
|
|
799
|
+
time.sleep(delay)
|
|
800
|
+
delay *= 2
|
|
789
801
|
raise AssertionError("unreachable") # pragma: no cover
|
|
790
802
|
|
|
791
|
-
def
|
|
792
|
-
"""
|
|
793
|
-
|
|
803
|
+
def release(self) -> Release:
|
|
804
|
+
"""Steps 0–1, retried through a publish window (`_retry_publish_window`).
|
|
805
|
+
Read once per `Fetcher`: a second call returns the memoized result."""
|
|
806
|
+
if self._release is not None:
|
|
807
|
+
return self._release
|
|
808
|
+
self._release, _, _ = self._retry_publish_window(lambda: self._load_release())
|
|
809
|
+
return self._release
|
|
810
|
+
|
|
811
|
+
def _read_goldens_or_raise(self, want: str) -> bytes:
|
|
812
|
+
"""One read of ``sdk-goldens.json``, verified against ``want`` (its row
|
|
813
|
+
in an already-verified SHA256SUMS). Raises `ArtifactCorruptError` for
|
|
814
|
+
either half of the new window symptom — a hash mismatch, or the sums
|
|
815
|
+
listing it while the source does not (yet) serve it — and
|
|
816
|
+
`SourceUnreachableError` for a plain I/O fault."""
|
|
817
|
+
src = str(self.source)
|
|
818
|
+
try:
|
|
819
|
+
blob = self.source.read(GOLDENS_ASSET)
|
|
820
|
+
except OSError as exc:
|
|
821
|
+
raise SourceUnreachableError(
|
|
822
|
+
f"chtypes: could not read {GOLDENS_ASSET} from {src}: {exc}"
|
|
823
|
+
) from exc
|
|
824
|
+
if blob is None:
|
|
825
|
+
raise ArtifactCorruptError(
|
|
826
|
+
f"chtypes: SHA256SUMS lists {GOLDENS_ASSET} but {src} does not serve it "
|
|
827
|
+
f"— the release disagrees with itself; not installing it"
|
|
828
|
+
)
|
|
829
|
+
got = hashlib.sha256(blob).hexdigest()
|
|
830
|
+
if got != want:
|
|
831
|
+
raise ArtifactCorruptError(
|
|
832
|
+
f"chtypes: {GOLDENS_ASSET} hashes to {got} but the signed SHA256SUMS "
|
|
833
|
+
f"says {want} — not installing it"
|
|
834
|
+
)
|
|
835
|
+
return blob
|
|
836
|
+
|
|
837
|
+
def _load_release(self, *, want_goldens: bool = False) -> tuple[Release, bytes | None, bool]:
|
|
838
|
+
"""One read of the release's three small files, plus — when
|
|
839
|
+
``want_goldens`` — the served golden set, as part of the SAME read.
|
|
840
|
+
Never memoizes: the caller does that, and only on success."""
|
|
794
841
|
if self.offline:
|
|
795
842
|
raise SourceUnreachableError(
|
|
796
843
|
f"chtypes: offline — not reading the release at {self.source}"
|
|
@@ -826,11 +873,12 @@ class Fetcher:
|
|
|
826
873
|
if index_raw is None:
|
|
827
874
|
raise SourceUnreachableError(f"chtypes: no index.json at {src} — not a chtypes release")
|
|
828
875
|
entries, tag, license_, license_url = _parse_index(index_raw, src)
|
|
876
|
+
sums = _parse_sums(sums_raw)
|
|
829
877
|
release = Release(
|
|
830
878
|
source=src,
|
|
831
879
|
tag=tag or self.tag,
|
|
832
880
|
entries=tuple(entries),
|
|
833
|
-
sums=
|
|
881
|
+
sums=sums,
|
|
834
882
|
signed_by=signed_by,
|
|
835
883
|
license=license_,
|
|
836
884
|
license_url=license_url,
|
|
@@ -845,12 +893,25 @@ class Fetcher:
|
|
|
845
893
|
f"chtypes: index.json says {entry.file} is {entry.sha256} but SHA256SUMS "
|
|
846
894
|
f"says {listed} — the release disagrees with itself; not installing it"
|
|
847
895
|
)
|
|
896
|
+
# The served golden set, read as part of the SAME set as the three
|
|
897
|
+
# objects above: it is a row in SHA256SUMS exactly like a tarball, so a
|
|
898
|
+
# stale pairing of it against the sums (or against nothing, if the sums
|
|
899
|
+
# row landed before the file itself is visible) is the same publish
|
|
900
|
+
# window, not a separate failure mode. A release that never lists it
|
|
901
|
+
# simply predates the served set — not a window symptom, so not raised.
|
|
902
|
+
goldens_blob: bytes | None = None
|
|
903
|
+
goldens_listed = False
|
|
904
|
+
if want_goldens:
|
|
905
|
+
want = sums.get(GOLDENS_ASSET)
|
|
906
|
+
goldens_listed = want is not None
|
|
907
|
+
if want is not None:
|
|
908
|
+
goldens_blob = self._read_goldens_or_raise(want)
|
|
848
909
|
if license_:
|
|
849
910
|
self._say(
|
|
850
911
|
f"artifacts are licensed under {license_}{' ' + license_url if license_url else ''}"
|
|
851
912
|
f" — LICENSE and NOTICE ship beside them"
|
|
852
913
|
)
|
|
853
|
-
return release
|
|
914
|
+
return release, goldens_blob, goldens_listed
|
|
854
915
|
|
|
855
916
|
# --------------------------------------------------------------- ensure
|
|
856
917
|
|
|
@@ -883,37 +944,53 @@ class Fetcher:
|
|
|
883
944
|
the same chain and lands beside the artifacts, where every binding's
|
|
884
945
|
golden test reads it offline.
|
|
885
946
|
|
|
947
|
+
The first look reuses `release`'s already-verified sums — cheap, and
|
|
948
|
+
right on the overwhelmingly common case that nothing is mid-publish.
|
|
949
|
+
Only a disagreement (a hash mismatch, or the sums listing it while the
|
|
950
|
+
source does not yet serve it) is retried, and a retry re-reads the
|
|
951
|
+
WHOLE consistent set fresh — SHA256SUMS, its signature, index.json and
|
|
952
|
+
the golden set together — never the golden set alone against the first
|
|
953
|
+
look's by-then possibly-stale sums.
|
|
954
|
+
|
|
886
955
|
Never raises: a release with no such row simply predates the served set,
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
956
|
+
a mismatch that never heals leaves the golden tests skipping loudly —
|
|
957
|
+
which is their job when there is nothing trustworthy to read — and a
|
|
958
|
+
set that cannot be written does the same. What it will not do is
|
|
959
|
+
install bytes the signed ``SHA256SUMS`` does not describe.
|
|
890
960
|
"""
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
961
|
+
attempts = RELEASE_LOAD_ATTEMPTS if self.source.kind == "http" else 1
|
|
962
|
+
delay = RELEASE_RETRY_DELAY
|
|
963
|
+
sums = self.release().sums
|
|
964
|
+
listed = False
|
|
965
|
+
blob: bytes | None = None
|
|
966
|
+
for attempt in range(1, attempts + 1):
|
|
967
|
+
try:
|
|
968
|
+
if attempt == 1:
|
|
969
|
+
want = sums.get(GOLDENS_ASSET)
|
|
970
|
+
listed = want is not None
|
|
971
|
+
blob = self._read_goldens_or_raise(want) if want is not None else None
|
|
972
|
+
else:
|
|
973
|
+
# A retry: the earlier `sums` may itself be stale by now, so
|
|
974
|
+
# this re-reads everything, not just the golden set.
|
|
975
|
+
_, blob, listed = self._load_release(want_goldens=True)
|
|
976
|
+
break
|
|
977
|
+
except (ArtifactUntrustedError, ArtifactCorruptError, SourceUnreachableError) as exc:
|
|
978
|
+
if attempt >= attempts:
|
|
979
|
+
self._say(f"{exc} — the golden tests will skip")
|
|
980
|
+
return None
|
|
981
|
+
self._say(
|
|
982
|
+
f"{exc} (attempt {attempt}/{attempts}) — this is what a release being "
|
|
983
|
+
f"published looks like from outside; retrying in {delay}s"
|
|
984
|
+
)
|
|
985
|
+
time.sleep(delay)
|
|
986
|
+
delay *= 2
|
|
987
|
+
if not listed:
|
|
894
988
|
self._say(
|
|
895
989
|
f"this release does not publish {GOLDENS_ASSET} "
|
|
896
990
|
f"(the SDKs' golden tests will skip until it does)"
|
|
897
991
|
)
|
|
898
992
|
return None
|
|
899
|
-
|
|
900
|
-
blob = self.source.read(GOLDENS_ASSET)
|
|
901
|
-
except OSError as exc:
|
|
902
|
-
self._say(f"could not read {GOLDENS_ASSET}: {exc} — the golden tests will skip")
|
|
903
|
-
return None
|
|
904
|
-
if blob is None:
|
|
905
|
-
self._say(
|
|
906
|
-
f"SHA256SUMS lists {GOLDENS_ASSET} but {self.source} does not serve it "
|
|
907
|
-
f"— NOT installing it"
|
|
908
|
-
)
|
|
909
|
-
return None
|
|
910
|
-
got = hashlib.sha256(blob).hexdigest()
|
|
911
|
-
if got != want:
|
|
912
|
-
self._say(
|
|
913
|
-
f"NOT installing {GOLDENS_ASSET}: it hashes to {got} but the signed "
|
|
914
|
-
f"SHA256SUMS says {want}"
|
|
915
|
-
)
|
|
916
|
-
return None
|
|
993
|
+
assert blob is not None # listed and no exception implies bytes were read
|
|
917
994
|
try:
|
|
918
995
|
self.dest.mkdir(parents=True, exist_ok=True)
|
|
919
996
|
path = self.dest / GOLDENS_ASSET
|
|
@@ -166,10 +166,9 @@ def encode_columns(columns: Sequence[str] | None) -> str | None:
|
|
|
166
166
|
|
|
167
167
|
`None` and an EMPTY sequence are the SAME input and both encode to
|
|
168
168
|
`None`, never to `"[]"`: `INSERT INTO t () FORMAT X` is code 62
|
|
169
|
-
`SYNTAX_ERROR` on every served line (measured 2026-09-15
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
accepted.
|
|
169
|
+
`SYNTAX_ERROR` on every served line (measured 2026-09-15 against explicit
|
|
170
|
+
column lists), so an implementation that renders an empty list into
|
|
171
|
+
parentheses sends a statement no ClickHouse has ever accepted.
|
|
173
172
|
|
|
174
173
|
No local validation of names — an unknown column, an `ALIAS` column and a
|
|
175
174
|
repeated name are all refused by the SERVER, with its own codes (16, 16,
|
|
@@ -1020,7 +1019,7 @@ class Library:
|
|
|
1020
1019
|
`deterministic_in_query`, `server_constant`, `stateful`,
|
|
1021
1020
|
`resolver_error_code`) — ClickHouse's own answers off this build's own
|
|
1022
1021
|
registry, the input to the statelessness gate
|
|
1023
|
-
(
|
|
1022
|
+
(the build's function-flags generator).
|
|
1024
1023
|
|
|
1025
1024
|
Part of the three-question introspection surface every SDK exposes
|
|
1026
1025
|
(docs/reference/bindings.md §Introspection). Raises `UnsupportedError` when the
|
|
@@ -235,7 +235,7 @@ class Outcome(StrEnum):
|
|
|
235
235
|
|
|
236
236
|
ACCEPTED = "accepted"
|
|
237
237
|
REJECTED = "rejected"
|
|
238
|
-
# The insert returns rc=0 and every later SELECT fails
|
|
238
|
+
# The insert returns rc=0 and every later SELECT fails (usually 691). This
|
|
239
239
|
# is an ACCEPTED insert, and reporting it as a rejection is wrong.
|
|
240
240
|
ACCEPTED_POISONED = "accepted_poisoned"
|
|
241
241
|
# This build declines to answer. Never scored as agreement.
|
|
@@ -500,7 +500,7 @@ class RowResult:
|
|
|
500
500
|
|
|
501
501
|
@property
|
|
502
502
|
def poisoned(self) -> bool:
|
|
503
|
-
"""Whether ClickHouse stored a value it cannot read back (
|
|
503
|
+
"""Whether ClickHouse stored a value it cannot read back (usually 691).
|
|
504
504
|
|
|
505
505
|
A poisoned row IS an accepted insert; every later SELECT on it fails.
|
|
506
506
|
"""
|