parquet_columns_string.f90 Source File


Source Code

!===========================================
! Author: Elmo Tempel (elmo.tempel@ut.ee)
!===========================================
!
!> The string kinds of `parquet_column` (`PK_STRING`, `PK_STRING_VEC`), which are the one place
!! the type-erased storage is not a Fortran array.
!!
!! Per DD1 a string column **embeds a `parquet_string_column`** — already a self-contained
!! offsets + payload + validity store — and delegates to it rather than duplicating variable-
!! length storage or hoisting string validity into the column bitmap. That is why these
!! procedures are hand-written while every array kind's equivalents are generated: they are
!! delegation, not array indexing.
!!
!! `PK_STRING_VEC` uses **one** string store for the whole column, flat-indexed
!! `(i-1)*width + e` (RF6/s1). One store rather than `width` stores means a single set of
!! offsets and one payload buffer, and — because the layout is element-major within a row — one
!! row's whole vector stays contiguous, which is what row access and append-row want.
!!
!! There is deliberately **no `data_ptr`** for the string kinds: their storage is not a plain
!! Fortran array, so there is nothing kind-matched to alias. Use `string_column` to reach the
!! embedded store (and its zero-copy `view`/`view_slice` handles) instead.
submodule (parquet_columns) parquet_columns_string
    implicit none
