parquet_argsort Module

pf_argsort over plain Fortran arrays, and the sort engine underneath it.

This module is a TIER, not a convenience facade, and what it does NOT import is the point. Its Fortran use graph reaches parquet_settings_base and the intrinsic modules and nothing else -- no parquet_bindings, and so no Arrow anywhere in the graph. A project that wants an argsort, or that wants parquet_sampling's weighted draws, compiles this tier and stops there instead of compiling the whole reader/writer stack. parquet_sorting sits on top and extends pf_argsort with the five element types that need a parquet column, a packed string store or a temporal element.

The C++ engine is reached through a procedure POINTER, and that indirection is what keeps this module Arrow-free. The second, independent engine in src/parquet_wrapper.cpp exists so the tests can check this one against it. Binding it is parquet_sorting_oracle's job; a program that never imports that module never compiles it, and fpm prunes it away. See parquet_argsort_bind_oracle below.

Naming. Everything public carries the pf_ prefix (parquet-fortran) rather than parquet_, because the subject is not a parquet file -- see CLAUDE.md's "Naming conventions". The module is parquet_argsort rather than pf_argsort because a module cannot share its name with a procedure it declares.

User guide: doc/pages/utilities/sorting.md.



Variables

Type Visibility Attributes Name Initial
logical, public, save :: dbg_fortran_engine = .true.

.true. routes drive_engine to the Fortran sort.

integer, public, parameter :: SK_INT = 1

key values live in ints.

integer, public, parameter :: SK_REAL = 2

key values live in reals.

integer, public, parameter :: SK_STR = 3

key values live in offsets/data.


Interfaces

public interface pf_argsort

The permutation that would sort values: perm(k) is the index of the element that belongs at position k. values is never modified.

The permutation's integer kind is chosen by how the caller declares perm. The integer(int32) form aborts when the array is longer than huge(1_int32) rather than truncating; declare perm as integer(int64) for arrays that large.

This tier covers the six intrinsic element types.

parquet_sorting imports this generic and adds its own specifics to it, so a program with a single use parquet_sorting sees one pf_argsort covering all eleven types.

  • private interface argsort_i32_i32()

    Arguments

    None
  • private interface argsort_i32_i64()

    Arguments

    None
  • private interface argsort_i64_i32()

    Arguments

    None
  • private interface argsort_i64_i64()

    Arguments

    None
  • private interface argsort_f32_i32()

    Arguments

    None
  • private interface argsort_f32_i64()

    Arguments

    None
  • private interface argsort_f64_i32()

    Arguments

    None
  • private interface argsort_f64_i64()

    Arguments

    None
  • private interface argsort_bool_i32()

    Arguments

    None
  • private interface argsort_bool_i64()

    Arguments

    None
  • private interface argsort_chr_i32()

    Arguments

    None
  • private interface argsort_chr_i64()

    Arguments

    None

interface

  • public module function pf_sort_threads() result(n)

    Resolves how many threads a sort should use. This is the only place the auto rule lives, and the only place in this module carrying OpenMP plumbing at all -- the same arrangement parquet_tables_parallel.f90 keeps for the table layer, and worth more here, since the alternative is that plumbing repeated in 65 generated bodies. How many threads an AUTOMATIC sort -- one where threads= is absent -- would use right now: omp_get_max_threads() when the caller is not inside an OpenMP parallel region, and 1 when they are, because a nested region is the caller's business.

    Public because the read-time parquet_open_reader(..., sort_by=) has to ask the same question from a different module, and one implementation of this rule is worth more than a private copy in each -- two copies drifting would mean a raw-array sort and a read-time sort silently disagreeing about when to thread. Useful in its own right for reporting or logging what an automatic sort is about to do.

    Arguments

    None

    Return Value integer

    threads an automatic sort would use; 1 means serial.

interface

  • public module function tail_team(nthreads, n) result(team)

    Team size for a trivially parallel whole-column loop, given an already-resolved sort thread count.

    Separate from resolve_thread_count because the question is different: that one answers "how many threads may this sort use", this one answers "is this particular O(n) loop big enough to be worth a team". Both are needed -- a 64-thread sort still should not open a team to copy 500 elements.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: nthreads

    the sort's resolved thread count.

    integer(kind=int64), intent(in) :: n

    elements the loop will walk.

    Return Value integer

    team size; 1 means run it serially.

interface

  • public module subroutine extract_i32(values, buf, descending, nulls_first, proc, is_valid, threads)

    Extracts a 32-bit integer key into the canonical form the engine takes.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int32), intent(in) :: values(:)
    type(sort_key_buf), intent(out), allocatable :: buf(:)

    one entry, or two for a timestamp.

    logical, intent(in) :: descending

    .true. sorts high to low.

    logical, intent(in) :: nulls_first

    .true. places nulls before values.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    logical, intent(in), optional :: is_valid(:)

    per element: .false. marks a null.

    integer, intent(in), optional :: threads

    thread request; absent = the automatic policy.

interface

  • public module subroutine extract_i64(values, buf, descending, nulls_first, proc, is_valid, threads)

    Extracts a 64-bit integer key into the canonical form the engine takes.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: values(:)
    type(sort_key_buf), intent(out), allocatable :: buf(:)

    one entry, or two for a timestamp.

    logical, intent(in) :: descending

    .true. sorts high to low.

    logical, intent(in) :: nulls_first

    .true. places nulls before values.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    logical, intent(in), optional :: is_valid(:)

    per element: .false. marks a null.

    integer, intent(in), optional :: threads

    thread request; absent = the automatic policy.

interface

  • public module subroutine extract_f32(values, buf, descending, nulls_first, proc, is_valid, threads)

    Extracts a 32-bit real key into the canonical form the engine takes.

    Arguments

    Type IntentOptional Attributes Name
    real(kind=real32), intent(in) :: values(:)
    type(sort_key_buf), intent(out), allocatable :: buf(:)

    one entry, or two for a timestamp.

    logical, intent(in) :: descending

    .true. sorts high to low.

    logical, intent(in) :: nulls_first

    .true. places nulls before values.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    logical, intent(in), optional :: is_valid(:)

    per element: .false. marks a null.

    integer, intent(in), optional :: threads

    thread request; absent = the automatic policy.

interface

  • public module subroutine extract_f64(values, buf, descending, nulls_first, proc, is_valid, threads)

    Extracts a 64-bit real key into the canonical form the engine takes.

    Arguments

    Type IntentOptional Attributes Name
    real(kind=real64), intent(in) :: values(:)
    type(sort_key_buf), intent(out), allocatable :: buf(:)

    one entry, or two for a timestamp.

    logical, intent(in) :: descending

    .true. sorts high to low.

    logical, intent(in) :: nulls_first

    .true. places nulls before values.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    logical, intent(in), optional :: is_valid(:)

    per element: .false. marks a null.

    integer, intent(in), optional :: threads

    thread request; absent = the automatic policy.

interface

  • public module subroutine extract_bool(values, buf, descending, nulls_first, proc, is_valid, threads)

    Extracts a logical key into the canonical form the engine takes.

    Arguments

    Type IntentOptional Attributes Name
    logical, intent(in) :: values(:)
    type(sort_key_buf), intent(out), allocatable :: buf(:)

    one entry, or two for a timestamp.

    logical, intent(in) :: descending

    .true. sorts high to low.

    logical, intent(in) :: nulls_first

    .true. places nulls before values.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    logical, intent(in), optional :: is_valid(:)

    per element: .false. marks a null.

    integer, intent(in), optional :: threads

    thread request; absent = the automatic policy.

interface

  • public module subroutine extract_chr(values, buf, descending, nulls_first, proc, is_valid, threads)

    Extracts a string key into the canonical form the engine takes.

    Arguments

    Type IntentOptional Attributes Name
    character(len=*), intent(in) :: values(:)
    type(sort_key_buf), intent(out), allocatable :: buf(:)

    one entry, or two for a timestamp.

    logical, intent(in) :: descending

    .true. sorts high to low.

    logical, intent(in) :: nulls_first

    .true. places nulls before values.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    logical, intent(in), optional :: is_valid(:)

    per element: .false. marks a null.

    integer, intent(in), optional :: threads

    thread request; absent = the automatic policy.

interface

  • public module subroutine drive_engine(keys, nrows, proc, perm, threads)

    Runs the C++ engine over keys, returning a 1-based permutation.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in), target :: keys(:)

    the keys, primary first.

    integer(kind=int64), intent(in) :: nrows

    rows each key describes.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    integer(kind=int64), intent(out), allocatable :: perm(:)

    the 1-based permutation.

    integer, intent(in), optional :: threads

    thread request; absent = auto.

interface

  • public module subroutine drive_engine_grouped(keys, nrows, proc, perm, threads, group_offsets, group_ekeys)

    drive_engine, plus the group boundaries when group_offsets is asked for.

    Absent group_offsets is exactly drive_engine, one-shot fast path and all. Present, it routes through engine_build_runs instead, which always uses the builder -- so asking for boundaries costs one extra copy of a single key. That is the documented price of one entry point serving three operations rather than three of them.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in), target :: keys(:)

    the keys, primary first.

    integer(kind=int64), intent(in) :: nrows

    rows each key describes.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    integer(kind=int64), intent(out), allocatable :: perm(:)

    the 1-based permutation.

    integer, intent(in), optional :: threads

    thread request; absent = auto.

    integer(kind=int64), intent(out), optional, allocatable :: group_offsets(:)

    group bounds.

    integer, intent(in), optional :: group_ekeys

    ENGINE keys defining a group; absent = all.

