!===========================================
! Author: Elmo Tempel (elmo.tempel@ut.ee)
!===========================================
!> Writer lifecycle (open/close/new_row_group/finish_row_group) and the
!> type-generic shared helpers every write-specifics child (parquet_write_numeric/
!> _string/_temporal) reaches by host association: schema/column-info lookups,
!> row-count and row-mask bookkeeping, output-name resolution, qc: integer-count
!> text formatting (shared by numeric and string qc: reports), and the
!> write_maml=.true. sidecar .maml.
submodule (parquet_core) parquet_write
    use iso_c_binding
    use iso_fortran_env, only: int32, int64, real32, real64
    use parquet_bindings
    use parquet_settings, only: parquet_valid_compressions, parquet_resolve_writer_compression, parquet_get_default_use_threads, &
        parquet_push_settings_to_cpp, &
        parquet_emit_warning, parquet_emit_error_context
    use parquet_temporal, only: parquet_date, parquet_time, parquet_timestamp
    implicit none
contains

    !> Every parquet_write_column variant calls this first: writer%handle is
    !> c_null_ptr until parquet_open_writer sets it, and every C++ entry point
    !> dereferences the handle immediately (see ConcurrencyGuard in
    !> parquet_wrapper.cpp) with no null check of its own -- calling in with an
    !> unopened writer previously crashed with an unhelpful SIGSEGV instead of
    !> a clean, diagnosable error.
    subroutine check_writer_open(writer)
        type(parquet_writer), intent(in) :: writer !! writer to check.
        if (.not. c_associated(writer%handle)) then
            error stop "parquet_write_column: writer has not been opened (call parquet_open_writer first)"
        end if
    end subroutine check_writer_open
    module procedure writer_lock_claim
        ! An unopened writer has no handle to guard. Leaving self%handle null here is what makes
        ! the finalizer below safe to run unconditionally.
        if (.not. c_associated(writer%handle)) return
        self%handle = writer%handle
        call parquet_writer_enter(self%handle)
    end procedure writer_lock_claim
    !> The name actually written to the parquet file/VOTable header for
    !> `col`: its output_name if set, else its (internal) name. Falling back
    !> to name defensively handles a parquet_column_type built without going
    !> through parquet_read_maml (output_name left unallocated).
    subroutine parquet_column_output_name(col, output_name)
        type(parquet_column_type), intent(in) :: col !! column whose output name is wanted.
        character(len=:), allocatable, intent(out) :: output_name !! col%output_name if set, else col%name.

        output_name = col%name
        if (allocated(col%output_name)) then
            if (len_trim(col%output_name) > 0) output_name = col%output_name
        end if
    end subroutine parquet_column_output_name
    !> Resolves the caller-facing (internal) column `name` used in a
    !> parquet_write_column call to the name that should actually be passed
    !> to the C++ append_* calls -- its col_map:-renamed output_name, or
    !> `name` itself if there's no schema (schema-less writer) or no match.
    subroutine parquet_resolve_output_name(writer, name, output_name)
        type(parquet_writer), intent(in) :: writer !! open writer.
        character(len=*), intent(in) :: name !! internal (caller-facing) column name.
        character(len=:), allocatable, intent(out) :: output_name !! name to actually pass to the C++ append_* calls.
        integer :: idx

        idx = parquet_get_enabled_column_index(writer, name)
        if (idx > 0) then
            call parquet_column_output_name(writer%enabled_columns(idx), output_name)
        else
            output_name = name
        end if
    end subroutine parquet_resolve_output_name
    module procedure parquet_get_enabled_column_index
        integer :: i

        parquet_get_enabled_column_index = 0
        if (.not. allocated(writer%enabled_columns)) return

        do i = 1, size(writer%enabled_columns)
            if (trim(writer%enabled_columns(i)%name) == trim(name)) then
                parquet_get_enabled_column_index = i
                return
            end if
        end do
    end procedure parquet_get_enabled_column_index
    module procedure parquet_get_defined_column_index
        integer :: i

        parquet_get_defined_column_index = 0

        do i = 1, size(writer%all_columns)
            if (trim(writer%all_columns(i)%name) == trim(name)) then
                parquet_get_defined_column_index = i
                return
            end if
        end do
    end procedure parquet_get_defined_column_index
    module procedure parquet_is_type_compatible
        character(len=:), allocatable :: schema_type, write_type

        schema_type = trim(actual_type)
        write_type = trim(expected_type)

        select case (schema_type)
        case ("boolean")
            parquet_is_type_compatible = write_type == "boolean" .or. write_type == "bool8"
        case ("float32", "float64")
            parquet_is_type_compatible = write_type == "float32" .or. write_type == "float64" .or. &
                                          write_type == "int32" .or. write_type == "int64"
        case ("int32", "int64")
            parquet_is_type_compatible = write_type == "int32" .or. write_type == "int64" .or. &
                                          write_type == "float32" .or. write_type == "float64"
        case default
            parquet_is_type_compatible = write_type == schema_type
        end select
    end procedure parquet_is_type_compatible
    !> The schema-declared data_type for `name` on a schema-enforced writer,
    !> or "" for a schema-less writer or an undefined column. Used by the
    !> parquet_append_as_schema_* family to decide whether a write call's own
    !> values need widening to match the schema's declared type.
    subroutine parquet_get_schema_type(writer, name, schema_type)
        type(parquet_writer), intent(in) :: writer !! writer to check.
        character(len=*), intent(in) :: name !! column name.
        character(len=:), allocatable, intent(out) :: schema_type !! schema-declared data_type, or "".
        integer :: idx

        schema_type = ""
        if (.not. writer%is_schema_enforced) return
        idx = parquet_get_defined_column_index(writer, name)
        if (idx == 0) return
        schema_type = trim(writer%all_columns(idx)%data_type)
    end subroutine parquet_get_schema_type
    !> "" for a schema-less writer with no output filename set either;
    !> otherwise " (file: X)", " (maml: Y)", or " (file: X, maml: Y)"
    !> depending on which of writer%filename/writer%maml_name are set
    !> (writer%filename is set by parquet_open_writer as soon as the writer
    !> is created, so it is virtually always present here; writer%maml_name
    !> is unset only for a schema assembled by hand without going through
    !> schema%init/parquet_schema(...), which always set it to
    !> "internal:<table>"). Appended to parquet_write_column's
    !> schema-mismatch and completeness errors so they're diagnosable
    !> without needing to know which parquet_open_writer call produced them.
    subroutine writer_context_suffix(writer, suffix)
        type(parquet_writer), intent(in) :: writer !! writer whose filename/maml_name is reported.
        character(len=:), allocatable, intent(out) :: suffix !! " (file: X, maml: Y)"-style suffix, or "".
        character(len=:), allocatable :: parts

        parts = ""
        if (allocated(writer%filename)) then
            if (len_trim(writer%filename) > 0) parts = "file: " // trim(writer%filename)
        end if
        if (allocated(writer%maml_name)) then
            if (len_trim(writer%maml_name) > 0) then
                if (len(parts) > 0) then
                    parts = parts // ", maml: " // trim(writer%maml_name)
                else
                    parts = "maml: " // trim(writer%maml_name) ! GCOVR_EXCL_LINE
                end if
            end if
        end if

        suffix = ""
        if (len(parts) > 0) suffix = " (" // parts // ")"
    end subroutine writer_context_suffix
    !> The type token `items(idx)` should be written to the file with, which is its own
    !> %datatype unless the caller has ALSO added a literal "<key>.datatype" entry of their own.
    !>
    !> A typed entry makes the writer synthesize a companion "<key>.datatype" key (see
    !> build_file_metadata, parquet_wrapper.cpp); a caller's explicit entry of that name would
    !> then be the second writer of it, and the file would carry two same-named entries free to
    !> disagree -- exactly the wrong-answer-with-no-abort failure a reader trusting the companion
    !> would inherit. So the explicit entry wins and the synthesized one is dropped, with a
    !> warning: refusing the whole write over a metadata naming clash would be disproportionate
    !> when the file is otherwise valid.
    !>
    !> Two consequences of resolving this here rather than in %add_metadata, both deliberate.
    !> The warning names the file/schema (per the error-context convention), which %add_metadata
    !> cannot -- but it therefore fires once per parquet_open_writer call, so a schema reused for
    !> several files warns once per file, and it is not silenced by that call's warn=.false.
    !> (which suppresses the duplicate-key warning at add time, a different check). And the key
    !> loses its VOTable type too, falling back to char/arraysize="*": the caller has taken over
    !> that key's type declaration, so the library declines to guess a second one.
    subroutine resolve_metadata_datatype(writer, items, idx, datatype)
        type(parquet_writer), intent(in) :: writer !! writer whose file/maml names the warning reports.
        type(parquet_metadata_entry), intent(in) :: items(:) !! every table metadata entry, scanned for a collision.
        integer, intent(in) :: idx !! 1-based index of the entry being written.
        character(len=:), allocatable, intent(out) :: datatype !! token to write, or "" for none.
        character(len=:), allocatable :: ctx
        integer :: j

        datatype = ""
        if (.not. allocated(items(idx)%datatype)) return
        if (len_trim(items(idx)%datatype) == 0) return
        datatype = trim(items(idx)%datatype)

        ! O(n^2) over table metadata entries, which is tens of entries at the sizes this is
        ! written for -- not worth a hash, and this runs once per file open.
        do j = 1, size(items)
            if (trim(items(j)%key) /= trim(items(idx)%key) // ".datatype") cycle
            call writer_context_suffix(writer, ctx)
            call parquet_emit_warning("parquet_open_writer: metadata key '" // trim(items(idx)%key) // &
                ".datatype' was added explicitly, so the type recorded for '" // trim(items(idx)%key) // &
                "' (" // datatype // ") is not written -- the explicit entry wins" // ctx)
            datatype = ""
            return
        end do
    end subroutine resolve_metadata_datatype
    module procedure parquet_assert_column_type
        integer :: idx
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.
        character(len=:), allocatable :: proc !! calling procedure named in the message.

        if (.not. writer%is_schema_enforced) return

        proc = "parquet_write_column"
        if (present(context)) proc = context

        idx = parquet_get_defined_column_index(writer, name)

        if (.not. parquet_is_type_compatible(writer%all_columns(idx)%data_type, expected_type)) then
            call writer_context_suffix(writer, ctx)
            error stop proc // ": type mismatch for column " // trim(name) // &
                                  " (expected " // trim(expected_type) // ", got " // &
                                  trim(writer%all_columns(idx)%data_type) // ")" // &
                                  ctx
        end if
    end procedure parquet_assert_column_type
    !> Records that `name` has now been written at least once, growing
    !> writer%written_names (schema-less writer only) so a later repeat
    !> write to the same name can be detected.
    !> Plain contained subroutine (not a module procedure) for the same
    !> reason as parquet_check_read_row_count above -- its body already
    !> lived in this file, the parquet_write parent, not a descendant
    !> submodule.
    subroutine parquet_mark_column_written(writer, name)
        type(parquet_writer), intent(inout) :: writer !! open (schema-less) writer being written to.
        character(len=*), intent(in) :: name !! column name just written.
        integer :: idx
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.

        if (.not. allocated(writer%enabled_columns)) then
            call parquet_check_and_mark_written_name(writer, name)
            return
        end if

        idx = parquet_get_enabled_column_index(writer, name)
        if (idx == 0) return

        if (writer%write_counts(idx) > 0) then
            call writer_context_suffix(writer, ctx)
            error stop "parquet_write_column: column written more than once: " // trim(name) // ctx
        end if
        writer%write_counts(idx) = writer%write_counts(idx) + 1
    end subroutine parquet_mark_column_written
    !> Schema-less writer (no cinfo, so enabled_columns/write_counts above
    !> don't exist and can't track this): checks `name` against every name
    !> already written by this writer, error stopping on a repeat, then
    !> records `name` as written. Grows written_names by one each call --
    !> fine here since it only holds as many entries as columns actually
    !> written (typically a handful to a few dozen), unlike a per-row buffer.
    subroutine parquet_check_and_mark_written_name(writer, name)
        type(parquet_writer), intent(inout) :: writer !! schema-less writer whose written_names gains one entry.
        character(len=*), intent(in) :: name !! column name just written.
        character(len=256), allocatable :: tmp(:)
        integer :: n, i
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.

        if (allocated(writer%written_names)) then
            do i = 1, size(writer%written_names)
                if (trim(writer%written_names(i)) == trim(name)) then
                    call writer_context_suffix(writer, ctx)
                    error stop "parquet_write_column: column written more than once: " // trim(name) // &
                        ctx
                end if
            end do
            n = size(writer%written_names)
            allocate(tmp(n + 1))
            tmp(1:n) = writer%written_names
            tmp(n + 1) = trim(name)
            call move_alloc(tmp, writer%written_names)
        else
            allocate(writer%written_names(1))
            writer%written_names(1) = trim(name)
        end if
    end subroutine parquet_check_and_mark_written_name
    module procedure parquet_is_column_enabled
        parquet_is_column_enabled = .true.
        if (.not. allocated(writer%enabled_columns)) return

        parquet_is_column_enabled = parquet_get_enabled_column_index(writer, name) > 0
    end procedure parquet_is_column_enabled
    module procedure parquet_get_column_col_size
        integer :: i

        parquet_get_column_col_size = 1
        if (.not. allocated(writer%enabled_columns)) return

        do i = 1, size(writer%enabled_columns)
            if (trim(writer%enabled_columns(i)%name) == trim(name)) then
                if (writer%enabled_columns(i)%col_size == parquet_size_auto) then
                    ! Deliberately does not tell the caller to "use the matrix write form": this is
                    ! also reached by a parquet_string_column write, which has no matrix form at all
                    ! (it is scalar-only, and rejects col_size /= 1 a few lines later).
                    error stop "parquet_write_column: col_size for column '" // trim(name) // "' is still " // &
                        "'auto' -- call schema%set_col_size before parquet_open_writer. Only a matrix (2-D) " // &
                        "write can resolve col_size from the data's own shape"
                end if
                parquet_get_column_col_size = max(1, writer%enabled_columns(i)%col_size)
                return
            end if
        end do
    end procedure parquet_get_column_col_size
    !> Resolves writer%all_columns(idx)/enabled_columns(idx)'s col_size against a matrix-form
    !> write call's own structural asize (size(values,1)): if col_size is still "auto" (a MAML
    !> col_size: auto not yet resolved by schema%set_col_size), this is the write that resolves
    !> it -- asize becomes the column's permanent col_size for the rest of this writer's lifetime,
    !> kept in sync across all_columns/enabled_columns and the C++-side column_metadata (so
    !> parquet_writer_get_chunk_size and a write_maml=.true. sidecar both see the resolved value).
    !> Otherwise (already resolved, from the MAML/set_col_size, or a previous write to the same
    !> column), asize must match exactly -- error stops (using `context`, e.g. "parquet_write_column"
    !> or "parquet_write_column_chunk", and optional pre-formatted `ctx` suffix, to match each
    !> call site's own existing message) on any mismatch.
    subroutine parquet_resolve_or_check_col_size(writer, name, idx, asize, context, ctx)
        type(parquet_writer), intent(inout) :: writer !! open (schema-enforced) writer.
        character(len=*), intent(in) :: name !! column being written.
        integer, intent(in) :: idx !! writer%all_columns index for this column.
        integer(int64), intent(in) :: asize !! this write call's own structural col_size.
        character(len=*), intent(in) :: context !! error message prefix ("parquet_write_column"/"parquet_write_column_chunk").
        character(len=*), intent(in), optional :: ctx !! optional writer_context_suffix text appended to the error.
        integer :: k
        character(len=:), allocatable :: outname

        if (writer%all_columns(idx)%col_size == parquet_size_auto) then
            writer%all_columns(idx)%col_size = int(asize)
            k = parquet_get_enabled_column_index(writer, name)
            if (k > 0) writer%enabled_columns(k)%col_size = int(asize)
            call parquet_resolve_output_name(writer, name, outname)
            call parquet_update_column_metadata_size(writer%handle, trim(outname)//char(0), &
                int(asize, kind=c_long_long), int(writer%all_columns(idx)%array_size, kind=c_long_long))
        else if (writer%all_columns(idx)%col_size /= asize) then
            if (present(ctx)) then
                error stop trim(context) // ": array size mismatch for column " // trim(name) // ctx
            else
                error stop trim(context) // ": array size mismatch for column " // trim(name)
            end if
        end if
    end subroutine parquet_resolve_or_check_col_size
    !> String-only counterpart of parquet_resolve_or_check_col_size, for array_size (maximum
    !> string length): if array_size is still "auto", this write resolves it to `item_len` (the
    !> caller's own Fortran-declared character length for this call, i.e. len(values(1)) or
    !> len(values(1,1)) -- a structural property of the call, not derived from actual string
    !> content, mirroring col_size's own shape-derived resolution). Otherwise a no-op: the
    !> existing max_item_len/array_size ceiling check at each call site runs unchanged afterward.
    !>
    !> A parquet_string_column write has no declared length to pass here at all; it records what it
    !> actually wrote in writer%observed_string_len instead, and parquet_reconcile_string_sizes
    !> below settles the column's reported array_size once, at close.
    subroutine parquet_resolve_or_check_array_size(writer, name, idx, item_len)
        type(parquet_writer), intent(inout) :: writer !! open (schema-enforced) writer.
        character(len=*), intent(in) :: name !! string column being written.
        integer, intent(in) :: idx !! writer%all_columns index for this column.
        integer, intent(in) :: item_len !! this write call's own declared character length.
        integer :: k
        character(len=:), allocatable :: outname

        if (writer%all_columns(idx)%array_size == parquet_size_auto) then
            writer%all_columns(idx)%array_size = item_len
            k = parquet_get_enabled_column_index(writer, name)
            if (k > 0) writer%enabled_columns(k)%array_size = item_len
            call parquet_resolve_output_name(writer, name, outname)
            call parquet_update_column_metadata_size(writer%handle, trim(outname)//char(0), &
                int(writer%all_columns(idx)%col_size, kind=c_long_long), int(item_len, kind=c_long_long))
        end if
    end subroutine parquet_resolve_or_check_array_size
    !> Makes every string column's reported array_size describe what was actually written, run once
    !> by parquet_close_writer BEFORE close_parquet_writer serializes the footer (the sidecar is
    !> rewritten from writer%all_columns afterwards, so both outputs follow from this one step).
    !>
    !> Only a parquet_string_column write can make this necessary, and it does so in two ways. A
    !> column declared `array_size: auto` and written only that way has no declared length anywhere
    !> to resolve from -- left alone it would reach the sidecar as the raw parquet_size_auto
    !> sentinel, i.e. `array_size: -1`, which is not a valid MAML at all (feature_risks.md Risk-83).
    !> And a column whose declaration is simply too small is written anyway: the compact path stores
    !> each element's own bytes and does not enforce array_size, which is deliberate -- a reader
    !> takes each length from the data and never needs the declaration. **Accepting an inaccurate
    !> declaration on the way in is a choice; repeating it on the way out is not**, so the value
    !> reported here is the longest element actually written whenever that exceeds the declaration.
    !>
    !> Deliberately at CLOSE rather than per write: raising the value mid-write would also raise the
    !> ceiling `any_item_too_long` enforces on the padded (character-array) paths, so a column
    !> written through both forms would have its declared limit quietly relaxed for the padded half.
    !> At close there are no writes left to affect.
    subroutine parquet_reconcile_string_sizes(writer)
        type(parquet_writer), intent(inout) :: writer !! writer being closed.
        integer :: i, k, resolved
        character(len=:), allocatable :: colname !! internal column name; must NOT be outname, see below.
        character(len=:), allocatable :: outname !! parquet_resolve_output_name scratch.

        if (.not. writer%is_schema_enforced) return
        if (.not. allocated(writer%all_columns) .or. .not. allocated(writer%observed_string_len)) return

        do i = 1, size(writer%all_columns)
            if (writer%all_columns(i)%array_size == parquet_size_auto) then
                ! An all-null or all-empty column observes 0, which is not a legal array_size; 1 is
                ! the same floor parquet_write_empty_columns applies for the same reason.
                resolved = max(1, writer%observed_string_len(i))
            else if (writer%observed_string_len(i) > writer%all_columns(i)%array_size) then
                resolved = writer%observed_string_len(i)
            else
                cycle
            end if

            writer%all_columns(i)%array_size = resolved
            colname = trim(writer%all_columns(i)%name)
            k = parquet_get_enabled_column_index(writer, colname)
            if (k > 0) writer%enabled_columns(k)%array_size = resolved
            ! colname and outname must stay SEPARATE variables. parquet_resolve_output_name's
            ! output_name dummy is allocatable intent(out), so it is deallocated on entry --
            ! passing one variable as both arguments frees the very storage its own intent(in)
            ! `name` dummy points at, and the trim(name) inside it then reads freed memory.
            call parquet_resolve_output_name(writer, colname, outname)
            call parquet_update_column_metadata_size(writer%handle, trim(outname)//char(0), &
                int(writer%all_columns(i)%col_size, kind=c_long_long), int(resolved, kind=c_long_long))
        end do
    end subroutine parquet_reconcile_string_sizes
    !> Every column in a file must have the same number of rows (Arrow/Parquet
    !> requirement). Called by every parquet_write_column variant with that
    !> call's own row count: the first call for a given writer fixes the
    !> expected row count, every later call must match it or error stop.
    !> Plain contained subroutine (not a module procedure) for the same
    !> reason as parquet_check_read_row_count above -- its body already
    !> lived in this file, the parquet_write parent, not a descendant
    !> submodule.
    subroutine parquet_check_row_count(writer, name, nrows)
        type(parquet_writer), intent(inout) :: writer !! open writer whose expected_nrows this call checks/sets.
        character(len=*), intent(in) :: name !! column being written; named only in the error-stop message.
        integer(c_long_long), intent(in) :: nrows !! row count of this write call's own values.
        character(len=32) :: expected_str, got_str
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.

        if (writer%expected_nrows < 0) then
            writer%expected_nrows = nrows
            return
        end if

        if (writer%expected_nrows /= nrows) then
            write(expected_str, '(i0)') writer%expected_nrows
            write(got_str, '(i0)') nrows
            call writer_context_suffix(writer, ctx)
            error stop "parquet_write_column: row count mismatch for column " // trim(name) // &
                ": expected " // trim(expected_str) // " rows (from an earlier column) but got " // trim(got_str) // &
                ctx
        end if
    end subroutine parquet_check_row_count
    !> Expands a `nrows`-length row-keep mask into an element-level mask for a flat array laid
    !> out `block_width` contiguous elements per row (the col_size for a numeric/logical/matrix
    !> column, or item_len*col_size for a packed fixed-width string buffer) -- i.e. mask(i)
    !> applies to elements ((i-1)*block_width+1) : (i*block_width). Used with `pack` to compact
    !> a flat values/is_valid buffer down to its kept rows in one step.
    function parquet_mask_expand_block(mask, block_width) result(elem_mask)
        logical, intent(in) :: mask(:) !! per-row keep mask.
        integer(int64), intent(in) :: block_width !! array elements per row.
        logical, allocatable :: elem_mask(:) !! element-level mask, size(mask)*block_width long.

        if (block_width == 1_int64) then
            elem_mask = mask
        else
            elem_mask = reshape(spread(mask, 1, int(block_width)), [size(mask, kind=int64)*block_width])
        end if
    end function parquet_mask_expand_block
    !> Reports whether a whole-file row mask (parquet_write_row_mask) is in force for a
    !> whole-column write of `nrows` (pre-mask) rows and, when one is, returns it in `row_mask`.
    !> `error stop`s if a file_mask is set but its size doesn't match `nrows` exactly. Also marks
    !> the writer as started (blocking a later parquet_write_row_mask call) and as having had a
    !> whole-column write (blocking parquet_write_chunk_row_mask).
    !>
    !> `masked` is .false. for the overwhelmingly common unmasked write, and `row_mask` is then
    !> left UNALLOCATED rather than filled with an all-.true. identity mask. Callers take their
    !> own fast path on it -- see the "unmasked fast path" note in parquet_write_numeric.f90.
    !> Fabricating the identity mask here is what used to make every ordinary write allocate and
    !> then `pack` through a full-length logical array that could not remove a single row.
    subroutine parquet_writer_whole_column_mask(writer, name, nrows, row_mask, masked)
        type(parquet_writer), intent(inout) :: writer !! open writer.
        character(len=*), intent(in) :: name !! column being written; named only in the error-stop message.
        integer(int64), intent(in) :: nrows !! this call's own (pre-mask) row count.
        logical, allocatable, intent(out) :: row_mask(:) !! the `nrows`-long row-keep mask; unallocated if .not. masked.
        logical, intent(out) :: masked !! .true. if a whole-file row mask applies to this write.
        character(len=32) :: expected_str, got_str
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.

        writer%write_started = .true.
        writer%any_whole_column_write = .true.

        masked = allocated(writer%file_mask)
        if (.not. masked) return

        if (size(writer%file_mask, kind=int64) /= nrows) then
            write(expected_str, '(i0)') size(writer%file_mask, kind=int64)
            write(got_str, '(i0)') nrows
            call writer_context_suffix(writer, ctx)
            error stop "parquet_write_column: values row count (" // trim(got_str) // &
                ") does not match the mask set via parquet_write_row_mask (" // trim(expected_str) // &
                " rows) for column " // trim(name) // ctx
        end if
        row_mask = writer%file_mask
    end subroutine parquet_writer_whole_column_mask
    module procedure parquet_write_row_mask_impl
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.
        type(writer_lock) :: lk !! Releases writer's concurrency guard on every exit path (FINAL).

        call check_writer_open(writer)
        call lk%claim(writer)
        call writer_context_suffix(writer, ctx)
        if (writer%write_started) error stop &
            "parquet_write_row_mask: must be called before the writer's first parquet_write_column " // &
            "or parquet_new_row_group call" // ctx
        if (allocated(writer%file_mask)) error stop &
            "parquet_write_row_mask: already called for this writer -- it can only be set once" // ctx
        if (size(mask, kind=int64) <= 0_int64) error stop &
            "parquet_write_row_mask: mask must not be zero-length" // ctx
        writer%file_mask = mask
    end procedure parquet_write_row_mask_impl
    module procedure parquet_write_chunk_row_mask_impl
        character(len=32) :: expected_str, got_str
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.
        type(writer_lock) :: lk !! Releases writer's concurrency guard on every exit path (FINAL).

        call check_writer_open(writer)
        call lk%claim(writer)
        call writer_context_suffix(writer, ctx)
        if (.not. writer%in_row_group) error stop &
            "parquet_write_chunk_row_mask: no row group is open (call parquet_new_row_group first)" // ctx
        if (writer%chunk_mask_set_this_group) error stop &
            "parquet_write_chunk_row_mask: already called for this row group" // ctx
        if (allocated(writer%file_mask)) error stop &
            "parquet_write_chunk_row_mask: cannot be used together with parquet_write_row_mask on the same writer" // ctx
        if (writer%any_whole_column_write) error stop &
            "parquet_write_chunk_row_mask: unavailable once a column has been written whole via " // &
            "parquet_write_column -- use parquet_write_row_mask instead" // ctx
        if (writer%row_group_first_write_done) error stop &
            "parquet_write_chunk_row_mask: must be called before this row group's first " // &
            "parquet_write_column_chunk call" // ctx
        if (writer%chunk_mask_scheme == 2) error stop &
            "parquet_write_chunk_row_mask: was not used for this writer's first row group -- it cannot be " // &
            "introduced for a later one" // ctx
        if (size(mask, kind=int64) /= writer%current_row_group_nrows) then
            write(expected_str, '(i0)') writer%current_row_group_nrows
            write(got_str, '(i0)') size(mask, kind=int64)
            error stop "parquet_write_chunk_row_mask: mask size (" // trim(got_str) // &
                ") does not match the open row group's own nrows (" // trim(expected_str) // ")" // ctx
        end if
        writer%chunk_mask = mask
        writer%chunk_mask_set_this_group = .true.
        ! The kept count for this row group is now known -- open (or re-open, with the correct
        ! kept count) the underlying C++ row group before any column's parquet_write_column_chunk
        ! call for it. See parquet_new_row_group_impl's own comment for why this can't happen any
        ! earlier for the per-row-group mask scheme, and row_group_is_empty for the kept-count-0 case.
        if (count(mask) == 0) then
            writer%row_group_is_empty = .true.
        else
            call parquet_writer_new_row_group(writer%handle, count(mask, kind=c_long_long))
            writer%cpp_row_group_open = .true.
        end if
    end procedure parquet_write_chunk_row_mask_impl
    module procedure parquet_open_writer
        integer :: i, k, n_enabled, jc
        character(len=:), allocatable :: compression_name, col_out_name
        character(len=:), allocatable :: dt !! resolve_metadata_datatype scratch.
        integer :: level_value, chunk_size_value
        logical :: comp_ok
        logical :: use_threads_value
        logical :: overwrite_value, file_exists
        !> The schema this writer actually reads, which is a local COPY of the caller's. It has to
        !! be a copy because `schema` is intent(in) and a schema whose %maml was populated
        !! directly (an embedded MAML, say) still needs parsing before %cinfo exists -- parsing a
        !! copy is legal under intent(in) and cannot race, since no other thread can see it.
        !! Unconditional rather than only-when-unparsed so that everything below reads one
        !! variable; the cost is one schema copy per file opened, on a path that already copies
        !! every column into writer%all_columns.
        type(parquet_schema) :: eff_schema
        !> An alias derived from parquet_settings' own list, never a second copy of it: the setter
        !! and this argument check must accept exactly the same set of codecs.
        character(len=12), parameter :: valid_compressions(6) = parquet_valid_compressions

        ! Refresh the C++ side's copy of every mirrored setting before any C++ state exists --
        ! see parquet_push_settings_to_cpp. `target_row_group_bytes` is read by the row-group
        ! sizing at close, so it has to be current from the moment this writer exists.
        call parquet_push_settings_to_cpp()

        overwrite_value = .true.
        if (present(overwrite)) overwrite_value = overwrite
        if (.not. overwrite_value) then
            inquire(file=trim(filename), exist=file_exists)
            if (file_exists) then
                error stop "parquet_open_writer: file already exists and overwrite=.false. (file: " // &
                    trim(filename) // ")"
            end if
        end if

        writer%handle = create_parquet_writer(trim(filename)//char(0))
        writer%filename = trim(filename)
        writer%is_schema_enforced = present(schema)
        writer%qc = present(schema)
        if (present(qc)) writer%qc = qc

        ! Defaults to "zstd" (level 3) -- Parquet-the-library's own built-in default is
        ! actually "uncompressed" (confirmed in parquet/properties.h), and the wider
        ! ecosystem convention on top of it (pyarrow, Spark, ...) is snappy, but this
        ! library intentionally goes one step further: zstd at a moderate level gives a
        ! meaningfully better compression ratio than snappy for a modest write-time cost,
        ! with no read-time penalty (see the compression benchmark backing this decision).
        ! Both defaults are overridable process-wide (parquet_set_default_compression /
        ! ..._level), and the whole resolution -- including the rule that level 3 applies only
        ! when the codec was defaulted all the way -- lives in one place rather than here; see
        ! parquet_resolve_writer_compression (src/parquet_settings.f90) for why that condition
        ! is load-bearing.
        call parquet_resolve_writer_compression(compression, compression_level, compression_name, level_value)

        comp_ok = .false.
        do jc = 1, size(valid_compressions)
            if (trim(compression_name) == trim(valid_compressions(jc))) then
                comp_ok = .true.
                exit
            end if
        end do
        if (.not. comp_ok) then
            error stop "parquet_open_writer: unknown compression codec '" // trim(compression_name) // &
                "' (expected one of: uncompressed, snappy, gzip, zstd, brotli, lz4) (file: " // trim(filename) // ")"
        end if

        chunk_size_value = -1 ! <= 0 tells the C++ side "not set": auto-size from the final row count at close time.
        if (present(chunk_size)) chunk_size_value = chunk_size

        use_threads_value = parquet_get_default_use_threads()
        if (present(use_threads)) use_threads_value = use_threads

        call parquet_set_writer_options(writer%handle, trim(compression_name)//char(0), &
            int(level_value, kind=c_int), int(chunk_size_value, kind=c_long_long), &
            merge(1_c_int, 0_c_int, use_threads_value))

        if (present(schema)) then
            eff_schema = schema
            if (.not. eff_schema%is_parsed()) then
                ! A schema built with %init/%add_field is already in step with its own text, and
                ! one loaded by parquet_parse_maml obviously is -- so this only fires for a schema
                ! whose %maml was populated directly (the embedded-MAML route) and never parsed.
                if (.not. allocated(eff_schema%maml%lines)) then
                    error stop "parquet_open_writer: schema is empty -- build it with " // &
                        "schema%init/%add_field, or load one with parquet_parse_maml (file: " // &
                        trim(filename) // ")"
                end if
                call parquet_parse_maml(eff_schema)
            end if
            if (allocated(eff_schema%maml%name)) writer%maml_name = eff_schema%maml%name

            allocate(writer%all_columns(size(eff_schema%cinfo%col, kind=int64)))
            writer%all_columns = eff_schema%cinfo%col
            allocate(writer%observed_string_len(size(eff_schema%cinfo%col, kind=int64)))
            writer%observed_string_len = 0

            n_enabled = 0
            do i = 1, size(eff_schema%cinfo%col)
                if (eff_schema%cinfo%col(i)%is_set) n_enabled = n_enabled + 1
            end do

            if (n_enabled > 0) then
                allocate(writer%enabled_columns(n_enabled))
                allocate(writer%write_counts(n_enabled))
                writer%write_counts = 0
                k = 0
            end if

            do i = 1, size(eff_schema%cinfo%col)
                if (eff_schema%cinfo%col(i)%is_set) then
                    k = k + 1
                    writer%enabled_columns(k) = eff_schema%cinfo%col(i)
                    ! Registers the schema under output_name (the column's
                    ! display/file name -- equal to name unless a col_map:
                    ! entry renamed it), not the internal name, since that's
                    ! what append_column's own arguments must also use for
                    ! Arrow to line up the field with its data.
                    call parquet_column_output_name(eff_schema%cinfo%col(i), col_out_name)
                    call parquet_add_column_info(&
                        writer, &
                        col_out_name, &
                        eff_schema%cinfo%col(i)%unit, &
                        eff_schema%cinfo%col(i)%info, &
                        eff_schema%cinfo%col(i)%ucd, &
                        eff_schema%cinfo%col(i)%data_type, &
                        eff_schema%cinfo%col(i)%array_size, &
                        eff_schema%cinfo%col(i)%col_size )
                    ! A protected column may hold no Null at all (parquet_check_protected), so
                    ! its Arrow field is written NON-nullable. The C++ side is told here, once,
                    ! rather than at each write: for the mask-carrying kinds an all-.true. mask is
                    ! erased before the append anyway, but a temporal column and a
                    ! parquet_string_column carry their null state inside the element with no mask
                    ! to erase, and protection is the only signal that can make their fields
                    ! non-nullable. Registered under the OUTPUT name, like every other C++-side
                    ! column registration above.
                    if (eff_schema%cinfo%col(i)%is_protected) then
                        call parquet_writer_set_protected_column(writer%handle, trim(col_out_name)//char(0))
                    end if
                end if
            end do

            if (allocated(eff_schema%metadata%items)) then
                do i = 1, size(eff_schema%metadata%items)
                    call resolve_metadata_datatype(writer, eff_schema%metadata%items, i, dt)
                    call parquet_add_table_metadata(writer%handle, &
                        trim(eff_schema%metadata%items(i)%key)//char(0), &
                        trim(eff_schema%metadata%items(i)%value)//char(0), &
                        trim(eff_schema%metadata%items(i)%description)//char(0), &
                        trim(dt)//char(0))
                end do
            end if
        end if

        if (present(write_maml)) then
            if (write_maml) then
                if (.not. present(schema)) then
                    error stop "parquet_open_writer: write_maml=.true. requires a schema " // &
                        "(prepared by parquet_parse_maml) to be present (file: " // trim(filename) // ")"
                end if
                ! No separate "schema present but not obtained from parquet_parse_maml" check here:
                ! every route that populates eff_schema%cinfo%col (read just above, unconditionally,
                ! to populate writer%all_columns) also establishes
                ! eff_schema%metadata%source_maml_lines -- both parquet_parse_maml forms set it
                ! outright, and schema%init sets it before %add_field starts appending to it -- so
                ! by the time a schema safely reaches this point it is already allocated.
                !
                ! The sidecar file itself is not written here: a col_size:/array_size: auto field
                ! may still be unresolved at this point (resolved later, at first write, or via
                ! schema%set_col_size/%set_array_size) -- writing now could bake in a stale "auto"
                ! that no longer matches the .parquet file's actual columns. parquet_close_writer
                ! writes it instead, once every column is guaranteed resolved.
                writer%sidecar_lines = eff_schema%metadata%source_maml_lines
                call parquet_prune_disabled_fields(writer%sidecar_lines, eff_schema%cinfo)
                writer%write_maml_requested = .true.
            end if
        end if
    end procedure parquet_open_writer
    !> Removes, from `lines` (a working copy of metadata%source_maml_lines),
    !> the `fields:` entries whose column is disabled (is_set = .false.) in
    !> `cinfo`, so that a sidecar .maml written via write_maml=.true. only
    !> lists the columns actually present in the .parquet file. Matches source
    !> MAML field blocks to cinfo%col by output_name (the name the source MAML
    !> text itself declares under fields:/name: -- equal to cinfo%col%name
    !> unless a col_map: rename applies); a block runs from its top-level
    !> "- ..." line up to (but not including) the next top-level line. If every
    !> column in cinfo is disabled, lines is left untouched instead of emptying
    !> out fields: entirely, since a MAML file with no fields fails
    !> parquet_validate_maml on the next read.
    subroutine parquet_prune_disabled_fields(lines, cinfo)
        character(len=:), allocatable, intent(inout) :: lines(:) !! working copy of source MAML lines, pruned in place.
        type(parquet_column_info), intent(in) :: cinfo !! schema whose disabled columns' fields: entries are removed.
        logical, allocatable :: keep(:)
        character(len=:), allocatable :: tline, key, cvalue, field_name, scratch_str
        character(len=:), allocatable :: new_lines(:)
        integer :: i, j, k, n, idx_fields, block_start, block_end, col_idx, n_keep

        if (.not. allocated(cinfo%col)) return
        if (size(cinfo%col) == 0) return
        if (.not. any(cinfo%col(:)%is_set)) return

        n = size(lines)
        idx_fields = 0
        do i = 1, n
            if (lines(i)(1:1) /= " " .and. parquet_maml_key_matches(lines(i), "fields:")) then
                idx_fields = i
                exit
            end if
        end do
        if (idx_fields == 0) return

        allocate(keep(n))
        keep = .true.

        i = idx_fields + 1
        do while (i <= n)
            if (len_trim(lines(i)) == 0) then
                i = i + 1
                cycle
            end if

            if (lines(i)(1:1) /= " " .and. index(trim(adjustl(lines(i))), "-") /= 1) exit

            if (lines(i)(1:1) /= " ") then
                ! Top-level "- ..." line: start of a new field block. It runs
                ! until the next top-level (non-indented) line.
                block_start = i
                block_end = i
                j = i + 1
                do while (j <= n)
                    if (len_trim(lines(j)) == 0) exit
                    if (lines(j)(1:1) /= " ") exit
                    block_end = j
                    j = j + 1
                end do

                field_name = ""
                do k = block_start, block_end
                    if (k == block_start) then
                        tline = trim(adjustl(lines(k)))
                        tline = trim(adjustl(tline(2:)))
                        if (len_trim(tline) == 0) cycle
                    else
                        tline = trim(adjustl(lines(k)))
                    end if
                    call parquet_split_key_value(tline, key, cvalue)
                    if (len_trim(key) == 0) cycle
                    call parquet_to_lower(trim(key), scratch_str)
                    if (scratch_str == "name") then
                        call parquet_unquote(cvalue, field_name)
                        exit
                    end if
                end do

                col_idx = 0
                if (len_trim(field_name) > 0) then
                    do k = 1, size(cinfo%col)
                        call parquet_column_output_name(cinfo%col(k), scratch_str)
                        if (trim(scratch_str) == trim(field_name)) then
                            col_idx = k
                            exit
                        end if
                    end do
                end if

                if (col_idx > 0) then
                    if (.not. cinfo%col(col_idx)%is_set) keep(block_start:block_end) = .false.
                end if

                i = block_end + 1
                cycle
            end if

            i = i + 1
        end do

        n_keep = count(keep)
        if (n_keep == n) return

        allocate(character(len=len(lines)) :: new_lines(n_keep))
        j = 0
        do i = 1, n
            if (keep(i)) then
                j = j + 1
                new_lines(j) = lines(i)
            end if
        end do

        call move_alloc(new_lines, lines)
    end subroutine parquet_prune_disabled_fields
    !> Rewrites every surviving field block's col_size:/array_size: value (if present) in `lines`
    !> (a working copy of a write_maml=.true. sidecar's source lines, already pruned of disabled
    !> fields by parquet_prune_disabled_fields) to `all_columns`' own final, fully-resolved
    !> value -- so a col_size: auto/array_size: auto placeholder in the original MAML source
    !> becomes the concrete number actually written to the .parquet file, matched by output_name
    !> the same way parquet_prune_disabled_fields matches field blocks to columns. Called by
    !> parquet_close_writer, once every enabled column's col_size/array_size is guaranteed
    !> resolved (parquet_close_writer's own missing-write check already ensures every enabled
    !> column was written at least once). A no-op for a column whose source never declared
    !> col_size:/array_size: at all (nothing to rewrite -- the implicit default of 1 already
    !> matches). Reallocates `lines` to a longer fixed element length up front if needed, since a
    !> resolved value's text (e.g. "col_size: 2000000000") can be longer than the original
    !> "col_size: auto"/blank line.
    subroutine parquet_rewrite_resolved_sizes(lines, all_columns)
        character(len=:), allocatable, intent(inout) :: lines(:) !! working copy of sidecar MAML lines, rewritten in place.
        type(parquet_column_type), intent(in) :: all_columns(:) !! writer's final, fully-resolved column state.
        character(len=:), allocatable :: tline, key, cvalue, field_name, scratch_str, new_text
        character(len=:), allocatable :: new_lines(:)
        integer :: i, j, k, n, idx_fields, block_start, block_end, col_idx, newlen
        character(len=32) :: numbuf

        if (size(all_columns) == 0) return

        n = size(lines)
        idx_fields = 0
        do i = 1, n
            if (lines(i)(1:1) /= " " .and. parquet_maml_key_matches(lines(i), "fields:")) then
                idx_fields = i
                exit
            end if
        end do
        if (idx_fields == 0) return

        ! Room for the longest possible "  array_size: <10-digit int>" replacement text, so no
        ! rewritten line below is ever truncated by a too-short fixed element length.
        newlen = max(len(lines), 30)
        if (newlen > len(lines)) then
            allocate(character(len=newlen) :: new_lines(n))
            do i = 1, n
                new_lines(i) = lines(i)
            end do
            call move_alloc(new_lines, lines)
        end if

        i = idx_fields + 1
        do while (i <= n)
            if (len_trim(lines(i)) == 0) then
                i = i + 1
                cycle
            end if

            if (lines(i)(1:1) /= " " .and. index(trim(adjustl(lines(i))), "-") /= 1) exit

            if (lines(i)(1:1) /= " ") then
                block_start = i
                block_end = i
                j = i + 1
                do while (j <= n)
                    if (len_trim(lines(j)) == 0) exit
                    if (lines(j)(1:1) /= " ") exit
                    block_end = j
                    j = j + 1
                end do

                field_name = ""
                do k = block_start, block_end
                    if (k == block_start) then
                        tline = trim(adjustl(lines(k)))
                        tline = trim(adjustl(tline(2:)))
                        if (len_trim(tline) == 0) cycle
                    else
                        tline = trim(adjustl(lines(k)))
                    end if
                    call parquet_split_key_value(tline, key, cvalue)
                    if (len_trim(key) == 0) cycle
                    call parquet_to_lower(trim(key), scratch_str)
                    if (scratch_str == "name") then
                        call parquet_unquote(cvalue, field_name)
                        exit
                    end if
                end do

                col_idx = 0
                if (len_trim(field_name) > 0) then
                    do k = 1, size(all_columns)
                        call parquet_column_output_name(all_columns(k), scratch_str)
                        if (trim(scratch_str) == trim(field_name)) then
                            col_idx = k
                            exit
                        end if
                    end do
                end if

                if (col_idx > 0) then
                    do k = block_start, block_end
                        tline = trim(adjustl(lines(k)))
                        if (k == block_start) then
                            tline = trim(adjustl(tline(2:)))
                            if (len_trim(tline) == 0) cycle
                        end if
                        call parquet_split_key_value(tline, key, cvalue)
                        if (len_trim(key) == 0) cycle
                        call parquet_to_lower(trim(key), scratch_str)
                        if (scratch_str == "col_size") then
                            write(numbuf, '(I0)') all_columns(col_idx)%col_size
                            new_text = "  col_size: " // trim(numbuf)
                            lines(k) = new_text
                        else if (scratch_str == "array_size") then
                            write(numbuf, '(I0)') all_columns(col_idx)%array_size
                            new_text = "  array_size: " // trim(numbuf)
                            lines(k) = new_text
                        end if
                    end do
                end if

                i = block_end + 1
                cycle
            end if

            i = i + 1
        end do
    end subroutine parquet_rewrite_resolved_sizes
    !> Writes `lines` to a sidecar .maml file next to `parquet_filename`: the same
    !> path with a trailing ".parquet" replaced by ".maml", or ".maml" appended if
    !> there is no ".parquet" suffix.
    subroutine parquet_write_maml_sidecar(parquet_filename, lines)
        character(len=*), intent(in) :: parquet_filename !! output .parquet path; sidecar name is derived from this.
        character(len=*), intent(in) :: lines(:) !! MAML source lines to write verbatim.
        character(len=:), allocatable :: maml_filename
        integer :: unit, i, n

        n = len(parquet_filename)
        if (n >= 8) then
            if (parquet_filename(n-7:n) == ".parquet") then
                maml_filename = parquet_filename(1:n-8) // ".maml"
            else
                maml_filename = parquet_filename // ".maml"
            end if
        else
            maml_filename = parquet_filename // ".maml"
        end if

        open(newunit=unit, file=maml_filename, status="replace", action="write", form="formatted")
        do i = 1, size(lines)
            write(unit, '(a)') trim(lines(i))
        end do
        close(unit)
    end subroutine parquet_write_maml_sidecar
    !> Declares one column on a schema-less writer (%is_schema_enforced
    !> .false.), mirroring the field metadata a MAML fields: entry would
    !> otherwise supply. Called internally the first time each column
    !> name is written; not part of the public API.
    !> Plain contained subroutine (not a module procedure) for the same
    !> reason as parquet_check_read_row_count above -- its body already
    !> lived in this file, the parquet_write parent, not a descendant
    !> submodule.
    subroutine parquet_add_column_info(writer, name, unit, description, ucd, data_type, array_size, col_size)
        type(parquet_writer), intent(inout) :: writer !! schema-less writer gaining this column.
        character(len=*), intent(in) :: name !! column name.
        character(len=*), intent(in) :: unit !! unit of measurement.
        character(len=*), intent(in) :: description !! short free-text description.
        character(len=*), intent(in) :: ucd !! IVOA Unified Content Descriptor.
        character(len=*), intent(in) :: data_type !! column's data type.
        integer, intent(in) :: array_size !! maximum string length (string columns only).
        integer, intent(in) :: col_size !! vector-column element count (1 for a scalar column).

        call parquet_add_column_metadata(&
            writer%handle, &
            trim(name)//char(0), &
            trim(unit)//char(0), &
            trim(description)//char(0), &
            trim(ucd)//char(0), &
            trim(data_type)//char(0), &
            int(array_size, kind=c_long_long), &
            int(col_size, kind=c_long_long) )
    end subroutine parquet_add_column_info
    !> Formats `value` as a trimmed plain integer for a qc-violation WARNING message.
    subroutine parquet_qc_format_int(value, text)
        integer(int64), intent(in) :: value !! value to format.
        character(len=:), allocatable, intent(out) :: text !! formatted, trimmed text.
        character(len=32) :: buf

        write(buf, '(i0)') value
        text = trim(adjustl(buf))
    end subroutine parquet_qc_format_int
    !> Errors out if `name`'s column is listed under extra: protected_cols:
    !> (see parquet_parse_protected_cols, or schema%set_protected) and `is_valid_flat` contains
    !> any .false. entry. A no-op for a schema-less writer or an unlisted column.
    !>
    !> Also reports, in the optional `protected`, whether this column is protected at all --
    !> which callers use to ERASE an all-.true. mask before writing. A protected column may hold
    !> no Null, so a mask that survived the check above says nothing; dropping it means no
    !> validity buffer is built and the column's Arrow field comes out non-nullable through the
    !> ordinary mask-presence rule, instead of needing a precedence rule between "protected" and
    !> "a mask was passed". Passing an all-.true. mask for a protected column is explicitly
    !> allowed -- declaring a column protected must not make the is_valid keyword unusable.
    subroutine parquet_check_protected(writer, name, is_valid_flat, protected)
        type(parquet_writer), intent(in) :: writer !! open (schema-enforced) writer.
        character(len=*), intent(in) :: name !! column name.
        logical, intent(in) :: is_valid_flat(:) !! flattened validity mask for this write call.
        logical, intent(out), optional :: protected !! .true. if this column is protected (mask may be erased).
        integer :: idx

        if (present(protected)) protected = .false.
        if (.not. writer%is_schema_enforced) return
        idx = parquet_get_defined_column_index(writer, name)
        if (idx == 0) return
        if (.not. writer%all_columns(idx)%is_protected) return
        if (present(protected)) protected = .true.
        if (.not. all(is_valid_flat)) then
            error stop "parquet_write_column: column '" // trim(name) // &
                "' is protected (extra: protected_cols:) and cannot contain Null values"
        end if
    end subroutine parquet_check_protected
    !> If writer%qc is set and the column declares an EXPLICIT, EMPTY qc: miss: -- the one form that
    !! asks for Null validation, see parquet_column_type%qc_allow_null -- prints a WARNING naming the
    !! column and how many of its elements are Null in this write call. Never errors: writing proceeds
    !! regardless, which is a deliberate write-side design choice (the read side escalates per
    !! qc_soft), and qc_min/qc_max are unaffected (they already only ever look at valid elements).
    !! No-op for a schema-less writer, an undeclared column, a column whose qc: miss: says Null/NA, and
    !! -- the common case -- a column declaring no qc: miss: at all.
    subroutine parquet_check_qc_miss(writer, name, is_valid_flat)
        type(parquet_writer), intent(in) :: writer !! open (schema-enforced) writer.
        character(len=*), intent(in) :: name !! column name.
        logical, intent(in) :: is_valid_flat(:) !! flattened validity mask for this write call (.true. => valid).
        integer :: idx
        integer(int64) :: n_null, n_total
        character(len=:), allocatable :: fmt_int, fmt_int2

        if (.not. writer%qc) return
        if (.not. writer%is_schema_enforced) return
        idx = parquet_get_defined_column_index(writer, name)
        if (idx == 0) return
        if (writer%all_columns(idx)%qc_allow_null) return

        n_total = size(is_valid_flat, kind=int64)
        n_null = count(.not. is_valid_flat, kind=int64)
        if (n_null == 0) return

        call parquet_qc_format_int(n_null, fmt_int)
        call parquet_qc_format_int(n_total, fmt_int2)
        call parquet_emit_warning("qc violation for column '" // trim(name) // "': " // fmt_int // " of " // &
            fmt_int2 // " element(s) are Null (qc: miss: is declared empty, so Nulls are not expected here)")
    end subroutine parquet_check_qc_miss
    !> Builds the int8 validity buffer and c_ptr passed down to the C++
    !> append_* functions from a caller's flattened `is_valid` mask (1 =
    !> valid, 0 = Null); `valid_ptr` stays c_null_ptr (no validity buffer
    !> passed to C++ at all) when `is_valid` was not supplied by the caller,
    !> which keeps every column non-nullable by default -- exactly today's
    !> behavior.
    subroutine parquet_make_valid_buf_write(is_valid_flat, valid_buf, valid_ptr)
        logical, intent(in), optional :: is_valid_flat(:) !! flattened validity mask, or absent for a non-nullable write.
        integer(c_int8_t), allocatable, target, intent(out) :: valid_buf(:) !! int8 validity buffer backing valid_ptr.
        type(c_ptr), intent(out) :: valid_ptr !! c_loc(valid_buf), or c_null_ptr if is_valid_flat is absent.
        integer(int64) :: i

        if (present(is_valid_flat)) then
            allocate(valid_buf(size(is_valid_flat, kind=int64)))
            do i = 1_int64, size(is_valid_flat, kind=int64)
                valid_buf(i) = merge(1_c_int8_t, 0_c_int8_t, is_valid_flat(i))
            end do
            valid_ptr = c_loc(valid_buf)
        else
            valid_ptr = c_null_ptr
        end if
    end subroutine parquet_make_valid_buf_write
    !> Validates that a parquet_write_column_chunk call's own row count (`nrows`, from the shape
    !> of its `values`) matches the currently-open row group's own size
    !> (writer%current_row_group_nrows, set by parquet_new_row_group) -- the streaming
    !> counterpart to parquet_check_row_count, which instead fixes/checks a row count against
    !> the whole file.
    !> Also reports whether a row mask applies to this chunk (`masked`) and, when one does,
    !> returns it in `row_mask`; `row_mask` is left UNALLOCATED otherwise, so an unmasked chunked
    !> write allocates and packs nothing. Both masking routes count: a whole-file mask
    !> (parquet_write_row_mask, whose per-row-group window parquet_new_row_group_impl has already
    !> sliced into writer%chunk_mask) and a per-row-group one (parquet_write_chunk_row_mask).
    !> They are mutually exclusive by parquet_write_chunk_row_mask's own guard.
    !>
    !> Enforces parquet_write_chunk_row_mask's all-or-nothing-per-writer rule too:
    !> writer%chunk_mask_scheme is fixed (0 -> 1 or 2) at the first parquet_write_column_chunk
    !> call of the writer's first row group, and any later row group must then agree with that
    !> decision. This subroutine therefore must still be CALLED on every chunk write, unmasked or
    !> not -- it also validates the chunk's row count and opens the underlying C++ row group. Only
    !> the mask itself is skippable.
    subroutine parquet_check_row_group_row_count(writer, name, nrows, row_mask, masked)
        type(parquet_writer), intent(inout) :: writer !! open writer, expected to have a row group open.
        character(len=*), intent(in) :: name !! column being written; named only in the error-stop message.
        integer(c_long_long), intent(in) :: nrows !! row count of this chunk's own values.
        logical, allocatable, intent(out) :: row_mask(:) !! the `nrows`-long row-keep mask; unallocated if .not. masked.
        logical, intent(out) :: masked !! .true. if a row mask applies to this chunk (either scheme).
        character(len=32) :: expected_str, got_str
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.

        call writer_context_suffix(writer, ctx)
        if (.not. writer%in_row_group) error stop &
            "parquet_write_column_chunk: no row group is open (call parquet_new_row_group first) for column " // &
            trim(name) // ctx
        if (nrows /= writer%current_row_group_nrows) then
            write(expected_str, '(i0)') writer%current_row_group_nrows
            write(got_str, '(i0)') nrows
            call writer_context_suffix(writer, ctx)
            error stop "parquet_write_column_chunk: row count mismatch for column " // trim(name) // &
                ": the open row group has " // trim(expected_str) // " rows but this chunk has " // trim(got_str) // &
                ctx
        end if

        if (writer%chunk_mask_scheme == 0) then
            writer%chunk_mask_scheme = merge(1, 2, writer%chunk_mask_set_this_group)
        else if (writer%chunk_mask_scheme == 1 .and. .not. writer%chunk_mask_set_this_group) then
            error stop "parquet_write_column_chunk: parquet_write_chunk_row_mask must be called for every " // &
                "row group once used for any one of them (column " // trim(name) // ")" // ctx
        end if
        if (.not. writer%cpp_row_group_open .and. .not. writer%row_group_is_empty) then
            ! No mask was ever set for this row group (parquet_write_chunk_row_mask was not
            ! called) -- the kept count is simply nrows (identity), exactly today's non-masked
            ! behavior. Open the underlying C++ row group now, before this first chunk is appended.
            ! (row_group_is_empty can only already be true here via an explicit
            ! parquet_write_chunk_row_mask call with an all-.false. mask -- identity is never empty
            ! since nrows itself is always positive.)
            call parquet_writer_new_row_group(writer%handle, nrows)
            writer%cpp_row_group_open = .true.
        end if
        writer%row_group_first_write_done = .true.
        masked = allocated(writer%file_mask) .or. writer%chunk_mask_set_this_group
        if (masked) row_mask = writer%chunk_mask
    end subroutine parquet_check_row_group_row_count
    !> Marks `name` as written for parquet_mark_column_written's own bookkeeping (write_counts/
    !> written_names), but only on its very first chunk ever -- unlike parquet_write_column, a
    !> chunk column legitimately receives many write calls (one per row group), and
    !> parquet_mark_column_written itself would error stop ("written more than once") on any
    !> call after the first.
    subroutine parquet_chunk_mark_written_if_first(writer, name)
        type(parquet_writer), intent(inout) :: writer !! open writer.
        character(len=*), intent(in) :: name !! column name.
        integer :: idx, i
        logical :: is_first

        is_first = .true.
        if (writer%is_schema_enforced) then
            if (allocated(writer%enabled_columns)) then
                idx = parquet_get_enabled_column_index(writer, name)
                if (idx > 0) is_first = (writer%write_counts(idx) == 0)
            end if
        else
            if (allocated(writer%written_names)) then
                do i = 1, size(writer%written_names)
                    if (trim(writer%written_names(i)) == trim(name)) then
                        is_first = .false.
                        exit
                    end if
                end do
            end if
        end if
        if (is_first) call parquet_mark_column_written(writer, name)
    end subroutine parquet_chunk_mark_written_if_first
    !> Shared worker for parquet_new_row_group_int32/_int64 -- see the parquet_new_row_group
    !> generic interface in parquet_core.f90. Also resets the current row group's masking state and,
    !> for a writer using the shared whole-file mask (parquet_write_row_mask) together with row
    !> groups, claims this row group's window (the next `nrows` positions of the stored mask) --
    !> see "Chunked masking mechanics -- shared whole-file mask" in feature_write_mask.md.
    subroutine parquet_new_row_group_impl(writer, nrows)
        type(parquet_writer), intent(inout) :: writer !! open writer.
        integer(c_long_long), intent(in) :: nrows !! row count for the new row group; must be positive.
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.
        type(writer_lock) :: lk !! Releases writer's concurrency guard on every exit path (FINAL).

        call check_writer_open(writer)
        call lk%claim(writer)
        call writer_context_suffix(writer, ctx)
        if (writer%in_row_group) error stop &
            "parquet_new_row_group: a row group is already open -- call parquet_finish_row_group first" // ctx
        if (nrows <= 0) error stop "parquet_new_row_group: nrows must be positive" // ctx
        writer%write_started = .true.
        writer%in_row_group = .true.
        writer%current_row_group_nrows = nrows
        writer%chunk_mask_set_this_group = .false.
        writer%row_group_first_write_done = .false.
        writer%cpp_row_group_open = .false.
        writer%row_group_is_empty = .false.

        if (allocated(writer%file_mask)) then
            ! The shared whole-file mask's window (and therefore this row group's post-mask kept
            ! count) is fully known right now -- unlike the per-row-group scheme below, there is no
            ! later call that could still change it -- so the underlying C++ row group can (and
            ! must, to avoid a col_size/row-count mismatch against the compacted data any
            ! parquet_write_column_chunk call below will supply) be opened immediately with the
            ! *kept* count, not `nrows` itself. A kept count of exactly 0 has no underlying C++ row
            ! group at all (Arrow's NewRowGroup requires a positive count) -- see row_group_is_empty.
            writer%mask_used_with_row_groups = .true.
            if (writer%mask_cursor + nrows > size(writer%file_mask, kind=int64)) then
                error stop "parquet_new_row_group: this row group's nrows would claim more positions of the " // &
                    "mask (set via parquet_write_row_mask) than it has left" // ctx
            end if
            writer%chunk_mask = writer%file_mask(writer%mask_cursor+1 : writer%mask_cursor+nrows)
            writer%mask_cursor = writer%mask_cursor + nrows
            if (count(writer%chunk_mask) == 0) then
                writer%row_group_is_empty = .true.
            else
                call parquet_writer_new_row_group(writer%handle, count(writer%chunk_mask, kind=c_long_long))
                writer%cpp_row_group_open = .true.
            end if
        else
            ! Per-row-group masking (parquet_write_chunk_row_mask) may or may not be used for this
            ! row group -- not knowable yet, since that call (if it comes at all) happens after
            ! this one. Leave chunk_mask UNALLOCATED, meaning "no mask for this row group unless
            ! parquet_write_chunk_row_mask says otherwise", and defer the actual C++ open to
            ! whichever comes first of parquet_write_chunk_row_mask_impl or
            ! parquet_check_row_group_row_count (this row group's first parquet_write_column_chunk
            ! call) -- both commit using the by-then-known kept count.
            !
            ! This used to store an all-.true. identity mask of `nrows` instead, which every
            ! chunk write of the row group then packed through to remove nothing.
            !
            ! The deallocate is DEFENSIVE, not load-bearing: parquet_check_row_group_row_count
            ! decides whether a mask applies from file_mask/chunk_mask_set_this_group, never from
            ! allocated(chunk_mask), so a previous row group's mask left lying here could not be
            ! read anyway (confirmed by mutation: removing this line changes no test result).
            ! It is kept so that the state matches what it claims -- a predicate keyed on
            ! allocated(chunk_mask) is the obvious thing for a later change to reach for, and it
            ! would be silently wrong if a stale mask survived here.
            if (allocated(writer%chunk_mask)) deallocate(writer%chunk_mask)
        end if
    end subroutine parquet_new_row_group_impl
    module procedure parquet_new_row_group_int32
        call parquet_new_row_group_impl(writer, int(nrows, kind=c_long_long))
    end procedure parquet_new_row_group_int32
    module procedure parquet_new_row_group_int64
        call parquet_new_row_group_impl(writer, nrows)
    end procedure parquet_new_row_group_int64
    module procedure parquet_finish_row_group
        type(writer_lock) :: lk !! Releases writer's concurrency guard on every exit path (FINAL).
        call check_writer_open(writer)
        call lk%claim(writer)
        if (writer%row_group_is_empty) then
            ! A zero-kept-row row group has no underlying C++ row group at all (never opened) --
            ! nothing to finish either.
            continue
        else
            if (.not. writer%cpp_row_group_open) then
                ! Fallback: this row group never got a mask (parquet_write_chunk_row_mask) nor any
                ! parquet_write_column_chunk call at all -- open the C++ row group now (identity
                ! kept count) so parquet_writer_finish_row_group's own "no columns have been
                ! written" diagnostic still fires exactly as it did before masking existed, rather
                ! than the less specific "no row group is open". Both of the next two lines always
                ! execute immediately before the very same call's C++-side abort (there is no
                ! legitimate way to reach this fallback and then NOT abort -- see
                ! scenario_mask_row_group_no_writes_at_all), and a std::abort() discards this whole
                ! process's coverage counters (Fortran gcov included, not just C++'s own -- see
                ! CLAUDE.md's "Fortran gcov attribution artifacts"/report_fatal_error notes), so
                ! these two lines can never show as covered despite genuinely executing every time.
                call parquet_writer_new_row_group(writer%handle, writer%current_row_group_nrows) ! GCOVR_EXCL_LINE
                writer%cpp_row_group_open = .true. ! GCOVR_EXCL_LINE
            end if
            call parquet_writer_finish_row_group(writer%handle)
        end if
        writer%in_row_group = .false.
        writer%current_row_group_nrows = 0
    end procedure parquet_finish_row_group
    module procedure parquet_get_chunk_size_writer_int32
        type(writer_lock) :: lk !! Releases writer's concurrency guard on every exit path (FINAL).
        call check_writer_open(writer)
        call lk%claim(writer)
        chunk_size = int(parquet_writer_get_chunk_size(writer%handle), kind=int32)
    end procedure parquet_get_chunk_size_writer_int32
    module procedure parquet_get_chunk_size_writer_int64
        type(writer_lock) :: lk !! Releases writer's concurrency guard on every exit path (FINAL).
        call check_writer_open(writer)
        call lk%claim(writer)
        chunk_size = parquet_writer_get_chunk_size(writer%handle)
    end procedure parquet_get_chunk_size_writer_int64
    !> Writes every enabled column as a ZERO-ROW column when parquet_close_writer finds that a
    !> schema-enforced writer had nothing written to it at all -- an analysis stage that legitimately
    !> produced no rows, which used to abort with "missing write for enabled column".
    !>
    !> The point of doing it HERE, through the ordinary write specifics, is that no guard has to
    !> learn an exception: parquet_mark_column_written increments write_counts and the C++ side
    !> receives a real (empty) array, so parquet_close_writer's own missing-write check and
    !> close_parquet_writer's "Missing column data before close" both pass on their existing terms.
    !> Synthesizing empty arrays anywhere else would mean a second implementation of "append a
    !> column of type T", which could drift from the real one.
    !>
    !> The resulting file carries the schema in full -- every declared column, its unit/info/ucd and
    !> the table metadata -- with 0 rows, so a reader finds the columns it expects rather than the
    !> 0-column file a schema-less writer produces. That is exactly what a caller writing zero-length
    !> arrays by hand already got, which is why this is expressed as "as if they had".
    !>
    !> Deliberately does NOTHING in four cases, each meaning rows were expected or none were
    !> promised: any write already happened (`write_started`, which parquet_new_row_group sets too,
    !> so an opened row group counts); a row mask was set; the writer is schema-less; or no column
    !> is enabled. The last two already produce a valid 0-column file.
    subroutine parquet_write_empty_columns_if_none_written(writer)
        type(parquet_writer), intent(inout) :: writer !! writer being closed with nothing written.
        integer :: i, ncols
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.
        character(len=:), allocatable :: cname !! this column's name, copied out of `writer`.
        character(len=:), allocatable :: dtype !! this column's declared data_type.
        !> Zero-sized actuals, one per declared type. A rank-1 zero-length array serves a vector
        !! column too: the specifics check `mod(size(values), col_size) == 0`, which 0 satisfies for
        !! any width, and derive nrows = 0 from it.
        integer(int32) :: empty_i32(0)
        integer(int64) :: empty_i64(0)
        real(real32) :: empty_f32(0)
        real(real64) :: empty_f64(0)
        logical :: empty_bool(0)
        character(len=1) :: empty_str(0)
        type(parquet_date) :: empty_date(0)
        type(parquet_time) :: empty_time(0)
        type(parquet_timestamp) :: empty_ts(0)

        if (.not. writer%is_schema_enforced) return
        if (writer%write_started) return
        if (.not. allocated(writer%enabled_columns)) return
        if (allocated(writer%file_mask)) return

        ! An unresolved col_size:/array_size: auto has no data to be resolved from, and every value
        ! is vacuously consistent with a width when there are no values -- so resolve to 1, which is
        ! also the only choice that keeps a write_maml=.true. sidecar valid (parquet_validate_maml
        ! requires a positive integer, and schema%set_col_size refuses anything below 1).
        do i = 1, size(writer%enabled_columns)
            if (writer%enabled_columns(i)%col_size == parquet_size_auto) writer%enabled_columns(i)%col_size = 1
            if (writer%enabled_columns(i)%array_size == parquet_size_auto) writer%enabled_columns(i)%array_size = 1
        end do
        do i = 1, size(writer%all_columns)
            if (writer%all_columns(i)%col_size == parquet_size_auto) writer%all_columns(i)%col_size = 1
            if (writer%all_columns(i)%array_size == parquet_size_auto) writer%all_columns(i)%array_size = 1
        end do

        call writer_context_suffix(writer, ctx)
        call parquet_emit_warning("parquet_close_writer: no column was written; writing every declared " // &
            "column with 0 rows" // ctx)

        ncols = size(writer%enabled_columns)
        do i = 1, ncols
            ! Copied out first: `writer` and a component of `writer` may not both be actual
            ! arguments of one call, since the callee defines `writer` (F2018 15.5.2.13).
            cname = trim(writer%enabled_columns(i)%name)
            dtype = trim(writer%enabled_columns(i)%data_type)
            select case (dtype)
            case ("int32")
                call parquet_write_int32_column(writer, cname, empty_i32)
            case ("int64")
                call parquet_write_int64_column(writer, cname, empty_i64)
            case ("float32")
                call parquet_write_float32_column(writer, cname, empty_f32)
            case ("float64")
                call parquet_write_float64_column(writer, cname, empty_f64)
            case ("boolean", "bool8")
                call parquet_write_logical_column(writer, cname, empty_bool)
            case ("string")
                call parquet_write_string_column(writer, cname, empty_str)
            case ("date")
                call parquet_write_date_column(writer, cname, empty_date)
            case ("time")
                call parquet_write_time_column(writer, cname, empty_time)
            case ("timestamp")
                call parquet_write_timestamp_column(writer, cname, empty_ts)
            case default
                ! Unreachable through the public API: parquet_validate_maml rejects any other
                ! data_type before a schema can reach a writer. GCOVR_EXCL_LINE
                !
                ! gcov attribution artifact: gfortran gives this statement no entry for its first
                ! line and a positive count for its CONTINUATION, so the excluded line below is
                ! expected to show hits even though the arm never runs -- if it ever did, the
                ! error stop would take the whole suite down with it.
                error stop "parquet_close_writer: cannot write an empty column of data_type '" // &
                    dtype // "' for column " // cname // ctx ! GCOVR_EXCL_LINE
            end select
        end do
    end subroutine parquet_write_empty_columns_if_none_written
    module procedure parquet_close_writer
        integer :: i
        character(len=32) :: cursor_str, mask_str
        character(len=:), allocatable :: ctx !! writer_context_suffix scratch.

        if (.not. c_associated(writer%handle)) then
            error stop "parquet_close_writer: writer has not been opened, or was already closed"
        end if

        call parquet_write_empty_columns_if_none_written(writer)

        ! Nested rather than one `.and.`: Fortran does not guarantee short-circuit evaluation, so a
        ! single combined condition references size(writer%file_mask) even when no mask was ever
        ! set and file_mask is unallocated (a runtime error under -fcheck=all, undefined otherwise).
        if (writer%mask_used_with_row_groups) then
            if (allocated(writer%file_mask)) then
                if (writer%mask_cursor /= size(writer%file_mask, kind=int64)) then
                    write(cursor_str, '(i0)') writer%mask_cursor
                    write(mask_str, '(i0)') size(writer%file_mask, kind=int64)
                    call writer_context_suffix(writer, ctx)
                    error stop "parquet_close_writer: the mask set via parquet_write_row_mask (" // trim(mask_str) // &
                        " rows) was not fully consumed by this writer's row groups (only " // trim(cursor_str) // &
                        " rows claimed)" // ctx
                end if
            end if
        end if

        if (writer%is_schema_enforced .and. allocated(writer%enabled_columns)) then
            do i = 1, size(writer%enabled_columns)
                if (writer%write_counts(i) == 0) then
                    ! Filename/schema name go on their own lines rather than into the error stop
                    ! text, to keep that text short -- so they are ERROR CONTEXT, not warnings, and
                    ! go through the channel that is never suppressed and never redirected. Routing
                    ! them through parquet_emit_warning instead would let verbosity="errors_only"
                    ! produce an abort that names no file at all.
                    if (allocated(writer%filename)) call parquet_emit_error_context( &
                        "parquet_close_writer: output file: " // trim(writer%filename))
                    if (allocated(writer%maml_name)) then
                        call parquet_emit_error_context("parquet_close_writer: schema: " // trim(writer%maml_name))
                    else
                        call parquet_emit_error_context("parquet_close_writer: schema: (unnamed, built in-memory)")
                    end if
                    error stop "parquet_close_writer: missing write for enabled column: " // trim(writer%enabled_columns(i)%name)
                end if
            end do
        end if

        call parquet_reconcile_string_sizes(writer)

        call close_parquet_writer(writer%handle)

        if (writer%write_maml_requested) then
            call parquet_rewrite_resolved_sizes(writer%sidecar_lines, writer%all_columns)
            call parquet_write_maml_sidecar(writer%filename, writer%sidecar_lines)
            deallocate(writer%sidecar_lines)
            writer%write_maml_requested = .false.
        end if

        writer%handle = c_null_ptr
        if (allocated(writer%all_columns)) deallocate(writer%all_columns)
        if (allocated(writer%observed_string_len)) deallocate(writer%observed_string_len)
        if (allocated(writer%write_counts)) deallocate(writer%write_counts)
        if (allocated(writer%enabled_columns)) deallocate(writer%enabled_columns)
        if (allocated(writer%written_names)) deallocate(writer%written_names)
        writer%is_schema_enforced = .false.
        writer%expected_nrows = -1_c_long_long
    end procedure parquet_close_writer
    !> Safety net for a writer whose handle is still open when it goes out of
    !> scope or is overwritten (e.g. reassigned, or an early RETURN between
    !> parquet_open_writer and parquet_close_writer): frees the underlying
    !> C++ object so the process doesn't leak it. Uses abandon_parquet_writer, not
    !> close_parquet_writer: the latter builds/writes the final table and checks that every
    !> declared column was written and every row group finished, and throws (an uncaught C++
    !> exception that crosses the extern "C" boundary and aborts the process) if not -- erroring,
    !> let alone crashing, from an implicit finalizer on an incompletely-written file would be
    !> surprising. The resulting output file is therefore not guaranteed complete/valid when
    !> reached this way -- always prefer calling parquet_close_writer explicitly.

end submodule parquet_write