contains
    !
    !> Aliases the embedded string store, so callers can use `parquet_string_column`'s own API
    !! (handles, searching, statistics) on a string column's values.
    !!
    !! **This is the implementation; the `string_column` binding below forwards to it.** Every
    !! per-cell accessor in this file comes in that pair -- see feature_ifx.md, and the typed-tier
    !! banner in `parquet_columns.f90`, for why the body has to live at the `type` end.
    !!
    !! Note the residual this file cannot remove: `col%str` is a `parquet_string_column` and its
    !! own accessors are type-bound, so a string read still converts `type` to `class` on the way
    !! into `parquet_strings`. That descriptor is one record of 248 bytes against `parquet_column`'s
    !! twenty of 1248, and closing it would mean adding public names to a deliberately
    !! near-independent module for an allocation-dominated path. Measure before widening scope.
    module procedure parquet_column_string_column
        if (.not. is_string_kind(col%kind)) then
            error stop EP//"string_column: column kind is "//trim(kind_text(col%kind))// &
                ", but this call requires a string kind"
        end if
        if (.not. allocated(col%str)) error stop EP//"string_column: string storage is not allocated"
        p => col%str
    end procedure parquet_column_string_column
    !
    !> Aliases the embedded string store (polymorphic form).
    module procedure string_column
        call parquet_column_string_column(self, p)
    end procedure string_column
    !
    !> Reads string element `i` out of a PK_STRING column.
    module procedure parquet_column_get_at_str
        call parquet_column_check_kind(col, PK_STRING, "get_at")
        call parquet_column_check_index(col, i, "get_at")
        call parquet_string_column_get(col%str, i, value, allow_null=.true.)
    end procedure parquet_column_get_at_str
    !
    !> Reads string element `i` out of a PK_STRING column (polymorphic form).
    module procedure get_at_str
        call parquet_column_get_at_str(self, i, value)
    end procedure get_at_str
    !
    !> Reads row `i`'s whole string vector out of a PK_STRING_VEC column.
    !!
    !! Values are blank-padded into the caller's fixed-length array; a value longer than the
    !! caller's element length is truncated by the assignment, which is the same contract the
    !! library's existing fixed-width string reads use.
    module procedure parquet_column_get_at_strv
        integer(int64) :: e, base, w
        call parquet_column_check_kind(col, PK_STRING_VEC, "get_at")
        call parquet_column_check_index(col, i, "get_at")
        w = int(col%width, int64)
        call parquet_column_check_width(col, size(value, kind=int64), "get_at")
        base = (i - 1_int64)*w
        ! `%copy_to` rather than `%get` into a temporary: `value(e)` is already a fixed-length slot,
        ! so going through an allocatable string costs a heap round trip per element for a copy that
        ! ends up blank-padded either way. Truncation semantics are identical -- see `%copy_to`.
        do e = 1_int64, w
            call parquet_string_column_copy_to(col%str, base + e, value(e), allow_null=.true.)
        end do
    end procedure parquet_column_get_at_strv
    !
    !> Reads row `i`'s whole string vector out of a PK_STRING_VEC column (polymorphic form).
    module procedure get_at_strv
        call parquet_column_get_at_strv(self, i, value)
    end procedure get_at_strv
    !
    !> Reads ONE element of row `i`'s vector out of a PK_STRING_VEC column.
    !!
    !! A vector string column is one flat store of `width * nrows` elements, element (e, row) at
    !! `(row-1)*width + e`. Reading a single one therefore needs no array and no padding, which is
    !! the whole difference from `get_at_strv` above.
    module procedure parquet_column_get_elem_strv
        call parquet_column_check_kind(col, PK_STRING_VEC, "get_elem")
        call parquet_column_check_index(col, i, "get_elem")
        call parquet_column_check_element(col, e, "get_elem")
        ! allow_null keeps a null element from aborting: it reads back as "", and %is_null is how
        ! a caller tells the two apart.
        call parquet_string_column_get(col%str, (i - 1_int64)*int(col%width, int64) + e, value, allow_null=.true.)
    end procedure parquet_column_get_elem_strv
    !
    !> Reads ONE element of row `i`'s string vector (polymorphic form).
    module procedure get_elem_strv
        call parquet_column_get_elem_strv(self, i, e, value)
    end procedure get_elem_strv
    !
    !> Writes ONE element of row `i`'s vector in a PK_STRING_VEC column.
    module procedure parquet_column_set_elem_strv
        call parquet_column_check_kind(col, PK_STRING_VEC, "set_elem")
        call parquet_column_check_index(col, i, "set_elem")
        call parquet_column_check_element(col, e, "set_elem")
        ! The store's own %set clears that element's null, exactly as it does for `set_at_str`.
        call parquet_string_column_set(col%str, (i - 1_int64)*int(col%width, int64) + e, value)
    end procedure parquet_column_set_elem_strv
    !
    !> Writes ONE element of row `i`'s string vector (polymorphic form).
    module procedure set_elem_strv
        call parquet_column_set_elem_strv(self, i, e, value)
    end procedure set_elem_strv
    !
    !> Writes string element `i` of a PK_STRING column.
    module procedure parquet_column_set_at_str
        logical :: mod_nulls
        mod_nulls = .true.
        if (present(modify_nulls)) mod_nulls = modify_nulls
        call parquet_column_check_kind(col, PK_STRING, "set_at")
        call parquet_column_check_index(col, i, "set_at")
        if (.not. mod_nulls) then
            if (parquet_string_column_is_null(col%str, i)) return
        end if
        call parquet_string_column_set(col%str, i, value)
    end procedure parquet_column_set_at_str
    !
    !> Writes string element `i` of a PK_STRING column (polymorphic form).
    module procedure set_at_str
        call parquet_column_set_at_str(self, i, value, modify_nulls)
    end procedure set_at_str
    !
    !> Writes row `i`'s whole string vector in a PK_STRING_VEC column. `value` is an ARRAY, so its
    !! trailing blanks are trimmed for the reason `refill_string_store` gives; `set_at_str` above
    !! takes a scalar and stores it verbatim.
    module procedure parquet_column_set_at_strv
        logical :: mod_nulls
        integer(int64) :: e, base, w
        mod_nulls = .true.
        if (present(modify_nulls)) mod_nulls = modify_nulls
        call parquet_column_check_kind(col, PK_STRING_VEC, "set_at")
        call parquet_column_check_index(col, i, "set_at")
        call parquet_column_check_width(col, size(value, kind=int64), "set_at")
        w = int(col%width, int64)
        base = (i - 1_int64)*w
        ! modify_nulls=.false. protects individual null ELEMENTS, not the whole row: a row with
        ! one null element still has its other elements written.
        do e = 1_int64, w
            if (.not. mod_nulls) then
                if (parquet_string_column_is_null(col%str, base + e)) cycle
            end if
            call parquet_string_column_set(col%str, base + e, value(e), trim=.true.)
        end do
    end procedure parquet_column_set_at_strv
    !
    !> Writes row `i`'s whole string vector (polymorphic form).
    module procedure set_at_strv
        call parquet_column_set_at_strv(self, i, value, modify_nulls)
    end procedure set_at_strv
    !
    !> Replaces every value of a PK_STRING column. Trailing blanks are trimmed -- see
    !! `refill_string_store` for why an ARRAY argument trims where a scalar one does not.
    !!
    !! With the default `modify_nulls=.true.` this clears the column's null state as it goes
    !! (RF9: writing a value to a null cell makes it non-null) — the string counterpart of the
    !! bitmap kinds dropping their bitmap outright.
    module procedure set_all_str
        logical :: mod_nulls
        mod_nulls = .true.
        if (present(modify_nulls)) mod_nulls = modify_nulls
        call check_kind(self, PK_STRING, "set_all")
        call check_nrows(self, size(values, kind=int64), "set_all")
        call refill_string_store(self%str, values, self%nrows, mod_nulls)
    end procedure set_all_str
    !
    !> Replaces every value of a PK_STRING_VEC column, from a (width, nrows) array. Trailing
    !! blanks are trimmed, as in `set_all_str`.
    module procedure set_all_strv
        logical :: mod_nulls
        mod_nulls = .true.
        if (present(modify_nulls)) mod_nulls = modify_nulls
        call check_kind(self, PK_STRING_VEC, "set_all")
        call check_width(self, size(values, 1, kind=int64), "set_all")
        call check_nrows(self, size(values, 2, kind=int64), "set_all")
        ! `values` is passed to an assumed-size dummy, so sequence association flattens it in
        ! column-major order -- which IS the store's own element-major (i-1)*width + e layout
        ! (RF6/s1), so the flat index the helper walks and the index this kind uses agree.
        call refill_string_store(self%str, values, self%nrows*int(self%width, int64), mod_nulls)
    end procedure set_all_strv
    !
    !> Rebuilds a string store from a flat array of `n` elements, in ONE linear pass.
    !!
    !! **Why a rebuild rather than `n` calls to `%set`.** `parquet_string_column%set` shifts the
    !! payload tail and rewrites every later offset whenever an element's length changes, so it
    !! is O(n) per element -- and filling a column changes every element's length. Setting each
    !! element in turn is therefore O(n²): a 15.6M-row column did not finish in ten minutes.
    !! Appending into a fresh store instead is linear, because `ensure_offsets_cap`/
    !! `ensure_data_cap` grow geometrically. Do not "simplify" this back into a per-element loop
    !! over `%set` — no test will fail, the column will simply stop being fillable at scale.
    !!
    !! **Trailing blanks are trimmed, and only for array arguments.** Every element of a
    !! `character(len=*)` array shares one declared length, so a shorter value is blank-padded by
    !! Fortran and its trailing blanks carry no information the caller could have meant. A
    !! `character(len=*)` SCALAR is exactly as long as the caller wrote it, so `set_at_str` stores
    !! it verbatim. `parquet_string_column`'s own API also stores bytes verbatim by design and
    !! offers explicit `trim=`/`strip=`.
    !!
    !! `modify_nulls = .false.` preserves a null element exactly: nulls carry no payload (`set_null`
    !! shrinks the span to zero width), so re-appending a null reproduces it.
    subroutine refill_string_store(str, values, n, modify_nulls)
        type(parquet_string_column), intent(inout) :: str !! the store to refill, in place.
        character(len=*), intent(in) :: values(*)         !! `n` elements, in flat store order.
        integer(int64), intent(in) :: n                   !! elements to write.
        logical, intent(in) :: modify_nulls               !! .false. leaves null elements untouched.
        logical, allocatable :: keep(:)
        integer(int64) :: k
        !
        ! One call rather than a sizing loop plus `n` x `%append_string`. The bulk build does the
        ! same two passes this used to -- exact byte count, then fill -- but inside `parquet_strings`,
        ! where the payload copy is a section-to-section assignment between two `character(len=1)`
        ! arrays instead of a `transfer` with a temporary per element. Measured on 1M x
        ! `character(len=24)`: `%set_all` 65.2 ms -> 9.5 ms (feature_optimise_A7.md, S7-5). The
        ! trimming rule is unchanged and now lives in one place: `%build_from` trims, because an
        ! array's elements share a declared length (see this procedure's own doc-comment above).
        if (modify_nulls) then
            call str%build_from(values(1:n))
        else
            ! `modify_nulls = .false.` preserves a null element exactly: nulls carry no payload, so
            ! the mask is all the bulk build needs to reproduce them. Reading it costs `n` calls,
            ! but the previous shape paid `2n` of the same call on this path, so it is cheaper here
            ! too -- and it is the rare path, `.true.` being the default.
            allocate(keep(n))
            do k = 1_int64, n
                keep(k) = str%is_null(k)
            end do
            call str%build_from(values(1:n), is_null=keep)
        end if
    end subroutine refill_string_store
    !
    !> Relays a flat run of `n` elements into `parquet_string_column%append_values`. Its only job is
    !! the assumed-size dummy: a rank-2 `values` sequence-associates with it and is then passed on
    !! as the contiguous rank-1 section the bulk entry point takes, with no copy. `reshape` would
    !! do the same flattening by copying the whole array.
    subroutine append_flat_strings(str, values, n)
        type(parquet_string_column), intent(inout) :: str !! the store to append to.
        character(len=*), intent(in) :: values(*)         !! `n` elements, in flat store order.
        integer(int64), intent(in) :: n                   !! elements to append.
        call parquet_string_column_append_values(str, values(1:n))
    end subroutine append_flat_strings
    !
    !> Appends rows to a PK_STRING column. `values` is an ARRAY, so trailing blanks are trimmed --
    !! the same rule `refill_string_store` states, applied here so that a column filled by
    !! `%append` and one filled by `%set_all` hold the same bytes.
    module procedure append_values_str
        integer(int64) :: n
        call check_kind(self, PK_STRING, "append_values")
        n = size(values, kind=int64)
        if (n == 0_int64) return
        ! One bulk append rather than n x %append_string, for the reason refill_string_store gives.
        call parquet_string_column_append_values(self%str, values)
        self%nrows = self%nrows + n
    end procedure append_values_str
    !
    !> Appends rows to a PK_STRING_VEC column, from a (width, n) array. Trailing blanks are
    !! trimmed, as in `append_values_str`.
    module procedure append_values_strv
        integer(int64) :: n
        call check_kind(self, PK_STRING_VEC, "append_values")
        call check_width(self, size(values, 1, kind=int64), "append_values")
        n = size(values, 2, kind=int64)
        if (n == 0_int64) return
        ! A rank-2 `values` is contiguous in column-major order, which IS the store's own
        ! element-major (i-1)*width + e layout (RF6/s1) -- the same identity `set_all_strv` relies
        ! on -- so the whole (width, n) block appends as one flat run of width*n elements.
        ! It reaches the rank-1 bulk entry point through an assumed-size relay rather than
        ! `reshape`, which would copy the entire array to produce a flattening that sequence
        ! association gives for free.
        call append_flat_strings(self%str, values, size(values, kind=int64))
        self%nrows = self%nrows + n
    end procedure append_values_strv
    !
end submodule parquet_columns_string