interface

  • public module subroutine runs_to_offsets(tie, nrows, offsets)

    Turns engine_build_runs' tie flags into the offsets group_offsets promises: length ngroups + 1, last entry nrows + 1, so group g is perm(o(g):o(g+1)-1) for every g with no last-iteration special case.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=c_int8_t), intent(in) :: tie(:)

    1 where a row ties the previous one.

    integer(kind=int64), intent(in) :: nrows

    rows sorted; tie may be longer.

    integer(kind=int64), intent(out), allocatable :: offsets(:)

    the group offsets.

interface

  • public module subroutine resolve_thread_count(threads, nrows, count)

    Arguments

    Type IntentOptional Attributes Name
    integer, intent(in), optional :: threads

    caller's request; absent means auto.

    integer(kind=int64), intent(in) :: nrows

    rows to be sorted.

    integer(kind=int64), intent(out) :: count

    resolved count; 1 sorts serially.

interface

  • public module subroutine fill_identity(perm, n, nthreads)

    Fills perm(1:n) with 1..n, threaded when nthreads and n justify it.

    Its own procedure rather than an inline loop because it is one of the three whole-column serial loops that bound a threaded sort's end-to-end speedup, and measuring it separately is how that was found. See bench/benchmark_sort_tail.f90.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(out) :: perm(:)

    receives 1..n.

    integer(kind=int64), intent(in) :: n

    elements to fill.

    integer(kind=int64), intent(in) :: nthreads

    the sort's resolved thread count.

interface

  • public module subroutine valid_from_mask(mask, n, proc, valid)

    Builds the engine's int8 validity array from a logical mask, leaving valid UNALLOCATED when the mask marks nothing null (the engine's no-nulls fast path).

    Arguments

    Type IntentOptional Attributes Name
    logical, intent(in) :: mask(:)

    .false. marks a null.

    integer(kind=int64), intent(in) :: n

    expected length.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    integer(kind=c_int8_t), intent(out), allocatable :: valid(:)

    1 per valid element.

interface

  • public module subroutine engine_build_runs(keys, nrows, proc, perm, tie, threads, group_ekeys)

    Sorts, and reports where the runs of EQUAL rows are: tie(k) is 1 when output position k holds a row comparing equal to the one before it. One call, because pf_unique/pf_rank need both and would otherwise build the permutation twice.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in), target :: keys(:)

    the keys, primary first.

    integer(kind=int64), intent(in) :: nrows

    rows each key describes.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    integer(kind=int64), intent(out), allocatable :: perm(:)

    the 1-based permutation.

    integer(kind=c_int8_t), intent(out), allocatable :: tie(:)

    1 where a row ties the previous.

    integer, intent(in), optional :: threads

    thread request; absent = auto.

    integer, intent(in), optional :: group_ekeys

    How many LEADING keys decide whether two rows tie; absent means all of them. Counted in ENGINE keys, already resolved from the caller's key count -- the two differ because one %add of a parquet_timestamp contributes two engine keys. The sort itself always uses every key; only the tie test is narrowed.

interface

  • public module subroutine narrow_perm(perm64, proc, perm32, threads)

    Narrows a 1-based int64 permutation to int32, aborting rather than truncating.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: perm64(:)

    the permutation.

    character(len=*), intent(in) :: proc

    calling procedure, for messages.

    integer(kind=int32), intent(out), allocatable :: perm32(:)

    the narrowed copy.

    integer, intent(in), optional :: threads

    caller's team request.

interface

  • public module subroutine narrow_offsets(offsets64, proc, offsets32)

    Narrows a group-offsets array to int32, aborting rather than truncating.

    NOT the same test as narrow_perm's, and the difference is exactly one row: a permutation's largest entry is n, but this array's is the sentinel n + 1. At n == huge(int32) the permutation narrows cleanly while the sentinel wraps negative, and a negative sentinel turns the last group's o(g+1) - 1 into a huge negative bound -- a silently empty or wildly wrong slice instead of an abort. So this checks the sentinel itself rather than the length.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: offsets64(:)

    the group offsets.

    character(len=*), intent(in) :: proc

    calling procedure.

    integer(kind=int32), intent(out), allocatable :: offsets32(:)

    the narrowed copy.

interface

  • public module function sort_tier_of(key, i) result(tier)

    RAW output tier of row i under one key: values(0), NaNs(1), nulls(2).

    Absolute, and neither descending nor nulls_first reaches this. That a descending sort still puts nulls last is Arrow's own rule. nulls_first is left out for a different reason: it only ever REVERSES the tier order, so sort_compare_key applies it once by negating the tier comparison rather than having this relabel on every call — which is what keeps the whole comparator chain inside GCC's default inlining budget. Only a real-family key can be tier 1.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: key

    the bound key.

    integer(kind=int64), intent(in) :: i

    row, 1-based.

    Return Value integer

    0, 1 or 2.

