Skip to content

Feat/sample overwrite ipv4 - #34

Merged
9bany merged 2 commits into
masterfrom
feat/sample-overwrite-ipv4
Sep 9, 2026
Merged

9bany merged 2 commits into
masterfrom
feat/sample-overwrite-ipv4

Conversation

@9bany

@9bany 9bany commented Sep 8, 2026

Copy link
Copy Markdown
Member

No description provided.

Three of the P2 rows closed last time as named refusals. Two of those refusals
were mine to make rather than the architecture's, and one of them rested on a
claim that was wrong.

`SAMPLE` was refused because a sample over roaring containers looked like either
a decode pass or a block of write-order ids wearing a sample's name. That is
true of sampling *blocks*. It is not true of sampling the id's **low bits**: a
container holds 65,536 consecutive ids, so "every tenth id" is a pattern inside
each container, built once per phase and intersected. One AND per container, no
record decoded, and spread evenly through every window of time rather than
picking some of them whole. The stride need not divide the container either -
a container beginning part-way through the pattern begins at a phase, and a
phase is arithmetic.

So `SELECT ... FROM t SAMPLE 10` is answered, and `SAMPLE 1/10` and `SAMPLE 0.1`
with it. The fraction is read through the decimal it already parsed as - units
and a scale, never a float - so `0.1` is exactly one in ten. What stays refused
is a fraction with no whole stride: `0.3` would be one in 3.33, and answering it
as one in three is a tenth more data under the name that was asked for. So is
`SAMPLE BY`, which names the column a sample is drawn on, where the unit here is
the record id.

`Rows::Sample` is a row-set variant, not a `Plan` variant: it sits inside the
row set a plan already reads, so `big-cluster/src/merge.rs` is untouched and
there is no new merge arm. It costs a PQL call (`Sample(<bitmap>, stride=n)`)
and one wire tag.

`INSERT OVERWRITE TABLE t (a) SELECT ...` empties and then writes. It is still
two operations with nothing wrapping them, and that is said rather than hidden -
a crash between them leaves the table empty, and the atomic replacement is
`EXCHANGE TABLES`. What it buys over writing the two statements is the order:
the source query is read whole and merged before anything is cleared, so a
`SELECT` that turns out to be unreadable leaves the table alone. It demands
`DELETE` as well as `INSERT`. `PARTITION (...)` keeps the refusal, narrowed to
it: a shard is computed from the record id and there is nothing to aim at.

`IPv4` is read as the `UINT(32)` an address is. The reason it was refused - that
a type name has to be a real `ColumnKind` to survive the round-trip gate - was
simply wrong: the gate compares `Column` structs, not spellings, and this
dialect already reads `TIMESTAMP` as `DATETIME`, `BIGINT` as `UINT(64)` and
`TEXT` as `SET`. `UUID` and `IPv6` stay refused, and the difference is what
would be lost: both are 128 bits where a bit-sliced value stops at 64, so the
only column they could land in is keyed, which answers `=` but not `<`.

`Refused::Sample` and `Refused::Overwrite` narrow rather than disappear;
`Refused::ALL` is unchanged at 97.
The last of the P2 rows that was refused on my judgement rather than the
architecture's. The refusal said `MUTEX` is the storage an enum wants and that
the missing half - a declared list of members - had nowhere to live. The first
half was right and is what makes this cheap; the second was a gap in the catalog
rather than a fact about it.

A member list does not fit in a fixed-width catalog record, so it follows the
shape `SAVED_QUERY_TEXT` already established: a new record kind carrying NUL-
joined chunks under the field's table and field id, with the total length in the
`FIELD` header's free word so a torn set is detectable. `FieldDef` grows a
`members` list; nothing below the catalog changes at all. No new `FieldKind`, no
`place()` behaviour, no storage format for a value - the field is a mutex and
stores an interned row id exactly as it did.

What the list buys is the two things a mutex cannot say:

- A value outside it is refused on the way in, at `fact::from_literal` and
  `fact::from_text` - both write funnels, so the declaration holds for a SQL
  insert and for the import route alike. The message names the members, which
  is the only question that error raises; the code stays the generic one for a
  value a field cannot take, because what to do about it is the same.
- `SHOW CREATE TABLE` answers `ENUM('new', 'paid')` rather than `MUTEX`. A
  mutex and an enum are one `FieldKind`, so what tells them apart there is
  whether a member list was declared.

A field that declares none is a plain mutex and takes anything. That is what
says the check belongs to the declaration rather than to the kind, and it is
also what every field written before this reads back as: an older `FIELD` record
carries zero in the word that now holds the blob length and is followed by no
member records, which is exactly the shape a field with no members has today.
`a_field_written_without_members_reads_back_as_a_plain_mutex` pins that.

`Enum8` and `Enum16` are the same declaration - the width names how many members
fit and the dictionary has no such ceiling. `Refused::EnumType` narrows to the
two shapes that stay refused: `'a' = 1`, whose number promises which integer the
value is stored as when the dictionary assigns that, and a repeated member,
which is unreachable and would make `SHOW CREATE TABLE` disagree with what was
typed.

`Declared` replaces five positional arguments to `Db::declare`, which is where
clippy drew the line and where a reader would have too.
@9bany
9bany merged commit 1acedd9 into master Sep 9, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant