// Grace hash join: when a join's inputs exceed the
// memory budget, both sides are partitioned by the hash of their join
// keys into per-partition spill runs (the same SpillStore the sort
// uses), and the existing in-memory join_step replays one partition at
// a time. Equal keys always land in the same partition, so a
// per-partition join is exactly the global join; NULL keys never match
// and may land anywhere. The engine still never touches the filesystem:
// partitions live in the injected SpillStore and are removed on every
// exit path.

///|
/// Total estimated byte size of a batch array.
fn batches_bytes(batches : Array[@types.DataBatch]) -> Int32 {
  let mut total = 0
  for b in batches {
    total += estimate_batch_bytes(b)
  }
  total
}

///|
fn mix32(h : Int32, v : Int32) -> Int32 {
  (h ^ v) * 16777619
}

///|
/// Partition id for row `r` of the key vectors: FNV-1a over the key
/// values (a NULL key component contributes a fixed marker), reduced
/// modulo `p`. Equal keys land in the same partition; distinct keys may
/// share one, which the per-partition join resolves.
fn key_partition(
  keys : Array[@types.ColumnVector],
  r : Int32,
  p : Int32,
) -> Int32 {
  // 2166136261 (the FNV offset basis) as a 32-bit two's complement
  let mut h = -2128831035
  for kv in keys {
    match kv.get(r) {
      @types.Null => h = mix32(h, -2128831035)
      @types.Boolean(v) => h = mix32(h, if v { 1 } else { 2 })
      @types.Int32(v) => h = mix32(h, v)
      @types.Int64(v) => h = mix32(h, v.to_int())
      @types.Float64(v) => h = mix32(h, v.to_int())
      @types.Str(s) =>
        for ch in s {
          h = mix32(h, ch.to_int())
        }
      @types.Date(v) => h = mix32(h, v)
    }
  }
  let m = h % p
  if m < 0 {
    m + p
  } else {
    m
  }
}

///|
/// Read one partition's spill runs back as batches (each spill file
/// deserializes to the batch sequence that was written into it).
fn read_partition(
  store : SpillStore,
  ids : Array[Int32],
) -> Array[@types.DataBatch] raise @types.SqlError {
  let out : Array[@types.DataBatch] = []
  for id in ids {
    let bs = @types.DataBatch::from_spill_bytes((store.read)(id))
    for b in bs {
      out.push(b)
    }
  }
  out
}

///|
/// Drop every spill file of both sides' partitions.
fn remove_join_partitions(
  store : SpillStore,
  wl : Array[RunWriter],
  wr : Array[RunWriter],
) -> Unit raise @types.SqlError {
  for w in wl {
    for id in w.ids {
      let _ = (store.remove)(id)
    }
  }
  for w in wr {
    for id in w.ids {
      let _ = (store.remove)(id)
    }
  }
}

///|
/// Route each row of `batch` into its key-hash partition writer.
fn partition_side(
  batch : @types.DataBatch,
  key_exprs : Array[PhysExpr],
  writers : Array[RunWriter],
) -> Unit raise @types.SqlError {
  let p = writers.length()
  let key_vecs : Array[@types.ColumnVector] = []
  for e in key_exprs {
    key_vecs.push(eval_vec(e, batch))
  }
  let idx : Array[Array[Int32]] = []
  for _ in 0..

0 { writers[k].push(take_rows(batch, idx[k])) } } } ///| /// Grace hash join: partition both sides by join-key hash into spill /// runs, then replay `join_step` per partition and concatenate. P is /// derived from the estimated input bytes so one partition's build side /// fits the budget; a partition that still overflows (hash skew) is a /// clean error — recursive re-partitioning is future work. fn grace_join( acc : Array[@types.DataBatch], right : Array[@types.DataBatch], step : JoinStep, budget : Int32, store : SpillStore, stats : Array[(String, Int32)], ) -> @types.DataBatch raise @types.SqlError { let total = batches_bytes(acc) + batches_bytes(right) let mut p = total * 4 / budget + 1 if p < 2 { p = 2 } if p > 64 { p = 64 } let wl : Array[RunWriter] = [] let wr : Array[RunWriter] = [] for _ in 0..

0 { stats.push(("spill partitions", p)) } errdefer remove_join_partitions(store, wl, wr) let out : Array[@types.DataBatch] = [] for k in 0..

budget || bb > budget { raise @types.SqlError::Execute( "join partition \{k} exceeds the memory budget (left \{lb} + build \{bb} bytes > \{budget}); hash skew on the join key or budget too small", ) } out.push(join_step(left_p, build_p, step)) } remove_join_partitions(store, wl, wr) concat_batches(out, step.out_dtypes) }