interface

  • public module function sort_compare_key(key, a, b) result(c)

    -1/0/+1 for rows a and b under ONE key, with its order and null placement applied.

    Two rows in the same non-value tier (both null, or both NaN) compare EQUAL, so the caller's index tiebreaker keeps them in file order. descending negates the answer within the value tier only.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: key

    the bound key.

    integer(kind=int64), intent(in) :: a

    first row, 1-based.

    integer(kind=int64), intent(in) :: b

    second row, 1-based.

    Return Value integer

    -1, 0 or +1.

interface

  • public module function sort_row_less(keys, a, b) result(less)

    THE sort comparator: every key in precedence order, then the row index as tiebreaker.

    The index tiebreaker makes this a TOTAL ORDER in which no two distinct rows compare equal, which is what makes an unstable sort produce the stable answer, makes nth_element deterministic, and makes a parallel result bit-identical to a serial one by construction. Keep it beside sort_keys_compare -- feature_risks.md Risk-34.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: a

    first row, 1-based.

    integer(kind=int64), intent(in) :: b

    second row, 1-based.

    Return Value logical

    .true. when a sorts before b.

interface

  • public module function sort_keys_compare(keys, a, b, nkeys) result(c)

    The same ordering as sort_row_less, three-way and WITHOUT the index tiebreaker.

    Everything that must recognise "these two rows are equal" -- binary search, run detection for pf_unique/pf_rank, merging, is_sorted -- needs this one, since under the tiebreaker no two rows ever are equal. Sorting is the only caller that must NOT use it. nkeys is how many LEADING keys take part, clamped to size(keys): run detection passes a prefix because "sort by field then magnitude, but group by field alone" is one pass.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: a

    first row, 1-based.

    integer(kind=int64), intent(in) :: b

    second row, 1-based.

    integer, intent(in) :: nkeys

    leading keys taking part.

    Return Value integer

    -1, 0 or +1.

interface

  • public module function sort_counting_candidate(keys, n, lo, hi) result(ok)

    Whether the single-key integer counting sort applies, and over what value range.

    lo/hi are the key's range over its VALID rows only — a null row's value slot holds whatever the buffer contained, so including it could widen the range past the bucket limit and decline the fast path for no reason. An all-null key answers .true. with lo == hi == 0, which yields the identity permutation.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys; only a lone integer key qualifies.

    integer(kind=int64), intent(in) :: n

    rows.

    integer(kind=int64), intent(out) :: lo

    smallest valid key value, or 0.

    integer(kind=int64), intent(out) :: hi

    largest valid key value, or 0.

    Return Value logical

    .true. when the counting path applies.

interface

  • public module function sort_is_sorted(keys, n) result(answer)

    Are rows 1..n already in order under every key?

    Over sort_keys_compare, so adjacent EQUAL rows are in order — the tiebreaker would turn this into "is the row index ascending".

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: n

    rows.

    Return Value logical

    .true. when already ordered.

interface

  • public module function sort_search_position(keys, n_search, upper) result(pos)

    Binary search for the target row, which the caller APPENDED as row n_search + 1.

    Preserve the appending. It is what removes any compare-a-row-against-a-value arm and so makes drift from the sort comparator structurally impossible — Risk-34.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys; row n_search+1 is the target.

    integer(kind=int64), intent(in) :: n_search

    rows being searched.

    logical, intent(in) :: upper

    .true. for upper_bound.

    Return Value integer(kind=int64)

    1-based insertion point in 1..n_search+1.

interface

  • public module subroutine sort_comparison_permutation(keys, n, perm)

    Fills perm with the 1-based permutation that puts rows 1..n in key order.

    The serial half of the pure-Fortran engine (feature_sort.md Stage 2): an INTROSORT -- quicksort with median-of-three pivoting, a depth-limited heapsort fallback and a final insertion pass -- ordering by sort_row_less and nothing else.

    It is unstable, and that is why it is correct. sort_row_less ends with a row index tiebreaker, so no two distinct rows compare equal and every correct sorting algorithm produces the SAME permutation -- the stable one. Switching this to a merge sort to "make it stable" would buy a temporary buffer and change no answer.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: n

    rows to order.

    integer(kind=int64), intent(inout) :: perm(:)

    receives n 1-based row indices.

