Mutation that changes a parquet_table's ROW SET: %filter_rows, %sort_by, %top_n,
%delete_rows, %truncate, %append and %append_null_rows.
Everything in this file detaches the table when it actually changes the row set (F-mut-7),
and that is why it is a separate file from parquet_tables_mutate.f90. Once the row set has
changed, a column still sitting in the file can never be lined up with the columns already in
memory again -- so rather than hand back silently misaligned data later, the table records that
it has left its file behind and every later read from that file is a named error.
A call that changes nothing changes nothing at all -- it does not touch a column, does not
invalidate a %col pointer, and does not detach. Detaching costs the caller their file, so it
is only ever paid for when something actually required it. Five calls reach this file with
nothing to do, all decided by the caller's own arguments (%truncate(n) with n >= %nrows(),
%filter_rows with an all-.true. mask, %delete_rows with no indices, %append of a
zero-row table, %append_null_rows(0)), plus one decided by the data (%sort_by whose
permutation moves no row, which includes every table of fewer than two rows). Each returns
early, AFTER its own validation -- a no-op still rejects a bad argument. %top_n with
n >= %nrows() reaches the same place by delegating to %sort_by, which is where its own
data-decided no-op comes from.
Two rules hold this together, and a new operation added here must follow both:
A column that has not been read is skipped, not an obstacle (table_mutable_column). That
is the approved policy: requiring every column to be resident first would force a lazy table to
read everything it has before it could drop a single row, which is exactly the memory cost
laziness and the slice regime exist to avoid. The skipped column is then unreadable for good --
detaching sees to that -- so a caller who wants it must %prefetch it BEFORE mutating, and the
detach guard is what says so if they did not.
%append is the one mutation a SHARED table permits, and the split between its two public
entry points and the private append_table_worker is what makes that safe: each entry point
takes the table's own lock exactly once and then calls the worker, and neither calls the other.
An OpenMP simple lock is not recursive, so a second acquisition on one thread deadlocks rather
than failing to build -- any future internal caller must come to the worker, never to
table_append_table. Everything else in this file is refused outright on a shared table
(table_check_not_shared), because it would pull storage out from under another thread.