interface

  • public module subroutine sort_build_permutation(keys, n, perm)

    THE engine entry point: the counting fast path where it applies, the introsort otherwise.

    Mirrors the C++ sort_build_permutation exactly, including that the range scan's lo/hi are carried from the candidate test into the placement pass rather than rescanned. The two paths answer identically — the counting one is stable by construction, which is the same answer the comparator's index tiebreaker gives.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: n

    rows to order.

    integer(kind=int64), intent(inout) :: perm(:)

    receives n 1-based row indices.

interface

  • public module subroutine sort_build_permutation_threaded(keys, n, nthreads, perm)

    THE engine entry point when a thread count is available -- Stage 4.

    Answers bit-identically to sort_build_permutation at every thread count, and that is a property of the ordering rather than of the implementation: sort_row_less ends in a row-index tiebreaker, so no two distinct rows compare equal, exactly one permutation is correct, and every correct algorithm must produce it. A threading bug therefore shows up as a WRONG permutation, never as a differently-ordered valid one.

    nthreads is a resolved count, never a sentinel -- resolve_thread_count has already applied the caller's threads=, the automatic policy and the in-parallel rule. This procedure applies only the two clauses that need the DATA to decide: the row floor (an internal team-scaled rule), below which a team costs more than it saves, and one thread meaning the plain serial path. Both are observable through parquet_debug_sort_threads_used, which is the only way a test can see either.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: n

    rows to order.

    integer(kind=int64), intent(in) :: nthreads

    resolved thread count; 1 sorts serially.

    integer(kind=int64), intent(inout) :: perm(:)

    receives n 1-based row indices.

interface

  • public module subroutine sort_counting_permutation(key, n, lo, hi, perm)

    Fills perm by counting sort over lo..hi, with the nulls placed as one block.

    Two O(n) passes and no comparisons at all. Stable by construction: the placement pass walks the input in index order, so equal values are emitted in file order — the same answer sort_row_less's index tiebreaker produces.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: key

    the lone integer key.

    integer(kind=int64), intent(in) :: n

    rows.

    integer(kind=int64), intent(in) :: lo

    smallest valid key value.

    integer(kind=int64), intent(in) :: hi

    largest valid key value.

    integer(kind=int64), intent(inout) :: perm(:)

    receives n 1-based row indices.

interface

  • public module subroutine sort_partial_permutation(keys, n, count, perm)

    The first count entries of the sorted permutation, by heap selection.

    std::partial_sort's algorithm, not a full sort truncated -- a test counts comparisons to hold that apart. Everything past count in perm is untouched.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: n

    rows available.

    integer(kind=int64), intent(in) :: count

    leading entries to order.

    integer(kind=int64), intent(inout) :: perm(:)

    receives count 1-based row indices.

interface

  • public module subroutine sort_nth_index(keys, n, nth, idx)

    The row a full sort would place at 1-based rank nth, by quickselect.

    Deterministic because the comparator is a total order: there is exactly one row at that rank, so this and a full sort cannot disagree. idx is 0 for an out-of-range rank, which every caller has already rejected.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: n

    rows.

    integer(kind=int64), intent(in) :: nth

    1-based rank wanted.

    integer(kind=int64), intent(out) :: idx

    1-based row index at that rank.

interface

  • public module subroutine sort_build_runs_permutation(keys, n, group_keys, perm, tie)

    Sorts, then flags where the runs of EQUAL rows begin. tie(1) is always 0.

    group_keys is how many LEADING keys decide a tie; the sort itself always uses every key. That asymmetry is what produces "grouped by field, ordered within group".

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: n

    rows.

    integer(kind=int64), intent(in) :: group_keys

    leading keys that decide a tie.

    integer(kind=int64), intent(inout) :: perm(:)

    receives n 1-based row indices.

    integer(kind=c_int8_t), intent(inout) :: tie(:)

    1 where a row ties with its predecessor.

interface

  • public module subroutine sort_merge_permutation(keys, n, na, perm)

    Merges the already-ordered ranges 1..na and na+1..n into one permutation.

    Ties take from the FIRST range, which is std::merge's stability guarantee and what makes pf_merge agree with pf_sort of the concatenation element for element.

    Arguments

    Type IntentOptional Attributes Name
    type(sort_key_buf), intent(in) :: keys(:)

    the keys, in precedence order.

    integer(kind=int64), intent(in) :: n

    total rows across both ranges.

    integer(kind=int64), intent(in) :: na

    rows in the first range.

    integer(kind=int64), intent(inout) :: perm(:)

    receives n 1-based row indices.

interface

  • public module function parquet_debug_using_fortran_sort_engine() result(on)

    Test-only reader for which engine drive_engine would use right now.

    Arguments

    None

    Return Value logical

    .true. when the Fortran engine is selected.

interface

  • public module function parquet_debug_sort_heapsort_calls() result(n)

    Test-only count of heapsort fallbacks since parquet_debug_set_sort_depth_limit.

    What makes the forced-fallback test non-vacuous: the quicksort and heapsort paths answer identically, so only this counter can say which one ran.

    Arguments

    None

    Return Value integer(kind=int64)

    heapsort fallbacks entered.

interface

  • public module function parquet_debug_sort_max_insertion_shift() result(n)

    Test-only reader for how far the insertion pass moved anything since it was armed.

    Must not exceed SORT_INSERTION_CUTOFF after a correct sort — that is the whole invariant the quicksort exists to establish, and the only observable that a defect in the partition or the heapsort has not simply been repaired by the insertion pass.

    Arguments

    None

    Return Value integer(kind=int64)

    largest shift, in positions.

interface

  • public module function parquet_debug_sort_refine_runs() result(n)

    Test-only count of parallel refine dispatches in the last string sort; 0 = serial.

    Arguments

    None

    Return Value integer(kind=int64)

    runs refined by a team, or sub-bucket loops that opened one.

interface

  • public module function parquet_debug_sort_radix_passes() result(n)

    Test-only count of radix scatter passes executed since that reset.

    What makes a test of any pass-count optimisation non-vacuous: the constant-digit skip and the narrow-integer bias both leave the permutation bit-identical, so this is the only thing that can say whether either fired. Zero means the radix path did not run at all, which is itself worth asserting -- a floor or a decline is easy to trip by accident and looks exactly like an optimisation working perfectly.

    Arguments

    None

    Return Value integer(kind=int64)

    passes executed.

interface

  • public module function parquet_debug_sort_threads_used() result(n)

    Test-only count of threads the engine's last permutation build opened; 1 = serial.

    What makes any Stage 4 threading test non-vacuous. The permutation is bit-identical at every thread count -- the comparator is a total order, so there is exactly one correct answer -- which means no assertion on perm can distinguish a threaded run from a serial one. A policy that silently refuses to thread is therefore invisible to every other test in the suite, and is the easiest Stage 4 bug to write.

    Reports the RESOLVED count, not the team the runtime actually granted. Has no effect on, and says nothing about, the C++ engine.

    Arguments

    None

    Return Value integer(kind=int64)

    threads resolved for the last build; 1 means serial.

interface

  • public module function parquet_debug_sort_split_buckets() result(n)

    Test-only count of buckets Design B's split produced; 0 means it did not run.

    Design B and the serial LSD loop answer identically by construction, so this is the only way to tell which one ran -- and therefore the only way any test of the split, the bucket cap or the balance test can be non-vacuous. Zero is informative rather than missing: it is exactly what a declined split looks like, which is the normal outcome on a low-cardinality key.

    Arguments

    None

    Return Value integer(kind=int64)

    buckets in the last split; 0 if Design B declined.

interface

  • public module function parquet_debug_sort_design() result(n)

    Test-only report of which parallel radix design ran: 0 serial, 1 A, 2 B.

    The three answer identically by construction, so this is the only way a test of the Design A fallback can be non-vacuous -- A is reached only when B declines, and an assertion on the permutation cannot distinguish A, B and the serial loop.

    Arguments

    None

    Return Value integer(kind=int64)

    0 serial, 1 Design A, 2 Design B.

interface

  • public module subroutine parquet_debug_set_sort_depth_limit(n)

    Test-only override for the introsort's depth limit; NEGATIVE restores the computed one.

    Zero makes the very first oversized range fall back to heapsort, which is the only way to reach that arm from a test-sized fixture. Has no effect on the C++ engine. Also ZEROES the heapsort counter, so a test arms and reads in the obvious order.

    Arguments

    Type IntentOptional Attributes Name
    integer, intent(in) :: n

    forced depth limit, or a negative value to restore.

interface

  • public module subroutine parquet_debug_set_sort_track_shift(on)

    Test-only arming of the final insertion pass's largest-shift tracker, zeroing it too.

    Off by default, because armed it writes process-global state from an ordinary sort.

    Arguments

    Type IntentOptional Attributes Name
    logical, intent(in) :: on

    .true. arms the tracker.

interface

  • public module subroutine parquet_debug_set_sort_radix_min_rows(n)

    Test-only override for the radix path's row floor; NEGATIVE restores the built-in.

    Used in both directions. A huge value DECLINES the radix path, which is what keeps the introsort's and the counting path's own negative controls non-vacuous now that the floor sits below their fixture sizes. A small one drives ordinary fixtures through the radix path. Has no effect on the C++ engine.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: n

    forced floor, or a negative value to restore.

interface

  • public module subroutine parquet_debug_set_sort_task_floor(n)

    Test-only override for the balanced split's task floor; NEGATIVE restores the built-in SORT_TASK_FLOOR.

    The floor binds only when nv / team falls below it -- small n with a large team -- which no fixture in the suite reaches, so without this the constant is unexercised rather than merely untuned. Also the sweep instrument: a crossover cannot be located by rebuilding, because it sits inside this project's cross-build noise floor. Has no effect on the C++ engine.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: n

    forced floor, or a negative value to restore.

interface

  • public module subroutine parquet_debug_set_sort_tail_min_rows(n)

    Test-only override for the TAIL passes' row floor; NEGATIVE restores the built-in.

    Separate from the sort's own floor because the tail is memcpy-shaped and crosses over an order of magnitude lower; the two shared one setting until this existed, which meant one number governing two different questions. Has no effect on the C++ engine.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: n

    forced floor, or a negative value to restore.

interface

  • public module subroutine parquet_debug_set_sort_engine_min_rows(n)

    Test-only override for the Fortran ENGINE's threading floor; NEGATIVE restores it.

    The engine's floor is internal and automatic, and there is no published setting for it -- sort_parallel_min_rows was retired once this rule replaced it.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: n

    forced floor, or a negative value to restore.

interface

  • public module subroutine parquet_debug_set_sort_counting_max_threads(n)

    Test-only override for the counting path's team ceiling; NEGATIVE restores it.

    Setting it to 1 restores the pre-fix behaviour (counting serial-only), which is how the fix is A/B'd in one binary; setting it high forces counting onto teams that should decline it. Has no effect on the C++ engine, which has never gated the counting path on the team at all.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: n

    forced ceiling, or a negative value to restore.

interface

  • public module subroutine parquet_debug_set_sort_split_min_card(n)

    Test-only override for the split's minimum distinct-value count; NEGATIVE restores the built-in SORT_SPLIT_MIN_CARD.

    Selects the design at a fixed cardinality: 0 forces refined Design B onto every key, a huge value forces Design A. Both directions are needed -- one keeps Design A's own coverage non-vacuous on keys that would otherwise take the split, the other reaches the split from low-cardinality fixtures. Has no effect on the C++ engine.

    Arguments

    Type IntentOptional Attributes Name
    integer(kind=int64), intent(in) :: n

    forced cardinality floor, or negative to restore.

interface

  • public module subroutine parquet_debug_set_sort_radix_fail_alloc(which)

    Test-only forcing of an allocation failure in the radix path, to reach its fallbacks.

    Selects WHICH allocation fails, because they are in series and a single flag would make the first mask every later one: 0 none, 1 the main scratch, 2 the deep string refine's, 3 the threaded tier split's counters, 4 Design B's task arrays, and 5 to 8 the four grow_run_list makes in turn. Every fallback answers identically, so no assertion on a permutation can tell one apart from the ordinary path -- pair 1 and 2 with the insertion-shift tracker, which can.

    Arguments

    Type IntentOptional Attributes Name
    integer, intent(in) :: which

    which allocation reports failure; 0 none.

interface

  • public module subroutine parquet_debug_reset_sort_radix_passes()

    Test-only zeroing of the executed-radix-pass counter, before the sort under test.

    Arguments

    None

Derived Types

type, public ::  sort_key_buf

One extracted sort key, in the canonical form the C++ engine takes.

Read more…

Components

Type Visibility Attributes Name Initial
integer, public :: family = SK_INT

SK_INT / SK_REAL / SK_STR.

logical, public :: descending = .false.

.true. sorts high to low.

logical, public :: nulls_first = .false.

.true. places nulls before values.

integer(kind=int64), public, allocatable :: ints(:)

SK_INT values.

real(kind=real64), public, allocatable :: reals(:)

SK_REAL values.

integer(kind=int64), public, allocatable :: offsets(:)

SK_STR: n+1 byte offsets, 0-based.

character(kind=c_char, len=1), public, allocatable :: data(:)

SK_STR: the packed bytes.

integer(kind=c_int8_t), public, allocatable :: valid(:)

1 = valid; UNALLOCATED means no nulls.


Subroutines

public subroutine parquet_argsort_bind_oracle(argsort_p, partial_p, nth_p, is_sorted_p, runs_p, search_p, merge_p)

Binds the seven C++-engine entry points. Called by parquet_sorting_oracle, and by nothing else; idempotent, so calling it on every engine selection costs nothing.

Arguments

Type IntentOptional Attributes Name
procedure(oracle_argsort_i) :: argsort_p

the whole-permutation entry point.

procedure(oracle_partial_i) :: partial_p

the partial sort.

procedure(oracle_nth_i) :: nth_p

nth-element selection.

procedure(oracle_is_sorted_i) :: is_sorted_p

the already-sorted test.

procedure(oracle_runs_i) :: runs_p

the grouped sort.

procedure(oracle_search_i) :: search_p

the binary search.

procedure(oracle_merge_i) :: merge_p

the merge.

public subroutine parquet_argsort_select_engine(use_fortran)

Selects which engine the dispatchers run. TEST-ONLY, and reached only through parquet_sorting_oracle's parquet_debug_use_fortran_sort_engine.

Arguments

Type IntentOptional Attributes Name
logical, intent(in) :: use_fortran

.true. selects the Fortran engine.

public subroutine oracle_argsort(keys, nrows, nthreads, proc, perm)

Relays onto the oracle's procedure pointers, and they exist for a COMPILER reason rather than a design one -- do not inline them back into the callers.

Read more…

Arguments

Type IntentOptional Attributes Name
type(sort_key_buf), intent(in), target :: keys(:)

the keys, primary first.

integer(kind=int64), intent(in) :: nrows

rows each key describes.

integer(kind=int64), intent(in) :: nthreads

resolved thread count.

character(len=*), intent(in) :: proc

calling procedure, for messages.

integer(kind=int64), intent(inout) :: perm(:)

the permutation to fill.

public subroutine oracle_partial(keys, nrows, count, proc, perm)

Relay onto p_partial; see oracle_argsort for why these exist.

Arguments

Type IntentOptional Attributes Name
type(sort_key_buf), intent(in), target :: keys(:)

the keys, primary first.

integer(kind=int64), intent(in) :: nrows

rows each key describes.

integer(kind=int64), intent(in) :: count

leading rows to order.

character(len=*), intent(in) :: proc

calling procedure, for messages.

integer(kind=int64), intent(inout) :: perm(:)

the permutation to fill.

public subroutine oracle_nth(keys, nrows, nth, proc, idx)

Relay onto p_nth; see oracle_argsort for why these exist.

Arguments

Type IntentOptional Attributes Name
type(sort_key_buf), intent(in), target :: keys(:)

the keys, primary first.

integer(kind=int64), intent(in) :: nrows

rows each key describes.

integer(kind=int64), intent(in) :: nth

the rank wanted, 1-based.

character(len=*), intent(in) :: proc

calling procedure, for messages.

integer(kind=int64), intent(out) :: idx

the row holding that rank.

public subroutine oracle_is_sorted(keys, nrows, proc, answer)

Relay onto p_is_sorted; see oracle_argsort for why these exist.

Arguments

Type IntentOptional Attributes Name
type(sort_key_buf), intent(in), target :: keys(:)

the keys, primary first.

integer(kind=int64), intent(in) :: nrows

rows each key describes.

character(len=*), intent(in) :: proc

calling procedure, for messages.

logical, intent(out) :: answer

whether the rows are ordered.

public subroutine oracle_runs(keys, nrows, nthreads, gek, proc, perm, tie)

Relay onto p_runs; see oracle_argsort for why these exist.

Arguments

Type IntentOptional Attributes Name
type(sort_key_buf), intent(in), target :: keys(:)

the keys, primary first.

integer(kind=int64), intent(in) :: nrows

rows each key describes.

integer(kind=int64), intent(in) :: nthreads

resolved thread count.

integer(kind=int64), intent(in) :: gek

engine keys defining a group.

character(len=*), intent(in) :: proc

calling procedure, for messages.

integer(kind=int64), intent(inout) :: perm(:)

the permutation to fill.

integer(kind=c_int8_t), intent(inout) :: tie(:)

1 where a row ties the previous.

public subroutine oracle_search(keys, nrows, n_search, upper, proc, pos)

Relay onto p_search; see oracle_argsort for why these exist.

Arguments

Type IntentOptional Attributes Name
type(sort_key_buf), intent(in), target :: keys(:)

the keys, primary first.

integer(kind=int64), intent(in) :: nrows

rows each key describes.

integer(kind=int64), intent(in) :: n_search

rows belonging to the haystack.

logical, intent(in) :: upper

upper rather than lower bound.

character(len=*), intent(in) :: proc

calling procedure, for messages.

integer(kind=int64), intent(out) :: pos

the insertion position found.

public subroutine oracle_merge(keys, nrows, na, proc, perm)

Relay onto p_merge; see oracle_argsort for why these exist.

Arguments

Type IntentOptional Attributes Name
type(sort_key_buf), intent(in), target :: keys(:)

the keys, primary first.

integer(kind=int64), intent(in) :: nrows

rows each key describes.

integer(kind=int64), intent(in) :: na

rows belonging to the first input.

character(len=*), intent(in) :: proc

calling procedure, for messages.

integer(kind=int64), intent(inout) :: perm(:)

the permutation to fill.

public subroutine check_oracle(bound, proc)

Aborts if the C++ engine was selected without being bound.

Read more…

Arguments

Type IntentOptional Attributes Name
logical, intent(in) :: bound

whether the entry point is associated.

character(len=*), intent(in) :: proc

calling procedure, for the message.