tranfi 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/LICENSE +177 -0
  2. package/NOTICE +8 -0
  3. package/README.md +443 -51
  4. package/app/assets/{index-pDFMluyz.js → index-BIAIKnrp.js} +1 -1
  5. package/app/index.html +1 -1
  6. package/binding.gyp +55 -3
  7. package/csrc/arena.c +7 -5
  8. package/csrc/batch.c +818 -71
  9. package/csrc/buffer.c +84 -8
  10. package/csrc/cJSON.c +262 -19
  11. package/csrc/cJSON.h +17 -1
  12. package/csrc/codec_csv.c +1074 -181
  13. package/csrc/codec_jsonl.c +830 -118
  14. package/csrc/codec_table.c +108 -78
  15. package/csrc/codec_text.c +286 -68
  16. package/csrc/compiler.c +31 -3
  17. package/csrc/config.h +21 -0
  18. package/csrc/dsl.c +4722 -485
  19. package/csrc/expr.c +363 -55
  20. package/csrc/expr.h +2 -0
  21. package/csrc/internal.h +316 -27
  22. package/csrc/ir.c +65 -18
  23. package/csrc/ir.h +41 -0
  24. package/csrc/ir_schema.c +20 -5
  25. package/csrc/ir_serialize.c +68 -6
  26. package/csrc/ir_sql.c +796 -185
  27. package/csrc/ir_validate.c +462 -6
  28. package/csrc/json_path.c +210 -0
  29. package/csrc/main.c +879 -30
  30. package/csrc/memory_estimate.c +477 -0
  31. package/csrc/op_acf.c +171 -21
  32. package/csrc/op_across.c +477 -0
  33. package/csrc/op_anomaly.c +167 -32
  34. package/csrc/op_assert.c +761 -0
  35. package/csrc/op_bin.c +168 -29
  36. package/csrc/op_cast.c +383 -55
  37. package/csrc/op_clip.c +30 -19
  38. package/csrc/op_date_trunc.c +208 -34
  39. package/csrc/op_datetime.c +259 -77
  40. package/csrc/op_derive.c +65 -97
  41. package/csrc/op_diff.c +146 -30
  42. package/csrc/op_ewma.c +149 -30
  43. package/csrc/op_explode.c +124 -26
  44. package/csrc/op_fill_down.c +125 -53
  45. package/csrc/op_fill_null.c +176 -31
  46. package/csrc/op_filter.c +89 -40
  47. package/csrc/op_frequency.c +571 -43
  48. package/csrc/op_grep.c +36 -18
  49. package/csrc/op_group_agg.c +1790 -119
  50. package/csrc/op_hash.c +48 -15
  51. package/csrc/op_head.c +21 -86
  52. package/csrc/op_interpolate.c +268 -62
  53. package/csrc/op_join.c +2700 -182
  54. package/csrc/op_json_extract.c +227 -0
  55. package/csrc/op_json_filter.c +384 -0
  56. package/csrc/op_json_flatten.c +293 -0
  57. package/csrc/op_json_schema.c +503 -0
  58. package/csrc/op_label_encode.c +328 -53
  59. package/csrc/op_lag.c +181 -0
  60. package/csrc/op_lead.c +141 -89
  61. package/csrc/op_normalize.c +363 -79
  62. package/csrc/op_onehot.c +345 -73
  63. package/csrc/op_pivot.c +1546 -162
  64. package/csrc/op_quarantine.c +189 -0
  65. package/csrc/op_registry.c +2062 -166
  66. package/csrc/op_rename.c +41 -50
  67. package/csrc/op_replace.c +270 -118
  68. package/csrc/op_rleid.c +297 -0
  69. package/csrc/op_rowid.c +559 -0
  70. package/csrc/op_sample.c +80 -23
  71. package/csrc/op_schema.c +1341 -0
  72. package/csrc/op_schema_infer.c +252 -0
  73. package/csrc/op_select.c +265 -65
  74. package/csrc/op_set.c +3449 -0
  75. package/csrc/op_skip.c +30 -87
  76. package/csrc/op_sort.c +670 -124
  77. package/csrc/op_source_name.c +120 -0
  78. package/csrc/op_split.c +65 -28
  79. package/csrc/op_split_data.c +41 -9
  80. package/csrc/op_stack.c +178 -222
  81. package/csrc/op_stats.c +206 -110
  82. package/csrc/op_step.c +217 -55
  83. package/csrc/op_tail.c +21 -12
  84. package/csrc/op_tee.c +338 -0
  85. package/csrc/op_top.c +260 -53
  86. package/csrc/op_trim.c +48 -19
  87. package/csrc/op_unique.c +1193 -150
  88. package/csrc/op_unpivot.c +100 -66
  89. package/csrc/op_validate.c +601 -24
  90. package/csrc/op_window.c +492 -51
  91. package/csrc/path_policy.c +85 -0
  92. package/csrc/pipeline.c +872 -99
  93. package/csrc/recipes.c +3 -1
  94. package/csrc/report.c +73 -30
  95. package/csrc/selector.c +1097 -0
  96. package/csrc/size_utils.c +352 -0
  97. package/csrc/spill.c +317 -0
  98. package/csrc/spill.h +21 -0
  99. package/csrc/tranfi.h +169 -1
  100. package/csrc/transform.h +209 -0
  101. package/csrc/transform_api.c +2237 -0
  102. package/csrc/transform_categorical.c +923 -0
  103. package/csrc/transform_internal.h +472 -0
  104. package/csrc/transform_json.c +3812 -0
  105. package/csrc/transform_numeric.c +1966 -0
  106. package/csrc/transform_sha256.c +154 -0
  107. package/csrc/transform_wasm.h +162 -0
  108. package/csrc/transform_wasm_api.c +1373 -0
  109. package/csrc/wasm_api.c +70 -9
  110. package/napi_api.c +219 -11
  111. package/napi_transform.c +1648 -0
  112. package/napi_transform.h +8 -0
  113. package/package.json +27 -11
  114. package/scripts/install-native.js +76 -0
  115. package/scripts/prepack.js +64 -0
  116. package/scripts/sync-csrc.js +23 -0
  117. package/src/cli.js +81 -41
  118. package/src/engines/duckdb.js +45 -12
  119. package/src/index.js +661 -42
  120. package/src/memory_policy.js +411 -0
  121. package/src/native.js +1 -5
  122. package/src/pipeline.js +454 -31
  123. package/src/recipe_json.js +80 -0
  124. package/src/server.js +10 -8
  125. package/src/transform.js +403 -0
  126. package/src/transform_error.js +10 -0
  127. package/src/wasm.js +6 -4
  128. package/wasm/index.js +498 -10
  129. package/wasm/tranfi_core.js +0 -0
  130. package/wasm/transform.js +1156 -0
  131. package/wasm/worker.js +786 -0
  132. package/csrc/plan.c +0 -206
package/csrc/op_join.c CHANGED
@@ -1,19 +1,33 @@
1
1
  /*
2
2
  * op_join.c — Hash join with lookup CSV file.
3
3
  *
4
- * Config: {"file": "lookup.csv", "on": "id" or "left=right", "how": "inner|left"}
5
- * Loads lookup file into memory once, then streams main data through with
6
- * hash map probes. Supports inner and left join.
4
+ * Config: {"file": "lookup.csv", "on": "id" or "left=right",
5
+ * "how": "inner|left|semi|anti", "max_lookup_bytes": 1048576,
6
+ * "max_lookup_rows": 100000, "max_lookup_keys": 100000,
7
+ * "max_state_bytes": 8388608,
8
+ * "max_matches_per_row": 10, "max_output_rows": 1000000}
9
+ * Loads a capped lookup file once, then streams main data through hash probes.
10
+ * Inner/left joins append lookup columns. Semi/anti joins keep only left rows
11
+ * and never duplicate them.
7
12
  */
8
13
 
9
14
  #include "internal.h"
15
+ #include "spill.h"
10
16
  #include "date_utils.h"
11
17
  #include "cJSON.h"
12
18
  #include <stdlib.h>
13
19
  #include <string.h>
14
20
  #include <stdio.h>
21
+ #include <limits.h>
22
+ #include <errno.h>
23
+ #include <stdint.h>
24
+ #include <unistd.h>
15
25
 
16
- /* Hash map bucket: key → array of row indices in lookup batch */
26
+ #define JOIN_DEFAULT_RUN_ROWS 8192
27
+ #define JOIN_DEFAULT_OUTPUT_ROWS 1024
28
+ #define JOIN_MIN_RUN_ROWS 16
29
+
30
+ /* Hash map bucket: key -> array of row indices in lookup batch */
17
31
  typedef struct {
18
32
  char *key;
19
33
  size_t *rows;
@@ -25,25 +39,184 @@ typedef struct {
25
39
  join_bucket *buckets;
26
40
  size_t n_buckets; /* power of 2 */
27
41
  size_t count;
42
+ size_t key_bytes;
43
+ size_t row_ref_capacity;
28
44
  } join_hash_map;
29
45
 
46
+ typedef tf_owned_cell_value join_key_data;
47
+
48
+ typedef struct {
49
+ int valid;
50
+ int is_null;
51
+ tf_type type;
52
+ join_key_data v;
53
+ } join_key_value;
54
+
55
+
56
+ typedef struct {
57
+ uint64_t ordinal;
58
+ uint8_t *nulls;
59
+ join_key_data *cells;
60
+ } join_spill_row;
61
+
62
+ typedef struct {
63
+ FILE *file;
64
+ join_spill_row row;
65
+ int has_row;
66
+ int done;
67
+ } join_spill_reader;
68
+
30
69
  typedef struct {
31
70
  char *file;
71
+ char *validated_file;
32
72
  char *left_col;
33
73
  char *right_col;
34
- int how; /* 0=inner, 1=left */
74
+ int how; /* 0=inner, 1=left, 2=semi, 3=anti */
75
+ int sorted; /* lookup and input are sorted by join key */
76
+ size_t max_lookup_rows; /* 0 = unlimited */
77
+ size_t max_lookup_keys; /* 0 = unlimited */
78
+ size_t max_lookup_bytes; /* 0 = unlimited */
79
+ size_t max_state_bytes; /* 0 = unlimited */
80
+ size_t max_matches_per_row; /* 0 = unlimited */
81
+ size_t max_output_rows; /* 0 = unlimited */
82
+ size_t output_rows; /* emitted rows across process calls */
83
+ size_t lookup_rows; /* decoded lookup rows retained or scanned */
84
+
85
+ int use_spill; /* exact external semi/anti filtering join */
86
+ char *spill_dir;
87
+ tf_spill_session *spill;
88
+ size_t spill_memory_bytes;
89
+ size_t configured_run_rows;
90
+ size_t run_rows;
91
+ size_t output_batch_rows;
92
+
93
+ int spill_has_schema;
94
+ char **spill_schema_names;
95
+ tf_type *spill_schema_types;
96
+ size_t spill_n_cols;
97
+ int spill_left_join_col;
98
+
99
+ char **spill_lookup_schema_names;
100
+ tf_type *spill_lookup_schema_types;
101
+ size_t spill_lookup_n_cols;
102
+ int spill_lookup_join_col;
103
+ char **spill_output_schema_names;
104
+ tf_type *spill_output_schema_types;
105
+ size_t spill_output_n_cols;
106
+
107
+ tf_batch *spill_left_buf;
108
+ tf_batch *spill_lookup_buf;
109
+ tf_batch *spill_out_buf;
110
+ uint64_t *spill_left_ordinals;
111
+ uint64_t *spill_lookup_ordinals;
112
+ uint64_t *spill_out_ordinals;
113
+ size_t spill_left_ordinal_cap;
114
+ size_t spill_lookup_ordinal_cap;
115
+ size_t spill_out_ordinal_cap;
116
+ uint64_t spill_next_left_ordinal;
117
+ uint64_t spill_next_lookup_ordinal;
118
+
119
+ char **spill_left_run_paths;
120
+ size_t spill_n_left_runs;
121
+ size_t spill_cap_left_runs;
122
+ char **spill_lookup_run_paths;
123
+ size_t spill_n_lookup_runs;
124
+ size_t spill_cap_lookup_runs;
125
+ char **spill_out_run_paths;
126
+ size_t spill_n_out_runs;
127
+ size_t spill_cap_out_runs;
128
+ size_t spill_left_run_seq;
129
+ size_t spill_lookup_run_seq;
130
+ size_t spill_out_run_seq;
131
+
132
+ join_spill_reader *spill_left_readers;
133
+ size_t spill_n_left_readers;
134
+ join_spill_reader *spill_lookup_readers;
135
+ size_t spill_n_lookup_readers;
136
+ join_spill_reader *spill_out_readers;
137
+ size_t spill_n_out_readers;
138
+
139
+ int spill_lookup_loaded;
140
+ int spill_key_merge_done;
141
+ int spill_output_merge_started;
142
+ int spill_output_merge_done;
143
+ char *spill_last_lookup_key;
144
+
145
+ size_t spill_bytes;
146
+ size_t spill_runs;
147
+ size_t spill_output_batches;
148
+ size_t spill_output_rows;
149
+ size_t spill_kept_rows;
150
+ size_t spill_lookup_keys;
151
+ size_t spill_lookup_key_bytes;
35
152
 
36
153
  tf_batch *lookup;
37
154
  int loaded;
38
155
  int lookup_join_col;
156
+ tf_type lookup_key_type;
157
+ int have_lookup_key_type;
39
158
 
40
159
  /* Lookup columns to include in output (excluding join key) */
41
160
  int *lookup_out_cols;
42
161
  size_t n_lookup_out;
43
162
 
44
163
  join_hash_map map;
164
+
165
+ FILE *sorted_file;
166
+ tf_decoder *sorted_decoder;
167
+ tf_batch **sorted_batches;
168
+ size_t sorted_n_batches;
169
+ size_t sorted_batch_index;
170
+ tf_batch *sorted_current;
171
+ size_t sorted_row;
172
+ int sorted_flushed;
173
+ int sorted_have_row;
174
+ int sorted_exhausted;
175
+ join_key_value prev_lookup_key;
176
+ int have_prev_lookup_key;
177
+ join_key_value prev_left_key;
178
+ int have_prev_left_key;
179
+ tf_batch *right_run;
180
+ join_key_value right_run_key;
181
+ int have_right_run;
182
+ char **lookup_schema_names;
183
+ tf_type *lookup_schema_types;
184
+ size_t n_lookup_schema_cols;
45
185
  } join_state;
46
186
 
187
+ static size_t join_batch_storage_bytes(const tf_batch *b);
188
+ static size_t join_hash_map_retained_bytes(const join_hash_map *map);
189
+ static size_t join_unsorted_retained_state_bytes(const join_hash_map *map,
190
+ const tf_batch *lookup,
191
+ size_t n_lookup_out);
192
+ static int join_check_unsorted_state_bytes(const join_state *st,
193
+ const join_hash_map *map,
194
+ const tf_batch *lookup,
195
+ size_t n_lookup_out,
196
+ tf_side_channels *side);
197
+ static int join_reserve_output_row(join_state *st, tf_side_channels *side);
198
+ static size_t join_spill_retained_state_bytes(const join_state *st);
199
+
200
+ TF_WARN_UNUSED static int join_write_error(tf_side_channels *side, const char *msg) {
201
+ return tf_side_write_error(side, msg);
202
+ }
203
+
204
+ TF_WARN_UNUSED static int join_limit_error_context(tf_side_channels *side, const char *field,
205
+ size_t limit, size_t actual,
206
+ const char *context) {
207
+ char msg[224];
208
+ snprintf(msg, sizeof(msg),
209
+ "join: %s=%zu exceeded %s (%zu)",
210
+ field, limit, context, actual);
211
+ return join_write_error(side, msg);
212
+ }
213
+
214
+ TF_WARN_UNUSED static int join_limit_error(tf_side_channels *side, const char *field,
215
+ size_t limit, size_t actual) {
216
+ return join_limit_error_context(side, field, limit, actual,
217
+ "while loading lookup side");
218
+ }
219
+
47
220
  /* FNV-1a hash */
48
221
  static uint64_t fnv1a(const char *s) {
49
222
  uint64_t h = 14695981039346656037ULL;
@@ -51,39 +224,111 @@ static uint64_t fnv1a(const char *s) {
51
224
  return h;
52
225
  }
53
226
 
54
- static void map_init(join_hash_map *m, size_t hint) {
227
+ static int map_init(join_hash_map *m, size_t hint) {
55
228
  size_t n = 64;
56
- while (n < hint * 2) n *= 2;
57
- m->buckets = calloc(n, sizeof(join_bucket));
229
+ size_t target = 0;
230
+ if (tf_size_mul(hint, 2, &target) != TF_OK) return -1;
231
+ while (n < target) {
232
+ if (n > (SIZE_MAX / 2)) return -1;
233
+ n *= 2;
234
+ }
235
+ m->buckets = tf_callocarray_checked(n, sizeof(join_bucket));
236
+ if (!m->buckets) return -1;
58
237
  m->n_buckets = n;
59
238
  m->count = 0;
239
+ m->key_bytes = 0;
240
+ m->row_ref_capacity = 0;
241
+ return 0;
60
242
  }
61
243
 
62
- static void map_insert(join_hash_map *m, const char *key, size_t row) {
244
+ static int map_insert(join_state *st, join_hash_map *m,
245
+ const char *key, size_t row,
246
+ tf_side_channels *side) {
63
247
  uint64_t h = fnv1a(key);
64
248
  size_t idx = h & (m->n_buckets - 1);
65
249
  /* Linear probe */
66
250
  while (m->buckets[idx].key) {
67
251
  if (strcmp(m->buckets[idx].key, key) == 0) {
252
+ /* Filtering joins only need key presence, not duplicate right rows. */
253
+ if (st->how >= 2) return 0;
254
+
68
255
  /* Add row to existing bucket */
69
256
  join_bucket *b = &m->buckets[idx];
257
+ if (st->max_matches_per_row > 0 && b->n_rows >= st->max_matches_per_row) {
258
+ if (join_limit_error_context(side, "max_matches_per_row",
259
+ st->max_matches_per_row, b->n_rows + 1,
260
+ "for lookup key") != TF_OK)
261
+ return -1;
262
+ return -1;
263
+ }
70
264
  if (b->n_rows >= b->rows_cap) {
71
- b->rows_cap = b->rows_cap ? b->rows_cap * 2 : 4;
72
- b->rows = realloc(b->rows, b->rows_cap * sizeof(size_t));
265
+ size_t old_cap = b->rows_cap;
266
+ size_t need = 0;
267
+ size_t new_cap = 0;
268
+ size_t new_row_ref_capacity = 0;
269
+ if (tf_size_add(b->n_rows, 1, &need) != TF_OK ||
270
+ tf_size_grow_pow2(b->rows_cap, need, 4, &new_cap) != TF_OK ||
271
+ tf_size_add(m->row_ref_capacity, new_cap - old_cap,
272
+ &new_row_ref_capacity) != TF_OK) {
273
+ return -1;
274
+ }
275
+ size_t *tmp = tf_reallocarray_checked(b->rows, new_cap, sizeof(size_t));
276
+ if (!tmp) return -1;
277
+ b->rows = tmp;
278
+ b->rows_cap = new_cap;
279
+ m->row_ref_capacity = new_row_ref_capacity;
73
280
  }
74
281
  b->rows[b->n_rows++] = row;
75
- return;
282
+ return 0;
76
283
  }
77
284
  idx = (idx + 1) & (m->n_buckets - 1);
78
285
  }
286
+
287
+ if (st->max_lookup_keys > 0 && m->count >= st->max_lookup_keys) {
288
+ if (join_limit_error(side, "max_lookup_keys", st->max_lookup_keys,
289
+ m->count + 1) != TF_OK)
290
+ return -1;
291
+ return -1;
292
+ }
293
+
79
294
  /* New key */
80
295
  join_bucket *b = &m->buckets[idx];
81
- b->key = strdup(key);
82
- b->rows = malloc(4 * sizeof(size_t));
83
- b->rows_cap = 4;
84
- b->rows[0] = row;
85
- b->n_rows = 1;
296
+ char *key_copy = strdup(key);
297
+ if (!key_copy) return -1;
298
+ size_t key_bytes_delta = 0;
299
+ size_t new_key_bytes = 0;
300
+ if (tf_size_add(strlen(key_copy), 1, &key_bytes_delta) != TF_OK ||
301
+ tf_size_add(m->key_bytes, key_bytes_delta, &new_key_bytes) != TF_OK) {
302
+ free(key_copy);
303
+ return -1;
304
+ }
305
+ size_t *rows = NULL;
306
+ size_t rows_cap = 0;
307
+ size_t n_rows = 0;
308
+ size_t new_row_ref_capacity = m->row_ref_capacity;
309
+ if (st->how >= 2) {
310
+ rows = NULL;
311
+ rows_cap = 0;
312
+ n_rows = 0;
313
+ } else {
314
+ if (tf_size_add(m->row_ref_capacity, 4, &new_row_ref_capacity) != TF_OK) {
315
+ free(key_copy);
316
+ return -1;
317
+ }
318
+ rows = tf_mallocarray_checked(4, sizeof(size_t));
319
+ if (!rows) { free(key_copy); return -1; }
320
+ rows_cap = 4;
321
+ rows[0] = row;
322
+ n_rows = 1;
323
+ }
324
+ b->key = key_copy;
325
+ b->rows = rows;
326
+ b->rows_cap = rows_cap;
327
+ b->n_rows = n_rows;
328
+ m->key_bytes = new_key_bytes;
329
+ m->row_ref_capacity = new_row_ref_capacity;
86
330
  m->count++;
331
+ return 0;
87
332
  }
88
333
 
89
334
  static join_bucket *map_find(join_hash_map *m, const char *key) {
@@ -108,256 +353,2442 @@ static void map_free(join_hash_map *m) {
108
353
  }
109
354
  free(m->buckets);
110
355
  m->buckets = NULL;
356
+ m->n_buckets = 0;
357
+ m->count = 0;
358
+ m->key_bytes = 0;
359
+ m->row_ref_capacity = 0;
111
360
  }
112
361
 
113
- /* Format a cell as string for join key comparison */
362
+ /* Serialize a typed join key. Tags avoid collisions such as int 1 vs string "1"
363
+ * and null vs a literal sentinel string. */
114
364
  static char *format_join_key(const tf_batch *b, size_t row, int col) {
115
- if (tf_batch_is_null(b, row, col)) return strdup("\\N");
116
- char buf[64];
117
- switch (b->col_types[col]) {
118
- case TF_TYPE_STRING: return strdup(tf_batch_get_string(b, row, col));
365
+ char buf[96];
366
+ tf_type type = b->col_types[col];
367
+ if (tf_batch_is_null(b, row, col)) {
368
+ snprintf(buf, sizeof(buf), "N:%d", (int)type);
369
+ return strdup(buf);
370
+ }
371
+
372
+ switch (type) {
373
+ case TF_TYPE_STRING: {
374
+ const char *v = tf_batch_get_string(b, row, col);
375
+ size_t len = v ? strlen(v) : 0;
376
+ char prefix[48];
377
+ int n = snprintf(prefix, sizeof(prefix), "S:%zu:", len);
378
+ if (n < 0) return NULL;
379
+ size_t prefix_len = (size_t)n;
380
+ char *out = malloc(prefix_len + len + 1);
381
+ if (!out) return NULL;
382
+ memcpy(out, prefix, prefix_len);
383
+ if (len) memcpy(out + prefix_len, v, len);
384
+ out[prefix_len + len] = '\0';
385
+ return out;
386
+ }
119
387
  case TF_TYPE_INT64:
120
- snprintf(buf, sizeof(buf), "%lld", (long long)tf_batch_get_int64(b, row, col));
388
+ snprintf(buf, sizeof(buf), "I:%lld", (long long)tf_batch_get_int64(b, row, col));
121
389
  return strdup(buf);
122
390
  case TF_TYPE_FLOAT64:
123
- snprintf(buf, sizeof(buf), "%.17g", tf_batch_get_float64(b, row, col));
391
+ snprintf(buf, sizeof(buf), "F:%.17g", tf_batch_get_float64(b, row, col));
124
392
  return strdup(buf);
125
393
  case TF_TYPE_BOOL:
126
- return strdup(tf_batch_get_bool(b, row, col) ? "true" : "false");
394
+ return strdup(tf_batch_get_bool(b, row, col) ? "B:1" : "B:0");
127
395
  case TF_TYPE_DATE:
128
- snprintf(buf, sizeof(buf), "%d", (int)tf_batch_get_date(b, row, col));
396
+ snprintf(buf, sizeof(buf), "D:%d", (int)tf_batch_get_date(b, row, col));
129
397
  return strdup(buf);
130
398
  case TF_TYPE_TIMESTAMP:
131
- snprintf(buf, sizeof(buf), "%lld", (long long)tf_batch_get_timestamp(b, row, col));
399
+ snprintf(buf, sizeof(buf), "T:%lld", (long long)tf_batch_get_timestamp(b, row, col));
400
+ return strdup(buf);
401
+ default:
402
+ snprintf(buf, sizeof(buf), "U:%d", (int)type);
132
403
  return strdup(buf);
404
+ }
405
+ }
406
+
407
+ static void join_key_value_clear(join_key_value *v) {
408
+ if (!v) return;
409
+ if (v->valid && !v->is_null && v->type == TF_TYPE_STRING) free(v->v.str);
410
+ memset(v, 0, sizeof(*v));
411
+ }
412
+
413
+ static int join_key_value_set(join_key_value *dst, const tf_batch *b, size_t row, int col) {
414
+ if (!dst || !b || col < 0) return TF_ERROR;
415
+ join_key_value_clear(dst);
416
+ dst->valid = 1;
417
+ dst->type = b->col_types[col];
418
+ dst->is_null = tf_batch_is_null(b, row, col) ? 1 : 0;
419
+ if (dst->is_null) return TF_OK;
420
+
421
+ switch (dst->type) {
422
+ case TF_TYPE_BOOL:
423
+ dst->v.b = tf_batch_get_bool(b, row, col) ? 1 : 0;
424
+ return TF_OK;
425
+ case TF_TYPE_INT64:
426
+ dst->v.i64 = tf_batch_get_int64(b, row, col);
427
+ return TF_OK;
428
+ case TF_TYPE_FLOAT64:
429
+ dst->v.f64 = tf_batch_get_float64(b, row, col);
430
+ return TF_OK;
431
+ case TF_TYPE_STRING: {
432
+ const char *s = tf_batch_get_string(b, row, col);
433
+ dst->v.str = strdup(s ? s : "");
434
+ return dst->v.str ? TF_OK : TF_ERROR;
435
+ }
436
+ case TF_TYPE_DATE:
437
+ dst->v.date = tf_batch_get_date(b, row, col);
438
+ return TF_OK;
439
+ case TF_TYPE_TIMESTAMP:
440
+ dst->v.i64 = tf_batch_get_timestamp(b, row, col);
441
+ return TF_OK;
442
+ default:
443
+ dst->is_null = 1;
444
+ return TF_OK;
445
+ }
446
+ }
447
+
448
+ static int join_key_value_copy(join_key_value *dst, const join_key_value *src) {
449
+ if (!dst || !src || !src->valid) return TF_ERROR;
450
+ join_key_value_clear(dst);
451
+ *dst = *src;
452
+ if (!src->is_null && src->type == TF_TYPE_STRING) {
453
+ dst->v.str = strdup(src->v.str ? src->v.str : "");
454
+ if (!dst->v.str) { memset(dst, 0, sizeof(*dst)); return TF_ERROR; }
455
+ }
456
+ return TF_OK;
457
+ }
458
+
459
+ static int join_key_compare_values(const join_key_value *a, const join_key_value *b, int *cmp) {
460
+ if (!a || !b || !cmp || !a->valid || !b->valid) return TF_ERROR;
461
+ if (a->type != b->type) return TF_ERROR;
462
+ if (a->is_null && b->is_null) { *cmp = 0; return TF_OK; }
463
+ if (a->is_null) { *cmp = 1; return TF_OK; }
464
+ if (b->is_null) { *cmp = -1; return TF_OK; }
465
+
466
+ switch (a->type) {
467
+ case TF_TYPE_BOOL:
468
+ *cmp = (int)a->v.b - (int)b->v.b;
469
+ return TF_OK;
470
+ case TF_TYPE_INT64:
471
+ case TF_TYPE_TIMESTAMP:
472
+ *cmp = (a->v.i64 > b->v.i64) - (a->v.i64 < b->v.i64);
473
+ return TF_OK;
474
+ case TF_TYPE_FLOAT64:
475
+ *cmp = (a->v.f64 > b->v.f64) - (a->v.f64 < b->v.f64);
476
+ return TF_OK;
477
+ case TF_TYPE_STRING:
478
+ *cmp = strcmp(a->v.str ? a->v.str : "", b->v.str ? b->v.str : "");
479
+ return TF_OK;
480
+ case TF_TYPE_DATE:
481
+ *cmp = (a->v.date > b->v.date) - (a->v.date < b->v.date);
482
+ return TF_OK;
133
483
  default:
134
- return strdup("\\N");
484
+ *cmp = 0;
485
+ return TF_OK;
486
+ }
487
+ }
488
+
489
+ static int join_key_compare_value_to_cell(const join_key_value *a, const tf_batch *b,
490
+ size_t row, int col, int *cmp) {
491
+ join_key_value rhs = {0};
492
+ if (join_key_value_set(&rhs, b, row, col) != TF_OK) return TF_ERROR;
493
+ int rc = join_key_compare_values(a, &rhs, cmp);
494
+ join_key_value_clear(&rhs);
495
+ return rc;
496
+ }
497
+
498
+ static int csv_header_has_column(const uint8_t *data, size_t len, const char *name) {
499
+ if (!data || !name) return 0;
500
+ size_t name_len = strlen(name);
501
+ size_t start = 0;
502
+ for (size_t i = 0; i <= len; i++) {
503
+ int end_field = (i == len || data[i] == ',' || data[i] == '\n' || data[i] == '\r');
504
+ if (!end_field) continue;
505
+ size_t a = start;
506
+ size_t b = i;
507
+ while (a < b && (data[a] == ' ' || data[a] == '\t')) a++;
508
+ while (b > a && (data[b - 1] == ' ' || data[b - 1] == '\t')) b--;
509
+ if (b > a + 1 && data[a] == '"' && data[b - 1] == '"') {
510
+ a++;
511
+ b--;
512
+ }
513
+ if (b - a == name_len && memcmp(data + a, name, name_len) == 0) return 1;
514
+ if (i == len || data[i] == '\n' || data[i] == '\r') return 0;
515
+ start = i + 1;
516
+ }
517
+ return 0;
518
+ }
519
+
520
+ static int csv_file_header_has_column(const char *path, const char *validated_path, const char *name) {
521
+ if (!path || !name) return 0;
522
+ FILE *f = tf_policy_fopen_read(path, validated_path);
523
+ if (!f) return 0;
524
+ size_t cap = 4096;
525
+ uint8_t *buf = malloc(cap);
526
+ if (!buf) { fclose(f); return 0; }
527
+ size_t len = 0;
528
+ int found_eol = 0;
529
+ while (len < cap) {
530
+ int ch = fgetc(f);
531
+ if (ch == EOF) break;
532
+ buf[len++] = (uint8_t)ch;
533
+ if (ch == '\n' || ch == '\r') { found_eol = 1; break; }
534
+ }
535
+ fclose(f);
536
+ int ok = (len > 0 && (found_eol || len < cap)) ? csv_header_has_column(buf, len, name) : 0;
537
+ free(buf);
538
+ return ok;
539
+ }
540
+
541
+ static void sorted_join_free_pending_batches(join_state *st) {
542
+ if (!st || !st->sorted_batches) return;
543
+ for (size_t i = st->sorted_batch_index; i < st->sorted_n_batches; i++) {
544
+ if (st->sorted_batches[i]) tf_batch_free(st->sorted_batches[i]);
545
+ }
546
+ free(st->sorted_batches);
547
+ st->sorted_batches = NULL;
548
+ st->sorted_n_batches = 0;
549
+ st->sorted_batch_index = 0;
550
+ }
551
+
552
+ static void sorted_join_close(join_state *st) {
553
+ if (!st) return;
554
+ if (st->sorted_current) {
555
+ tf_batch_free(st->sorted_current);
556
+ st->sorted_current = NULL;
557
+ }
558
+ sorted_join_free_pending_batches(st);
559
+ if (st->sorted_decoder) {
560
+ st->sorted_decoder->destroy(st->sorted_decoder);
561
+ st->sorted_decoder = NULL;
562
+ }
563
+ if (st->sorted_file) {
564
+ fclose(st->sorted_file);
565
+ st->sorted_file = NULL;
566
+ }
567
+ st->sorted_have_row = 0;
568
+ st->sorted_exhausted = 1;
569
+ }
570
+
571
+ static void sorted_join_free_schema(join_state *st) {
572
+ if (!st) return;
573
+ for (size_t i = 0; i < st->n_lookup_schema_cols; i++) {
574
+ free(st->lookup_schema_names ? st->lookup_schema_names[i] : NULL);
575
+ }
576
+ free(st->lookup_schema_names);
577
+ free(st->lookup_schema_types);
578
+ st->lookup_schema_names = NULL;
579
+ st->lookup_schema_types = NULL;
580
+ st->n_lookup_schema_cols = 0;
581
+ }
582
+
583
+ static int sorted_join_capture_schema(join_state *st, const tf_batch *b) {
584
+ if (st->n_lookup_schema_cols > 0) return TF_OK;
585
+ st->n_lookup_schema_cols = b->n_cols;
586
+ st->lookup_schema_names = tf_callocarray_checked(b->n_cols ? b->n_cols : 1, sizeof(char *));
587
+ st->lookup_schema_types = tf_callocarray_checked(b->n_cols ? b->n_cols : 1, sizeof(tf_type));
588
+ if (!st->lookup_schema_names || !st->lookup_schema_types) return TF_ERROR;
589
+ for (size_t c = 0; c < b->n_cols; c++) {
590
+ st->lookup_schema_names[c] = strdup(b->col_names[c] ? b->col_names[c] : "");
591
+ if (!st->lookup_schema_names[c]) return TF_ERROR;
592
+ st->lookup_schema_types[c] = b->col_types[c];
593
+ }
594
+ st->lookup_join_col = tf_batch_col_index(b, st->right_col);
595
+ if (st->lookup_join_col < 0) return TF_ERROR;
596
+ st->lookup_key_type = b->col_types[st->lookup_join_col];
597
+ st->have_lookup_key_type = 1;
598
+ if (st->how < 2) {
599
+ st->lookup_out_cols = tf_mallocarray_checked(b->n_cols ? b->n_cols : 1, sizeof(int));
600
+ if (!st->lookup_out_cols) return TF_ERROR;
601
+ st->n_lookup_out = 0;
602
+ for (size_t c = 0; c < b->n_cols; c++) {
603
+ if ((int)c != st->lookup_join_col) st->lookup_out_cols[st->n_lookup_out++] = (int)c;
604
+ }
605
+ }
606
+ return TF_OK;
607
+ }
608
+
609
+ static tf_batch *sorted_join_create_run_batch(const join_state *st) {
610
+ tf_batch *b = tf_batch_create(st->n_lookup_schema_cols, 16);
611
+ if (!b) return NULL;
612
+ for (size_t c = 0; c < st->n_lookup_schema_cols; c++) {
613
+ if (tf_batch_set_schema(b, c, st->lookup_schema_names[c], st->lookup_schema_types[c]) != TF_OK) {
614
+ tf_batch_free(b);
615
+ return NULL;
616
+ }
617
+ }
618
+ return b;
619
+ }
620
+
621
+ static int sorted_join_open(join_state *st, tf_side_channels *side) {
622
+ if (!st || st->sorted_file || st->sorted_decoder) return TF_OK;
623
+ if (!csv_file_header_has_column(st->file, st->validated_file, st->right_col)) {
624
+ if (join_write_error(side, "sorted join: lookup file is missing join column") != TF_OK)
625
+ return TF_ERROR;
626
+ return TF_ERROR;
627
+ }
628
+ st->sorted_file = tf_policy_fopen_read(st->file, st->validated_file);
629
+ if (!st->sorted_file) {
630
+ if (join_write_error(side, "sorted join: cannot open lookup file") != TF_OK)
631
+ return TF_ERROR;
632
+ return TF_ERROR;
633
+ }
634
+ st->sorted_decoder = tf_csv_decoder_create(NULL);
635
+ if (!st->sorted_decoder) return TF_ERROR;
636
+ st->lookup_join_col = -1;
637
+ return TF_OK;
638
+ }
639
+
640
+ static int sorted_join_next_batch(join_state *st, tf_side_channels *side) {
641
+ if (st->sorted_current) {
642
+ tf_batch_free(st->sorted_current);
643
+ st->sorted_current = NULL;
644
+ }
645
+
646
+ for (;;) {
647
+ if (st->sorted_batches && st->sorted_batch_index < st->sorted_n_batches) {
648
+ st->sorted_current = st->sorted_batches[st->sorted_batch_index++];
649
+ if (st->sorted_current && sorted_join_capture_schema(st, st->sorted_current) != TF_OK) {
650
+ if (join_write_error(side, "sorted join: failed to resolve lookup schema") != TF_OK)
651
+ return TF_ERROR;
652
+ return TF_ERROR;
653
+ }
654
+ st->sorted_row = 0;
655
+ if (st->sorted_current && st->sorted_current->n_rows > 0) return TF_OK;
656
+ if (st->sorted_current) {
657
+ tf_batch_free(st->sorted_current);
658
+ st->sorted_current = NULL;
659
+ }
660
+ continue;
661
+ }
662
+
663
+ sorted_join_free_pending_batches(st);
664
+ if (st->sorted_flushed) {
665
+ st->sorted_exhausted = 1;
666
+ return TF_OK;
667
+ }
668
+
669
+ uint8_t buf[64 * 1024];
670
+ size_t n = fread(buf, 1, sizeof(buf), st->sorted_file);
671
+ tf_batch **batches = NULL;
672
+ size_t n_batches = 0;
673
+ int rc;
674
+ if (n > 0) {
675
+ rc = st->sorted_decoder->decode(st->sorted_decoder, buf, n, &batches, &n_batches, side);
676
+ } else {
677
+ if (ferror(st->sorted_file)) {
678
+ if (join_write_error(side, "sorted join: failed reading lookup file") != TF_OK)
679
+ return TF_ERROR;
680
+ return TF_ERROR;
681
+ }
682
+ st->sorted_flushed = 1;
683
+ rc = st->sorted_decoder->flush(st->sorted_decoder, &batches, &n_batches, side);
684
+ }
685
+ if (rc != TF_OK) {
686
+ tf_batch_array_free(batches, n_batches);
687
+ return TF_ERROR;
688
+ }
689
+ st->sorted_batches = batches;
690
+ st->sorted_n_batches = n_batches;
691
+ st->sorted_batch_index = 0;
692
+ if (n_batches == 0) {
693
+ free(batches);
694
+ st->sorted_batches = NULL;
695
+ if (st->sorted_flushed) {
696
+ st->sorted_exhausted = 1;
697
+ return TF_OK;
698
+ }
699
+ }
700
+ }
701
+ }
702
+
703
+ static int sorted_join_advance_lookup(join_state *st, tf_side_channels *side) {
704
+ if (!st->sorted_file && sorted_join_open(st, side) != TF_OK) return TF_ERROR;
705
+ if (st->sorted_have_row) {
706
+ st->sorted_row++;
707
+ st->sorted_have_row = 0;
708
+ }
709
+
710
+ for (;;) {
711
+ if (!st->sorted_current || st->sorted_row >= st->sorted_current->n_rows) {
712
+ if (sorted_join_next_batch(st, side) != TF_OK) return TF_ERROR;
713
+ if (st->sorted_exhausted) return TF_OK;
714
+ continue;
715
+ }
716
+
717
+ join_key_value cur = {0};
718
+ if (join_key_value_set(&cur, st->sorted_current, st->sorted_row, st->lookup_join_col) != TF_OK) {
719
+ return TF_ERROR;
720
+ }
721
+ if (st->have_prev_lookup_key) {
722
+ int cmp = 0;
723
+ if (join_key_compare_values(&st->prev_lookup_key, &cur, &cmp) != TF_OK) {
724
+ join_key_value_clear(&cur);
725
+ if (join_write_error(side, "sorted join: lookup key type changed across batches") != TF_OK)
726
+ return TF_ERROR;
727
+ return TF_ERROR;
728
+ }
729
+ if (cmp > 0) {
730
+ join_key_value_clear(&cur);
731
+ if (join_write_error(side, "sorted join: lookup side is not sorted by join key") != TF_OK)
732
+ return TF_ERROR;
733
+ return TF_ERROR;
734
+ }
735
+ }
736
+ if (join_key_value_copy(&st->prev_lookup_key, &cur) != TF_OK) {
737
+ join_key_value_clear(&cur);
738
+ return TF_ERROR;
739
+ }
740
+ join_key_value_clear(&cur);
741
+ st->have_prev_lookup_key = 1;
742
+ st->sorted_have_row = 1;
743
+ st->lookup_rows++;
744
+ return TF_OK;
745
+ }
746
+ }
747
+
748
+ static void sorted_join_clear_right_run(join_state *st) {
749
+ if (!st) return;
750
+ if (st->right_run) {
751
+ tf_batch_free(st->right_run);
752
+ st->right_run = NULL;
753
+ }
754
+ join_key_value_clear(&st->right_run_key);
755
+ st->have_right_run = 0;
756
+ }
757
+
758
+ static int sorted_join_load_next_run(join_state *st, tf_side_channels *side) {
759
+ sorted_join_clear_right_run(st);
760
+ if (!st->sorted_have_row) {
761
+ if (sorted_join_advance_lookup(st, side) != TF_OK) return TF_ERROR;
762
+ }
763
+ if (!st->sorted_have_row) {
764
+ st->sorted_exhausted = 1;
765
+ return TF_OK;
766
+ }
767
+
768
+ if (join_key_value_set(&st->right_run_key, st->sorted_current, st->sorted_row,
769
+ st->lookup_join_col) != TF_OK) return TF_ERROR;
770
+ st->have_right_run = 1;
771
+
772
+ if (st->how < 2) {
773
+ st->right_run = sorted_join_create_run_batch(st);
774
+ if (!st->right_run) return TF_ERROR;
135
775
  }
776
+
777
+ while (st->sorted_have_row) {
778
+ int cmp = 0;
779
+ if (join_key_compare_value_to_cell(&st->right_run_key, st->sorted_current,
780
+ st->sorted_row, st->lookup_join_col, &cmp) != TF_OK) {
781
+ if (join_write_error(side, "sorted join: lookup key type changed across rows") != TF_OK)
782
+ return TF_ERROR;
783
+ return TF_ERROR;
784
+ }
785
+ if (cmp != 0) break;
786
+
787
+ if (st->how < 2) {
788
+ if (st->max_matches_per_row > 0 && st->right_run->n_rows >= st->max_matches_per_row) {
789
+ if (join_limit_error_context(side, "max_matches_per_row", st->max_matches_per_row,
790
+ st->right_run->n_rows + 1,
791
+ "for sorted lookup key") != TF_OK)
792
+ return TF_ERROR;
793
+ return TF_ERROR;
794
+ }
795
+ size_t dst = st->right_run->n_rows;
796
+ if (tf_batch_ensure_capacity(st->right_run, dst + 1) != TF_OK) return TF_ERROR;
797
+ if (tf_batch_copy_row(st->right_run, dst, st->sorted_current, st->sorted_row) != TF_OK)
798
+ return TF_ERROR;
799
+ if (tf_batch_expose_row(st->right_run, dst) != TF_OK) return TF_ERROR;
800
+ }
801
+
802
+ if (sorted_join_advance_lookup(st, side) != TF_OK) return TF_ERROR;
803
+ }
804
+ return TF_OK;
136
805
  }
137
806
 
138
- /* Copy a cell from src batch to dst batch (different column index) */
139
- static void copy_cell(tf_batch *dst, size_t dr, size_t dc,
140
- const tf_batch *src, size_t sr, int sc) {
141
- if (tf_batch_is_null(src, sr, sc)) {
142
- tf_batch_set_null(dst, dr, dc);
143
- return;
807
+ static int sorted_join_ensure_run_at_least(join_state *st, const tf_batch *left,
808
+ size_t left_row, int left_ci,
809
+ tf_side_channels *side) {
810
+ if (!st->have_right_run && !st->sorted_exhausted) {
811
+ if (sorted_join_load_next_run(st, side) != TF_OK) return TF_ERROR;
812
+ }
813
+ while (st->have_right_run) {
814
+ int cmp = 0;
815
+ if (join_key_compare_value_to_cell(&st->right_run_key, left, left_row, left_ci, &cmp) != TF_OK) {
816
+ if (join_write_error(side, "sorted join: left and lookup join key types differ") != TF_OK)
817
+ return TF_ERROR;
818
+ return TF_ERROR;
819
+ }
820
+ if (cmp >= 0) return TF_OK;
821
+ if (sorted_join_load_next_run(st, side) != TF_OK) return TF_ERROR;
822
+ if (st->sorted_exhausted && !st->have_right_run) return TF_OK;
823
+ }
824
+ return TF_OK;
825
+ }
826
+
827
+ static int sorted_join_check_left_order(join_state *st, const tf_batch *left,
828
+ size_t row, int left_ci,
829
+ tf_side_channels *side) {
830
+ join_key_value cur = {0};
831
+ if (join_key_value_set(&cur, left, row, left_ci) != TF_OK) return TF_ERROR;
832
+ if (st->have_prev_left_key) {
833
+ int cmp = 0;
834
+ if (join_key_compare_values(&st->prev_left_key, &cur, &cmp) != TF_OK) {
835
+ join_key_value_clear(&cur);
836
+ if (join_write_error(side, "sorted join: left join key type changed across batches") != TF_OK)
837
+ return TF_ERROR;
838
+ return TF_ERROR;
839
+ }
840
+ if (cmp > 0) {
841
+ join_key_value_clear(&cur);
842
+ if (join_write_error(side, "sorted join: left side is not sorted by join key") != TF_OK)
843
+ return TF_ERROR;
844
+ return TF_ERROR;
845
+ }
144
846
  }
145
- switch (src->col_types[sc]) {
146
- case TF_TYPE_BOOL: tf_batch_set_bool(dst, dr, dc, tf_batch_get_bool(src, sr, sc)); break;
147
- case TF_TYPE_INT64: tf_batch_set_int64(dst, dr, dc, tf_batch_get_int64(src, sr, sc)); break;
148
- case TF_TYPE_FLOAT64: tf_batch_set_float64(dst, dr, dc, tf_batch_get_float64(src, sr, sc)); break;
149
- case TF_TYPE_STRING: tf_batch_set_string(dst, dr, dc, tf_batch_get_string(src, sr, sc)); break;
150
- case TF_TYPE_DATE: tf_batch_set_date(dst, dr, dc, tf_batch_get_date(src, sr, sc)); break;
151
- case TF_TYPE_TIMESTAMP: tf_batch_set_timestamp(dst, dr, dc, tf_batch_get_timestamp(src, sr, sc)); break;
152
- default: tf_batch_set_null(dst, dr, dc); break;
847
+ if (join_key_value_copy(&st->prev_left_key, &cur) != TF_OK) {
848
+ join_key_value_clear(&cur);
849
+ return TF_ERROR;
153
850
  }
851
+ join_key_value_clear(&cur);
852
+ st->have_prev_left_key = 1;
853
+ return TF_OK;
154
854
  }
155
855
 
156
- static int load_lookup(join_state *st) {
157
- FILE *f = fopen(st->file, "rb");
856
+ static int load_lookup(join_state *st, tf_side_channels *side) {
857
+ FILE *f = NULL;
858
+ uint8_t *data = NULL;
859
+ tf_decoder *dec = NULL;
860
+ tf_batch **batches = NULL;
861
+ tf_batch **flush_batches = NULL;
862
+ tf_batch **all_batches = NULL;
863
+ size_t n_batches = 0, n_flush = 0, total_batches = 0, total_rows = 0;
864
+ tf_batch *merged = NULL;
865
+ int *lookup_out_cols = NULL;
866
+ join_hash_map map = {0};
867
+
868
+ f = tf_policy_fopen_read(st->file, st->validated_file);
158
869
  if (!f) return TF_ERROR;
159
870
 
160
- /* Read entire file */
161
- fseek(f, 0, SEEK_END);
871
+ /* Read entire lookup file, guarded by max_lookup_bytes when provided. */
872
+ if (fseek(f, 0, SEEK_END) != 0) goto fail;
162
873
  long fsize = ftell(f);
163
- fseek(f, 0, SEEK_SET);
164
- if (fsize <= 0) { fclose(f); return TF_ERROR; }
874
+ if (fseek(f, 0, SEEK_SET) != 0) goto fail;
875
+ if (fsize <= 0) goto fail;
876
+ if (st->max_lookup_bytes > 0 && (size_t)fsize > st->max_lookup_bytes) {
877
+ if (join_limit_error(side, "max_lookup_bytes", st->max_lookup_bytes,
878
+ (size_t)fsize) != TF_OK)
879
+ goto fail;
880
+ goto fail;
881
+ }
165
882
 
166
- uint8_t *data = malloc((size_t)fsize);
167
- if (!data) { fclose(f); return TF_ERROR; }
883
+ data = malloc((size_t)fsize);
884
+ if (!data) goto fail;
168
885
  size_t nread = fread(data, 1, (size_t)fsize, f);
169
886
  fclose(f);
170
- if (nread != (size_t)fsize) { free(data); return TF_ERROR; }
887
+ f = NULL;
888
+ if (nread != (size_t)fsize) goto fail;
171
889
 
172
890
  /* Decode using CSV decoder */
173
- tf_decoder *dec = tf_csv_decoder_create(NULL);
174
- if (!dec) { free(data); return TF_ERROR; }
891
+ dec = tf_csv_decoder_create(NULL);
892
+ if (!dec) goto fail;
175
893
 
176
- tf_batch **batches = NULL;
177
- size_t n_batches = 0;
178
- if (dec->decode(dec, data, nread, &batches, &n_batches) != TF_OK) {
179
- dec->destroy(dec);
180
- free(data);
181
- return TF_ERROR;
182
- }
894
+ if (dec->decode(dec, data, nread, &batches, &n_batches, NULL) != TF_OK) goto fail;
895
+ if (dec->flush(dec, &flush_batches, &n_flush, NULL) != TF_OK) goto fail;
183
896
 
184
- tf_batch **flush_batches = NULL;
185
- size_t n_flush = 0;
186
- dec->flush(dec, &flush_batches, &n_flush);
187
-
188
- /* Count total rows and merge into single batch */
189
- size_t total_rows = 0;
190
- size_t total_batches = n_batches + n_flush;
191
- tf_batch **all_batches = malloc(total_batches * sizeof(tf_batch *));
192
- if (!all_batches) { dec->destroy(dec); free(data); return TF_ERROR; }
897
+ total_batches = n_batches + n_flush;
898
+ all_batches = tf_mallocarray_checked(total_batches ? total_batches : 1, sizeof(tf_batch *));
899
+ if (!all_batches) goto fail;
193
900
  for (size_t i = 0; i < n_batches; i++) {
194
901
  all_batches[i] = batches[i];
195
902
  total_rows += batches[i]->n_rows;
903
+ if (st->max_lookup_rows > 0 && total_rows > st->max_lookup_rows) {
904
+ if (join_limit_error(side, "max_lookup_rows", st->max_lookup_rows,
905
+ total_rows) != TF_OK)
906
+ goto fail;
907
+ goto fail;
908
+ }
196
909
  }
197
910
  for (size_t i = 0; i < n_flush; i++) {
198
911
  all_batches[n_batches + i] = flush_batches[i];
199
912
  total_rows += flush_batches[i]->n_rows;
913
+ if (st->max_lookup_rows > 0 && total_rows > st->max_lookup_rows) {
914
+ if (join_limit_error(side, "max_lookup_rows", st->max_lookup_rows,
915
+ total_rows) != TF_OK)
916
+ goto fail;
917
+ goto fail;
918
+ }
919
+ }
920
+
921
+ if (total_batches == 0 && st->how >= 2) {
922
+ if (!csv_header_has_column(data, nread, st->right_col)) goto fail;
923
+ if (map_init(&map, 1) != 0) goto fail;
924
+ if (join_check_unsorted_state_bytes(st, &map, NULL, 0, side) != TF_OK) goto fail;
925
+
926
+ free(all_batches);
927
+ all_batches = NULL;
928
+ tf_batch_array_free(batches, n_batches);
929
+ batches = NULL;
930
+ tf_batch_array_free(flush_batches, n_flush);
931
+ flush_batches = NULL;
932
+ dec->destroy(dec);
933
+ dec = NULL;
934
+ free(data);
935
+ data = NULL;
936
+
937
+ st->lookup_rows = 0;
938
+ st->lookup = NULL;
939
+ st->lookup_join_col = -1;
940
+ st->lookup_out_cols = NULL;
941
+ st->n_lookup_out = 0;
942
+ st->map = map;
943
+ return TF_OK;
200
944
  }
945
+ if (total_batches == 0) goto fail;
946
+ if (total_rows == 0 && st->how >= 2) {
947
+ tf_batch *first = all_batches[0];
948
+ int lookup_join_col = tf_batch_col_index(first, st->right_col);
949
+ if (lookup_join_col < 0) goto fail;
950
+ if (map_init(&map, 1) != 0) goto fail;
951
+ if (join_check_unsorted_state_bytes(st, &map, NULL, 0, side) != TF_OK) goto fail;
201
952
 
202
- if (total_batches == 0 || total_rows == 0) {
953
+ tf_batch_array_free(batches, n_batches);
954
+ batches = NULL;
955
+ tf_batch_array_free(flush_batches, n_flush);
956
+ flush_batches = NULL;
203
957
  free(all_batches);
204
- free(batches); free(flush_batches);
205
- dec->destroy(dec); free(data);
206
- return TF_ERROR;
958
+ all_batches = NULL;
959
+ dec->destroy(dec);
960
+ dec = NULL;
961
+ free(data);
962
+ data = NULL;
963
+
964
+ st->lookup_rows = 0;
965
+ st->lookup = NULL;
966
+ st->lookup_join_col = lookup_join_col;
967
+ st->lookup_key_type = first->col_types[lookup_join_col];
968
+ st->have_lookup_key_type = 1;
969
+ st->lookup_out_cols = NULL;
970
+ st->n_lookup_out = 0;
971
+ st->map = map;
972
+ return TF_OK;
207
973
  }
974
+ if (total_rows == 0) goto fail;
208
975
 
209
976
  /* Use first batch as schema source */
210
977
  tf_batch *first = all_batches[0];
211
- tf_batch *merged = tf_batch_create(first->n_cols, total_rows);
212
- if (!merged) {
213
- for (size_t i = 0; i < total_batches; i++) tf_batch_free(all_batches[i]);
214
- free(all_batches); free(batches); free(flush_batches);
215
- dec->destroy(dec); free(data);
216
- return TF_ERROR;
978
+ merged = tf_batch_create(first->n_cols, total_rows);
979
+ if (!merged) goto fail;
980
+ for (size_t c = 0; c < first->n_cols; c++) {
981
+ if (tf_batch_set_schema(merged, c, first->col_names[c], first->col_types[c]) != TF_OK) goto fail;
217
982
  }
218
- for (size_t c = 0; c < first->n_cols; c++)
219
- tf_batch_set_schema(merged, c, first->col_names[c], first->col_types[c]);
220
983
 
221
984
  size_t dst_row = 0;
222
985
  for (size_t b = 0; b < total_batches; b++) {
223
986
  for (size_t r = 0; r < all_batches[b]->n_rows; r++) {
224
- tf_batch_copy_row(merged, dst_row, all_batches[b], r);
225
- merged->n_rows = ++dst_row;
987
+ if (tf_batch_copy_row(merged, dst_row, all_batches[b], r) != TF_OK) goto fail;
988
+ if (tf_batch_expose_row(merged, dst_row) != TF_OK) goto fail;
989
+ dst_row++;
226
990
  }
227
991
  }
228
992
 
229
- for (size_t i = 0; i < total_batches; i++) tf_batch_free(all_batches[i]);
993
+ for (size_t i = 0; i < total_batches; i++) all_batches[i] = NULL;
994
+ tf_batch_array_free(batches, n_batches);
995
+ batches = NULL;
996
+ tf_batch_array_free(flush_batches, n_flush);
997
+ flush_batches = NULL;
230
998
  free(all_batches);
231
- free(batches);
232
- free(flush_batches);
999
+ all_batches = NULL;
233
1000
  dec->destroy(dec);
1001
+ dec = NULL;
234
1002
  free(data);
235
-
236
- st->lookup = merged;
1003
+ data = NULL;
237
1004
 
238
1005
  /* Find join column in lookup */
239
- st->lookup_join_col = tf_batch_col_index(merged, st->right_col);
240
- if (st->lookup_join_col < 0) return TF_ERROR;
1006
+ int lookup_join_col = tf_batch_col_index(merged, st->right_col);
1007
+ if (lookup_join_col < 0) goto fail;
241
1008
 
242
- /* Determine output columns (all except join key) */
243
- st->lookup_out_cols = malloc(merged->n_cols * sizeof(int));
244
- st->n_lookup_out = 0;
245
- for (size_t c = 0; c < merged->n_cols; c++) {
246
- if ((int)c != st->lookup_join_col)
247
- st->lookup_out_cols[st->n_lookup_out++] = (int)c;
1009
+ st->lookup_key_type = merged->col_types[lookup_join_col];
1010
+ st->have_lookup_key_type = 1;
1011
+
1012
+ size_t n_lookup_out = 0;
1013
+ if (st->how < 2) {
1014
+ /* Determine output columns for mutating joins (all except join key). */
1015
+ lookup_out_cols = tf_mallocarray_checked(merged->n_cols ? merged->n_cols : 1, sizeof(int));
1016
+ if (!lookup_out_cols) goto fail;
1017
+ for (size_t c = 0; c < merged->n_cols; c++) {
1018
+ if ((int)c != lookup_join_col)
1019
+ lookup_out_cols[n_lookup_out++] = (int)c;
1020
+ }
248
1021
  }
249
1022
 
250
1023
  /* Build hash map */
251
- map_init(&st->map, merged->n_rows);
1024
+ if (map_init(&map, merged->n_rows) != 0) goto fail;
1025
+ const tf_batch *retained_lookup = (st->how >= 2) ? NULL : merged;
1026
+ size_t retained_lookup_out = (st->how >= 2) ? 0 : n_lookup_out;
1027
+ if (join_check_unsorted_state_bytes(st, &map, retained_lookup, retained_lookup_out, side) != TF_OK) goto fail;
252
1028
  for (size_t r = 0; r < merged->n_rows; r++) {
253
- char *key = format_join_key(merged, r, st->lookup_join_col);
254
- if (key) { map_insert(&st->map, key, r); free(key); }
1029
+ char *key = format_join_key(merged, r, lookup_join_col);
1030
+ if (!key) goto fail;
1031
+ int rc = map_insert(st, &map, key, r, side);
1032
+ free(key);
1033
+ if (rc != 0) goto fail;
1034
+ if (join_check_unsorted_state_bytes(st, &map, retained_lookup, retained_lookup_out, side) != TF_OK) goto fail;
255
1035
  }
256
1036
 
1037
+ if (st->how >= 2) {
1038
+ tf_batch_free(merged);
1039
+ merged = NULL;
1040
+ }
1041
+ st->lookup_rows = total_rows;
1042
+ st->lookup = merged;
1043
+ st->lookup_join_col = lookup_join_col;
1044
+ st->lookup_out_cols = lookup_out_cols;
1045
+ st->n_lookup_out = n_lookup_out;
1046
+ st->map = map;
257
1047
  return TF_OK;
258
- }
259
-
260
- static int join_process(tf_step *self, tf_batch *in, tf_batch **out,
261
- tf_side_channels *side) {
262
- (void)side;
263
- join_state *st = self->state;
264
- *out = NULL;
265
1048
 
266
- if (!st->loaded) {
267
- if (load_lookup(st) != TF_OK) return TF_ERROR;
268
- st->loaded = 1;
1049
+ fail:
1050
+ if (f) fclose(f);
1051
+ if (all_batches) {
1052
+ for (size_t i = 0; i < total_batches; i++) tf_batch_free(all_batches[i]);
1053
+ free(all_batches);
1054
+ if (batches) {
1055
+ for (size_t i = 0; i < n_batches; i++) batches[i] = NULL;
1056
+ }
1057
+ if (flush_batches) {
1058
+ for (size_t i = 0; i < n_flush; i++) flush_batches[i] = NULL;
1059
+ }
269
1060
  }
1061
+ tf_batch_array_free(batches, n_batches);
1062
+ tf_batch_array_free(flush_batches, n_flush);
1063
+ if (dec) dec->destroy(dec);
1064
+ free(data);
1065
+ if (merged) tf_batch_free(merged);
1066
+ free(lookup_out_cols);
1067
+ map_free(&map);
1068
+ return TF_ERROR;
1069
+ }
270
1070
 
271
- int left_ci = tf_batch_col_index(in, st->left_col);
272
- if (left_ci < 0) return TF_ERROR;
1071
+ static int join_reserve_output_row(join_state *st, tf_side_channels *side) {
1072
+ if (st->max_output_rows > 0 && st->output_rows >= st->max_output_rows) {
1073
+ if (join_limit_error_context(side, "max_output_rows",
1074
+ st->max_output_rows, st->output_rows + 1,
1075
+ "while emitting joined output") != TF_OK)
1076
+ return 0;
1077
+ return 0;
1078
+ }
1079
+ st->output_rows++;
1080
+ return 1;
1081
+ }
273
1082
 
274
- size_t n_out_cols = in->n_cols + st->n_lookup_out;
275
1083
 
276
- /* Worst case: each input row matches multiple lookup rows.
277
- * Start with input size, grow as needed. */
278
- size_t out_cap = in->n_rows > 0 ? in->n_rows : 16;
279
- tf_batch *ob = tf_batch_create(n_out_cols, out_cap);
280
- if (!ob) return TF_ERROR;
1084
+ static int join_ensure_ordinals(uint64_t **ord, size_t *cap, size_t need) {
1085
+ if (*cap >= need) return TF_OK;
1086
+ size_t new_cap = 0;
1087
+ if (tf_size_grow_pow2(*cap, need, 16, &new_cap) != TF_OK) return TF_ERROR;
1088
+ uint64_t *tmp = tf_reallocarray_checked(*ord, new_cap, sizeof(uint64_t));
1089
+ if (!tmp) return TF_ERROR;
1090
+ *ord = tmp;
1091
+ *cap = new_cap;
1092
+ return TF_OK;
1093
+ }
281
1094
 
282
- /* Set schema: main columns + lookup columns */
283
- for (size_t c = 0; c < in->n_cols; c++)
284
- tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
285
- for (size_t k = 0; k < st->n_lookup_out; k++) {
286
- int lc = st->lookup_out_cols[k];
287
- tf_batch_set_schema(ob, in->n_cols + k,
288
- st->lookup->col_names[lc],
289
- st->lookup->col_types[lc]);
1095
+ static size_t join_spill_estimated_row_bytes(const join_state *st) {
1096
+ size_t bytes = 40;
1097
+ for (size_t c = 0; c < st->spill_n_cols; c++) {
1098
+ bytes += 1;
1099
+ switch (st->spill_schema_types[c]) {
1100
+ case TF_TYPE_BOOL: bytes += 1; break;
1101
+ case TF_TYPE_INT64: bytes += sizeof(int64_t); break;
1102
+ case TF_TYPE_FLOAT64: bytes += sizeof(double); break;
1103
+ case TF_TYPE_STRING: bytes += sizeof(char *) + 64; break;
1104
+ case TF_TYPE_DATE: bytes += sizeof(int32_t); break;
1105
+ case TF_TYPE_TIMESTAMP: bytes += sizeof(int64_t); break;
1106
+ default: break;
1107
+ }
290
1108
  }
1109
+ return bytes < 64 ? 64 : bytes;
1110
+ }
291
1111
 
292
- size_t out_row = 0;
293
- for (size_t r = 0; r < in->n_rows; r++) {
294
- char *key = format_join_key(in, r, left_ci);
295
- if (!key) continue;
296
-
297
- join_bucket *bucket = map_find(&st->map, key);
298
- free(key);
1112
+ static tf_batch *join_spill_create_left_batch(const join_state *st, size_t capacity) {
1113
+ tf_batch *b = tf_batch_create(st->spill_n_cols, capacity ? capacity : 16);
1114
+ if (!b) return NULL;
1115
+ for (size_t c = 0; c < st->spill_n_cols; c++) {
1116
+ if (tf_batch_set_schema(b, c, st->spill_schema_names[c], st->spill_schema_types[c]) != TF_OK) {
1117
+ tf_batch_free(b);
1118
+ return NULL;
1119
+ }
1120
+ }
1121
+ return b;
1122
+ }
299
1123
 
300
- if (bucket) {
301
- /* Emit one row per match */
302
- for (size_t m = 0; m < bucket->n_rows; m++) {
303
- size_t lr = bucket->rows[m];
304
- tf_batch_ensure_capacity(ob, out_row + 1);
305
- /* Copy main columns */
306
- for (size_t c = 0; c < in->n_cols; c++)
307
- copy_cell(ob, out_row, c, in, r, (int)c);
308
- /* Copy lookup columns */
309
- for (size_t k = 0; k < st->n_lookup_out; k++)
310
- copy_cell(ob, out_row, in->n_cols + k, st->lookup, lr, st->lookup_out_cols[k]);
311
- ob->n_rows = ++out_row;
1124
+ static tf_batch *join_spill_create_lookup_batch(const join_state *st, size_t capacity) {
1125
+ if (st->how < 2) {
1126
+ tf_batch *b = tf_batch_create(st->spill_lookup_n_cols, capacity ? capacity : 16);
1127
+ if (!b) return NULL;
1128
+ for (size_t c = 0; c < st->spill_lookup_n_cols; c++) {
1129
+ if (tf_batch_set_schema(b, c, st->spill_lookup_schema_names[c],
1130
+ st->spill_lookup_schema_types[c]) != TF_OK) {
1131
+ tf_batch_free(b);
1132
+ return NULL;
312
1133
  }
313
- } else if (st->how == 1) {
314
- /* Left join: emit main + nulls */
315
- tf_batch_ensure_capacity(ob, out_row + 1);
316
- for (size_t c = 0; c < in->n_cols; c++)
317
- copy_cell(ob, out_row, c, in, r, (int)c);
318
- for (size_t k = 0; k < st->n_lookup_out; k++)
319
- tf_batch_set_null(ob, out_row, in->n_cols + k);
320
- ob->n_rows = ++out_row;
321
1134
  }
322
- /* Inner join + no match: skip */
1135
+ return b;
323
1136
  }
324
1137
 
325
- if (out_row > 0)
326
- *out = ob;
327
- else
328
- tf_batch_free(ob);
329
-
330
- return TF_OK;
331
- }
332
-
333
- static int join_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
334
- (void)self; (void)side; *out = NULL; return TF_OK;
1138
+ tf_batch *b = tf_batch_create(1, capacity ? capacity : 16);
1139
+ if (!b) return NULL;
1140
+ if (tf_batch_set_schema(b, 0, st->right_col ? st->right_col : "key", st->lookup_key_type) != TF_OK) {
1141
+ tf_batch_free(b);
1142
+ return NULL;
1143
+ }
1144
+ return b;
335
1145
  }
336
1146
 
337
- static void join_destroy(tf_step *self) {
338
- join_state *st = self->state;
339
- if (st) {
340
- free(st->file);
341
- free(st->left_col);
342
- free(st->right_col);
343
- if (st->lookup) tf_batch_free(st->lookup);
344
- free(st->lookup_out_cols);
345
- map_free(&st->map);
346
- free(st);
1147
+ static tf_batch *join_spill_create_output_batch(const join_state *st, size_t capacity) {
1148
+ if (st->how < 2) {
1149
+ tf_batch *b = tf_batch_create(st->spill_output_n_cols, capacity ? capacity : 16);
1150
+ if (!b) return NULL;
1151
+ for (size_t c = 0; c < st->spill_output_n_cols; c++) {
1152
+ if (tf_batch_set_schema(b, c, st->spill_output_schema_names[c],
1153
+ st->spill_output_schema_types[c]) != TF_OK) {
1154
+ tf_batch_free(b);
1155
+ return NULL;
1156
+ }
1157
+ }
1158
+ return b;
347
1159
  }
348
- free(self);
1160
+ return join_spill_create_left_batch(st, capacity);
349
1161
  }
350
1162
 
351
- tf_step *tf_join_create(const cJSON *args) {
352
- if (!args) return NULL;
353
- cJSON *file_j = cJSON_GetObjectItemCaseSensitive(args, "file");
354
- cJSON *on_j = cJSON_GetObjectItemCaseSensitive(args, "on");
1163
+ static int join_spill_check_input_schema(join_state *st, const tf_batch *in, tf_side_channels *side) {
1164
+ if (in->n_cols != st->spill_n_cols) {
1165
+ if (join_write_error(side, "join spill: input schema changed across batches") != TF_OK)
1166
+ return TF_ERROR;
1167
+ return TF_ERROR;
1168
+ }
1169
+ for (size_t c = 0; c < in->n_cols; c++) {
1170
+ if (in->col_types[c] != st->spill_schema_types[c] ||
1171
+ strcmp(in->col_names[c] ? in->col_names[c] : "", st->spill_schema_names[c]) != 0) {
1172
+ if (join_write_error(side, "join spill: input schema changed across batches") != TF_OK)
1173
+ return TF_ERROR;
1174
+ return TF_ERROR;
1175
+ }
1176
+ }
1177
+ int left_ci = tf_batch_col_index(in, st->left_col);
1178
+ if (left_ci < 0 || left_ci != st->spill_left_join_col) {
1179
+ if (join_write_error(side, "join spill: left join column is missing or moved") != TF_OK)
1180
+ return TF_ERROR;
1181
+ return TF_ERROR;
1182
+ }
1183
+ return TF_OK;
1184
+ }
1185
+
1186
+ static int join_spill_init_schema(join_state *st, const tf_batch *in, tf_side_channels *side) {
1187
+ if (st->spill_has_schema) return join_spill_check_input_schema(st, in, side);
1188
+ int left_ci = tf_batch_col_index(in, st->left_col);
1189
+ if (left_ci < 0) {
1190
+ if (join_write_error(side, "join spill: left join column not found") != TF_OK)
1191
+ return TF_ERROR;
1192
+ return TF_ERROR;
1193
+ }
1194
+ st->spill_n_cols = in->n_cols;
1195
+ st->spill_schema_names = tf_callocarray_checked(in->n_cols ? in->n_cols : 1, sizeof(char *));
1196
+ st->spill_schema_types = tf_callocarray_checked(in->n_cols ? in->n_cols : 1, sizeof(tf_type));
1197
+ if (!st->spill_schema_names || !st->spill_schema_types) return TF_ERROR;
1198
+ for (size_t c = 0; c < in->n_cols; c++) {
1199
+ st->spill_schema_names[c] = strdup(in->col_names[c] ? in->col_names[c] : "");
1200
+ if (!st->spill_schema_names[c]) return TF_ERROR;
1201
+ st->spill_schema_types[c] = in->col_types[c];
1202
+ }
1203
+ st->spill_left_join_col = left_ci;
1204
+ if (st->configured_run_rows > 0) {
1205
+ st->run_rows = st->configured_run_rows;
1206
+ } else if (st->spill_memory_bytes > 0) {
1207
+ size_t row_bytes = join_spill_estimated_row_bytes(st);
1208
+ st->run_rows = st->spill_memory_bytes / (row_bytes * 4);
1209
+ if (st->run_rows < JOIN_MIN_RUN_ROWS) st->run_rows = JOIN_MIN_RUN_ROWS;
1210
+ } else {
1211
+ st->run_rows = JOIN_DEFAULT_RUN_ROWS;
1212
+ }
1213
+ st->spill_left_buf = join_spill_create_left_batch(st, st->run_rows);
1214
+ if (st->how >= 2)
1215
+ st->spill_out_buf = join_spill_create_output_batch(st, st->run_rows);
1216
+ if (!st->spill_left_buf || (st->how >= 2 && !st->spill_out_buf)) return TF_ERROR;
1217
+ st->lookup_key_type = in->col_types[left_ci];
1218
+ st->have_lookup_key_type = 0;
1219
+ st->spill_has_schema = 1;
1220
+ return TF_OK;
1221
+ }
1222
+
1223
+ static int join_spill_compare_batch_key(const tf_batch *b, size_t ca, size_t cb,
1224
+ size_t ra, size_t rb) {
1225
+ int null_a = tf_batch_is_null(b, ra, ca);
1226
+ int null_b = tf_batch_is_null(b, rb, cb);
1227
+ if (null_a && null_b) return 0;
1228
+ if (null_a) return 1;
1229
+ if (null_b) return -1;
1230
+ switch (b->col_types[ca]) {
1231
+ case TF_TYPE_BOOL:
1232
+ return (int)tf_batch_get_bool(b, ra, ca) - (int)tf_batch_get_bool(b, rb, cb);
1233
+ case TF_TYPE_INT64: {
1234
+ int64_t a = tf_batch_get_int64(b, ra, ca), v = tf_batch_get_int64(b, rb, cb);
1235
+ return (a > v) - (a < v);
1236
+ }
1237
+ case TF_TYPE_FLOAT64: {
1238
+ double a = tf_batch_get_float64(b, ra, ca), v = tf_batch_get_float64(b, rb, cb);
1239
+ return (a > v) - (a < v);
1240
+ }
1241
+ case TF_TYPE_STRING:
1242
+ return strcmp(tf_batch_get_string(b, ra, ca), tf_batch_get_string(b, rb, cb));
1243
+ case TF_TYPE_DATE: {
1244
+ int32_t a = tf_batch_get_date(b, ra, ca), v = tf_batch_get_date(b, rb, cb);
1245
+ return (a > v) - (a < v);
1246
+ }
1247
+ case TF_TYPE_TIMESTAMP: {
1248
+ int64_t a = tf_batch_get_timestamp(b, ra, ca), v = tf_batch_get_timestamp(b, rb, cb);
1249
+ return (a > v) - (a < v);
1250
+ }
1251
+ default:
1252
+ return 0;
1253
+ }
1254
+ }
1255
+
1256
+ typedef struct {
1257
+ const join_state *st;
1258
+ const tf_batch *batch;
1259
+ const uint64_t *ordinals;
1260
+ int kind; /* 0=left by key, 1=lookup by key, 2=output by ordinal */
1261
+ } join_spill_sort_ctx;
1262
+
1263
+ static int join_spill_compare_indices(const void *ctx, size_t ra, size_t rb) {
1264
+ const join_spill_sort_ctx *sort = (const join_spill_sort_ctx *)ctx;
1265
+ if (sort->kind == 2) {
1266
+ uint64_t oa = sort->ordinals[ra], ob = sort->ordinals[rb];
1267
+ return (oa > ob) - (oa < ob);
1268
+ }
1269
+ size_t col;
1270
+ if (sort->kind == 1)
1271
+ col = (sort->st->how < 2) ? (size_t)sort->st->spill_lookup_join_col : 0u;
1272
+ else
1273
+ col = (size_t)sort->st->spill_left_join_col;
1274
+ int cmp = join_spill_compare_batch_key(sort->batch, col, col, ra, rb);
1275
+ if (cmp != 0) return cmp;
1276
+ uint64_t oa = sort->ordinals[ra], ob = sort->ordinals[rb];
1277
+ return (oa > ob) - (oa < ob);
1278
+ }
1279
+
1280
+ static size_t *join_spill_sorted_indices(const join_state *st, const tf_batch *b,
1281
+ const uint64_t *ordinals, int kind) {
1282
+ size_t n = b ? b->n_rows : 0;
1283
+ size_t *idx = tf_mallocarray_checked(n ? n : 1, sizeof(size_t));
1284
+ if (!idx) return NULL;
1285
+ for (size_t i = 0; i < n; i++) idx[i] = i;
1286
+ join_spill_sort_ctx ctx = { .st = st, .batch = b, .ordinals = ordinals, .kind = kind };
1287
+ tf_sort_indices(idx, n, join_spill_compare_indices, &ctx);
1288
+ return idx;
1289
+ }
1290
+
1291
+ static int join_spill_write_exact(FILE *f, const void *ptr, size_t len) {
1292
+ return fwrite(ptr, 1, len, f) == len ? TF_OK : TF_ERROR;
1293
+ }
1294
+
1295
+ static int join_spill_read_exact(FILE *f, void *ptr, size_t len) {
1296
+ return fread(ptr, 1, len, f) == len ? TF_OK : TF_ERROR;
1297
+ }
1298
+
1299
+ static int join_spill_write_cell(FILE *f, const tf_batch *b, size_t r, size_t c) {
1300
+ uint8_t is_null = tf_batch_is_null(b, r, c) ? 1 : 0;
1301
+ if (join_spill_write_exact(f, &is_null, sizeof(is_null)) != TF_OK) return TF_ERROR;
1302
+ if (is_null) return TF_OK;
1303
+ switch (b->col_types[c]) {
1304
+ case TF_TYPE_BOOL: { uint8_t v = tf_batch_get_bool(b, r, c) ? 1 : 0; return join_spill_write_exact(f, &v, sizeof(v)); }
1305
+ case TF_TYPE_INT64: { int64_t v = tf_batch_get_int64(b, r, c); return join_spill_write_exact(f, &v, sizeof(v)); }
1306
+ case TF_TYPE_FLOAT64: { double v = tf_batch_get_float64(b, r, c); return join_spill_write_exact(f, &v, sizeof(v)); }
1307
+ case TF_TYPE_STRING: {
1308
+ const char *str = tf_batch_get_string(b, r, c);
1309
+ uint64_t len = str ? (uint64_t)strlen(str) : 0;
1310
+ if (join_spill_write_exact(f, &len, sizeof(len)) != TF_OK) return TF_ERROR;
1311
+ return len ? join_spill_write_exact(f, str, (size_t)len) : TF_OK;
1312
+ }
1313
+ case TF_TYPE_DATE: { int32_t v = tf_batch_get_date(b, r, c); return join_spill_write_exact(f, &v, sizeof(v)); }
1314
+ case TF_TYPE_TIMESTAMP: { int64_t v = tf_batch_get_timestamp(b, r, c); return join_spill_write_exact(f, &v, sizeof(v)); }
1315
+ default: return TF_OK;
1316
+ }
1317
+ }
1318
+
1319
+ static int join_spill_append_path(char ***paths, size_t *n, size_t *cap, char *path) {
1320
+ if (*n == *cap) {
1321
+ size_t need = 0;
1322
+ size_t new_cap = 0;
1323
+ if (tf_size_add(*n, 1, &need) != TF_OK ||
1324
+ tf_size_grow_pow2(*cap, need, 8, &new_cap) != TF_OK) {
1325
+ return TF_ERROR;
1326
+ }
1327
+ char **tmp = tf_reallocarray_checked(*paths, new_cap, sizeof(char *));
1328
+ if (!tmp) return TF_ERROR;
1329
+ *paths = tmp;
1330
+ *cap = new_cap;
1331
+ }
1332
+ (*paths)[(*n)++] = path;
1333
+ return TF_OK;
1334
+ }
1335
+
1336
+ static int join_spill_write_run(join_state *st, tf_batch *batch, const uint64_t *ordinals,
1337
+ const size_t *indices, size_t n, const char *kind) {
1338
+ if (n == 0) return TF_OK;
1339
+ char label[48];
1340
+ snprintf(label, sizeof(label), "join-%s", kind ? kind : "run");
1341
+ char *path = NULL;
1342
+ FILE *f = tf_spill_open_run_file(st->spill, label, &path);
1343
+ if (!f) return TF_ERROR;
1344
+ for (size_t i = 0; i < n; i++) {
1345
+ size_t r = indices[i];
1346
+ uint64_t ordinal = ordinals[r];
1347
+ if (join_spill_write_exact(f, &ordinal, sizeof(ordinal)) != TF_OK) goto fail;
1348
+ for (size_t c = 0; c < batch->n_cols; c++) {
1349
+ if (join_spill_write_cell(f, batch, r, c) != TF_OK) goto fail;
1350
+ }
1351
+ }
1352
+ long pos = ftell(f);
1353
+ if (pos > 0) st->spill_bytes += (size_t)pos;
1354
+ if (fclose(f) != 0) {
1355
+ tf_set_last_error("join spill: failed closing run file");
1356
+ remove(path);
1357
+ free(path);
1358
+ return TF_ERROR;
1359
+ }
1360
+ int rc;
1361
+ if (strcmp(kind, "left") == 0) {
1362
+ rc = join_spill_append_path(&st->spill_left_run_paths, &st->spill_n_left_runs, &st->spill_cap_left_runs, path);
1363
+ } else if (strcmp(kind, "lookup") == 0) {
1364
+ rc = join_spill_append_path(&st->spill_lookup_run_paths, &st->spill_n_lookup_runs, &st->spill_cap_lookup_runs, path);
1365
+ } else {
1366
+ rc = join_spill_append_path(&st->spill_out_run_paths, &st->spill_n_out_runs, &st->spill_cap_out_runs, path);
1367
+ }
1368
+ if (rc != TF_OK) { remove(path); free(path); return TF_ERROR; }
1369
+ st->spill_runs++;
1370
+ return TF_OK;
1371
+ fail:
1372
+ tf_set_last_error("join spill: failed writing run file");
1373
+ fclose(f);
1374
+ remove(path);
1375
+ free(path);
1376
+ return TF_ERROR;
1377
+ }
1378
+
1379
+ static int join_spill_write_left_run(join_state *st) {
1380
+ if (!st->spill_left_buf || st->spill_left_buf->n_rows == 0) return TF_OK;
1381
+ size_t *idx = join_spill_sorted_indices(st, st->spill_left_buf, st->spill_left_ordinals, 0);
1382
+ if (!idx) return TF_ERROR;
1383
+ int rc = join_spill_write_run(st, st->spill_left_buf, st->spill_left_ordinals, idx,
1384
+ st->spill_left_buf->n_rows, "left");
1385
+ free(idx);
1386
+ if (rc != TF_OK) return TF_ERROR;
1387
+ tf_batch_free(st->spill_left_buf);
1388
+ st->spill_left_buf = join_spill_create_left_batch(st, st->run_rows);
1389
+ free(st->spill_left_ordinals);
1390
+ st->spill_left_ordinals = NULL;
1391
+ st->spill_left_ordinal_cap = 0;
1392
+ return st->spill_left_buf ? TF_OK : TF_ERROR;
1393
+ }
1394
+
1395
+ static int join_spill_write_lookup_run(join_state *st) {
1396
+ if (!st->spill_lookup_buf || st->spill_lookup_buf->n_rows == 0) return TF_OK;
1397
+ size_t *idx = join_spill_sorted_indices(st, st->spill_lookup_buf, st->spill_lookup_ordinals, 1);
1398
+ if (!idx) return TF_ERROR;
1399
+ int rc = join_spill_write_run(st, st->spill_lookup_buf, st->spill_lookup_ordinals, idx,
1400
+ st->spill_lookup_buf->n_rows, "lookup");
1401
+ free(idx);
1402
+ if (rc != TF_OK) return TF_ERROR;
1403
+ tf_batch_free(st->spill_lookup_buf);
1404
+ st->spill_lookup_buf = join_spill_create_lookup_batch(st, st->run_rows);
1405
+ free(st->spill_lookup_ordinals);
1406
+ st->spill_lookup_ordinals = NULL;
1407
+ st->spill_lookup_ordinal_cap = 0;
1408
+ return st->spill_lookup_buf ? TF_OK : TF_ERROR;
1409
+ }
1410
+
1411
+ static int join_spill_write_output_run(join_state *st) {
1412
+ if (!st->spill_out_buf || st->spill_out_buf->n_rows == 0) return TF_OK;
1413
+ size_t *idx = join_spill_sorted_indices(st, st->spill_out_buf, st->spill_out_ordinals, 2);
1414
+ if (!idx) return TF_ERROR;
1415
+ int rc = join_spill_write_run(st, st->spill_out_buf, st->spill_out_ordinals, idx,
1416
+ st->spill_out_buf->n_rows, "out");
1417
+ free(idx);
1418
+ if (rc != TF_OK) return TF_ERROR;
1419
+ tf_batch_free(st->spill_out_buf);
1420
+ st->spill_out_buf = join_spill_create_output_batch(st, st->run_rows);
1421
+ free(st->spill_out_ordinals);
1422
+ st->spill_out_ordinals = NULL;
1423
+ st->spill_out_ordinal_cap = 0;
1424
+ return st->spill_out_buf ? TF_OK : TF_ERROR;
1425
+ }
1426
+
1427
+ static void join_spill_row_clear(join_spill_row *row, const tf_type *types, size_t n_cols) {
1428
+ if (!row || !row->cells || !row->nulls) return;
1429
+ for (size_t c = 0; c < n_cols; c++) {
1430
+ if (!row->nulls[c] && types[c] == TF_TYPE_STRING) free(row->cells[c].str);
1431
+ row->cells[c].str = NULL;
1432
+ row->nulls[c] = 1;
1433
+ }
1434
+ }
1435
+
1436
+ static int join_spill_row_init(join_spill_row *row, size_t n_cols) {
1437
+ row->ordinal = 0;
1438
+ row->nulls = tf_callocarray_checked(n_cols ? n_cols : 1, sizeof(uint8_t));
1439
+ row->cells = tf_callocarray_checked(n_cols ? n_cols : 1, sizeof(join_key_data));
1440
+ if (!row->nulls || !row->cells) {
1441
+ free(row->nulls);
1442
+ free(row->cells);
1443
+ row->nulls = NULL;
1444
+ row->cells = NULL;
1445
+ return TF_ERROR;
1446
+ }
1447
+ for (size_t c = 0; c < n_cols; c++) row->nulls[c] = 1;
1448
+ return TF_OK;
1449
+ }
1450
+
1451
+ static void join_spill_row_free(join_spill_row *row, const tf_type *types, size_t n_cols) {
1452
+ if (!row) return;
1453
+ join_spill_row_clear(row, types, n_cols);
1454
+ free(row->nulls);
1455
+ free(row->cells);
1456
+ row->nulls = NULL;
1457
+ row->cells = NULL;
1458
+ }
1459
+
1460
+ static int join_spill_read_cell_value(FILE *f, join_spill_row *row, const tf_type *types, size_t c) {
1461
+ switch (types[c]) {
1462
+ case TF_TYPE_BOOL: { uint8_t v = 0; if (join_spill_read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].b = v; return TF_OK; }
1463
+ case TF_TYPE_INT64: { int64_t v = 0; if (join_spill_read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].i64 = v; return TF_OK; }
1464
+ case TF_TYPE_FLOAT64: { double v = 0; if (join_spill_read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].f64 = v; return TF_OK; }
1465
+ case TF_TYPE_STRING: {
1466
+ uint64_t len = 0;
1467
+ if (join_spill_read_exact(f, &len, sizeof(len)) != TF_OK) return TF_ERROR;
1468
+ if (len > (uint64_t)SIZE_MAX - 1) return TF_ERROR;
1469
+ char *str = malloc((size_t)len + 1);
1470
+ if (!str) return TF_ERROR;
1471
+ if (len && join_spill_read_exact(f, str, (size_t)len) != TF_OK) { free(str); return TF_ERROR; }
1472
+ str[len] = '\0';
1473
+ row->cells[c].str = str;
1474
+ return TF_OK;
1475
+ }
1476
+ case TF_TYPE_DATE: { int32_t v = 0; if (join_spill_read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].date = v; return TF_OK; }
1477
+ case TF_TYPE_TIMESTAMP: { int64_t v = 0; if (join_spill_read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].i64 = v; return TF_OK; }
1478
+ default: return TF_OK;
1479
+ }
1480
+ }
1481
+
1482
+ static int join_spill_reader_advance(join_spill_reader *reader, const tf_type *types, size_t n_cols) {
1483
+ if (!reader || !reader->file || reader->done) return 0;
1484
+ join_spill_row_clear(&reader->row, types, n_cols);
1485
+ if (fread(&reader->row.ordinal, sizeof(reader->row.ordinal), 1, reader->file) != 1) {
1486
+ if (feof(reader->file)) {
1487
+ reader->done = 1;
1488
+ reader->has_row = 0;
1489
+ return 0;
1490
+ }
1491
+ tf_set_last_error("join spill: failed reading run file");
1492
+ return -1;
1493
+ }
1494
+ for (size_t c = 0; c < n_cols; c++) {
1495
+ uint8_t is_null = 1;
1496
+ if (join_spill_read_exact(reader->file, &is_null, sizeof(is_null)) != TF_OK) {
1497
+ tf_set_last_error("join spill: corrupt run file");
1498
+ return -1;
1499
+ }
1500
+ reader->row.nulls[c] = is_null ? 1 : 0;
1501
+ if (!reader->row.nulls[c] && join_spill_read_cell_value(reader->file, &reader->row, types, c) != TF_OK) {
1502
+ tf_set_last_error("join spill: corrupt run file");
1503
+ return -1;
1504
+ }
1505
+ }
1506
+ reader->has_row = 1;
1507
+ return 1;
1508
+ }
1509
+
1510
+ static const tf_type *join_spill_reader_types(const join_state *st, int kind, size_t *n_cols) {
1511
+ if (kind == 1) {
1512
+ if (st->how < 2) {
1513
+ *n_cols = st->spill_lookup_n_cols;
1514
+ return st->spill_lookup_schema_types;
1515
+ }
1516
+ *n_cols = 1;
1517
+ return &st->lookup_key_type;
1518
+ }
1519
+ if (kind == 2 && st->how < 2) {
1520
+ *n_cols = st->spill_output_n_cols;
1521
+ return st->spill_output_schema_types;
1522
+ }
1523
+ *n_cols = st->spill_n_cols;
1524
+ return st->spill_schema_types;
1525
+ }
1526
+
1527
+ static void join_spill_close_readers(join_state *st, int kind) {
1528
+ join_spill_reader **readers;
1529
+ size_t *n_readers;
1530
+ if (kind == 0) { readers = &st->spill_left_readers; n_readers = &st->spill_n_left_readers; }
1531
+ else if (kind == 1) { readers = &st->spill_lookup_readers; n_readers = &st->spill_n_lookup_readers; }
1532
+ else { readers = &st->spill_out_readers; n_readers = &st->spill_n_out_readers; }
1533
+ if (!*readers) return;
1534
+ size_t n_cols = 0;
1535
+ const tf_type *types = join_spill_reader_types(st, kind, &n_cols);
1536
+ for (size_t i = 0; i < *n_readers; i++) {
1537
+ if ((*readers)[i].file) fclose((*readers)[i].file);
1538
+ join_spill_row_free(&(*readers)[i].row, types, n_cols);
1539
+ }
1540
+ free(*readers);
1541
+ *readers = NULL;
1542
+ *n_readers = 0;
1543
+ }
1544
+
1545
+ static void join_spill_remove_paths(char ***paths, size_t *n, size_t *cap) {
1546
+ for (size_t i = 0; i < *n; i++) {
1547
+ if ((*paths)[i]) {
1548
+ remove((*paths)[i]);
1549
+ free((*paths)[i]);
1550
+ (*paths)[i] = NULL;
1551
+ }
1552
+ }
1553
+ free(*paths);
1554
+ *paths = NULL;
1555
+ *n = 0;
1556
+ *cap = 0;
1557
+ }
1558
+
1559
+ static int join_spill_open_readers(join_state *st, int kind) {
1560
+ char **paths;
1561
+ size_t n_paths;
1562
+ join_spill_reader **readers;
1563
+ size_t *n_readers;
1564
+ if (kind == 0) {
1565
+ paths = st->spill_left_run_paths; n_paths = st->spill_n_left_runs;
1566
+ readers = &st->spill_left_readers; n_readers = &st->spill_n_left_readers;
1567
+ } else if (kind == 1) {
1568
+ paths = st->spill_lookup_run_paths; n_paths = st->spill_n_lookup_runs;
1569
+ readers = &st->spill_lookup_readers; n_readers = &st->spill_n_lookup_readers;
1570
+ } else {
1571
+ paths = st->spill_out_run_paths; n_paths = st->spill_n_out_runs;
1572
+ readers = &st->spill_out_readers; n_readers = &st->spill_n_out_readers;
1573
+ }
1574
+ if (n_paths == 0) return TF_OK;
1575
+ *readers = tf_callocarray_checked(n_paths, sizeof(join_spill_reader));
1576
+ if (!*readers) return TF_ERROR;
1577
+ *n_readers = n_paths;
1578
+ size_t n_cols = 0;
1579
+ const tf_type *types = join_spill_reader_types(st, kind, &n_cols);
1580
+ for (size_t i = 0; i < n_paths; i++) {
1581
+ (*readers)[i].file = fopen(paths[i], "rb");
1582
+ if (!(*readers)[i].file) { tf_set_last_error("join spill: cannot reopen run file"); return TF_ERROR; }
1583
+ if (join_spill_row_init(&(*readers)[i].row, n_cols) != TF_OK) return TF_ERROR;
1584
+ int rc = join_spill_reader_advance(&(*readers)[i], types, n_cols);
1585
+ if (rc < 0) return TF_ERROR;
1586
+ }
1587
+ return TF_OK;
1588
+ }
1589
+
1590
+ static int join_spill_compare_row_cell(const join_spill_row *a, size_t ac, tf_type at,
1591
+ const join_spill_row *b, size_t bc, tf_type bt) {
1592
+ if (at != bt) return (int)at - (int)bt;
1593
+ int null_a = a->nulls[ac] != 0;
1594
+ int null_b = b->nulls[bc] != 0;
1595
+ if (null_a && null_b) return 0;
1596
+ if (null_a) return 1;
1597
+ if (null_b) return -1;
1598
+ switch (at) {
1599
+ case TF_TYPE_BOOL: return (int)a->cells[ac].b - (int)b->cells[bc].b;
1600
+ case TF_TYPE_INT64:
1601
+ case TF_TYPE_TIMESTAMP: return (a->cells[ac].i64 > b->cells[bc].i64) - (a->cells[ac].i64 < b->cells[bc].i64);
1602
+ case TF_TYPE_FLOAT64: return (a->cells[ac].f64 > b->cells[bc].f64) - (a->cells[ac].f64 < b->cells[bc].f64);
1603
+ case TF_TYPE_STRING: return strcmp(a->cells[ac].str ? a->cells[ac].str : "", b->cells[bc].str ? b->cells[bc].str : "");
1604
+ case TF_TYPE_DATE: return (a->cells[ac].date > b->cells[bc].date) - (a->cells[ac].date < b->cells[bc].date);
1605
+ default: return 0;
1606
+ }
1607
+ }
1608
+
1609
+ static int join_spill_compare_left_rows(const join_state *st, const join_spill_row *a,
1610
+ const join_spill_row *b) {
1611
+ size_t c = (size_t)st->spill_left_join_col;
1612
+ int cmp = join_spill_compare_row_cell(a, c, st->spill_schema_types[c], b, c, st->spill_schema_types[c]);
1613
+ if (cmp != 0) return cmp;
1614
+ return (a->ordinal > b->ordinal) - (a->ordinal < b->ordinal);
1615
+ }
1616
+
1617
+ static int join_spill_compare_lookup_rows(const join_state *st, const join_spill_row *a,
1618
+ const join_spill_row *b) {
1619
+ size_t c = (st->how < 2) ? (size_t)st->spill_lookup_join_col : 0u;
1620
+ tf_type t = (st->how < 2) ? st->spill_lookup_schema_types[c] : st->lookup_key_type;
1621
+ int cmp = join_spill_compare_row_cell(a, c, t, b, c, t);
1622
+ if (cmp != 0) return cmp;
1623
+ return (a->ordinal > b->ordinal) - (a->ordinal < b->ordinal);
1624
+ }
1625
+
1626
+ static int join_spill_compare_lookup_to_left(const join_state *st, const join_spill_row *lookup,
1627
+ const join_spill_row *left) {
1628
+ size_t lc = (size_t)st->spill_left_join_col;
1629
+ size_t rc = (st->how < 2) ? (size_t)st->spill_lookup_join_col : 0u;
1630
+ tf_type rt = (st->how < 2) ? st->spill_lookup_schema_types[rc] : st->lookup_key_type;
1631
+ return join_spill_compare_row_cell(lookup, rc, rt, left, lc, st->spill_schema_types[lc]);
1632
+ }
1633
+
1634
+ static int join_spill_best_left_reader(const join_state *st) {
1635
+ int best = -1;
1636
+ for (size_t i = 0; i < st->spill_n_left_readers; i++) {
1637
+ const join_spill_reader *r = &st->spill_left_readers[i];
1638
+ if (!r->has_row || r->done) continue;
1639
+ if (best < 0) { best = (int)i; continue; }
1640
+ int cmp = join_spill_compare_left_rows(st, &r->row, &st->spill_left_readers[best].row);
1641
+ if (cmp < 0 || (cmp == 0 && i < (size_t)best)) best = (int)i;
1642
+ }
1643
+ return best;
1644
+ }
1645
+
1646
+ static int join_spill_best_lookup_reader(const join_state *st) {
1647
+ int best = -1;
1648
+ for (size_t i = 0; i < st->spill_n_lookup_readers; i++) {
1649
+ const join_spill_reader *r = &st->spill_lookup_readers[i];
1650
+ if (!r->has_row || r->done) continue;
1651
+ if (best < 0) { best = (int)i; continue; }
1652
+ int cmp = join_spill_compare_lookup_rows(st, &r->row, &st->spill_lookup_readers[best].row);
1653
+ if (cmp < 0 || (cmp == 0 && i < (size_t)best)) best = (int)i;
1654
+ }
1655
+ return best;
1656
+ }
1657
+
1658
+ static int join_spill_best_output_reader(const join_state *st) {
1659
+ int best = -1;
1660
+ for (size_t i = 0; i < st->spill_n_out_readers; i++) {
1661
+ const join_spill_reader *r = &st->spill_out_readers[i];
1662
+ if (!r->has_row || r->done) continue;
1663
+ if (best < 0) { best = (int)i; continue; }
1664
+ uint64_t a = r->row.ordinal;
1665
+ uint64_t b = st->spill_out_readers[best].row.ordinal;
1666
+ if (a < b || (a == b && i < (size_t)best)) best = (int)i;
1667
+ }
1668
+ return best;
1669
+ }
1670
+
1671
+ static int join_spill_append_key_bytes(char **buf, size_t *len, size_t *cap, const void *src, size_t n) {
1672
+ size_t need = 0;
1673
+ if (tf_size_add(*len, n, &need) != TF_OK ||
1674
+ tf_size_add(need, 1, &need) != TF_OK) {
1675
+ return TF_ERROR;
1676
+ }
1677
+ if (need > *cap) {
1678
+ size_t new_cap = 0;
1679
+ if (tf_size_grow_pow2(*cap, need, 64, &new_cap) != TF_OK) return TF_ERROR;
1680
+ char *tmp = tf_reallocarray_checked(*buf, new_cap, sizeof(char));
1681
+ if (!tmp) return TF_ERROR;
1682
+ *buf = tmp;
1683
+ *cap = new_cap;
1684
+ }
1685
+ memcpy(*buf + *len, src, n);
1686
+ *len += n;
1687
+ (*buf)[*len] = '\0';
1688
+ return TF_OK;
1689
+ }
1690
+
1691
+ static int join_spill_append_key_str(char **buf, size_t *len, size_t *cap, const char *str) {
1692
+ return join_spill_append_key_bytes(buf, len, cap, str, strlen(str));
1693
+ }
1694
+
1695
+ static char *join_spill_build_key(const join_state *st, const join_spill_row *row, int lookup_row) {
1696
+ size_t c;
1697
+ tf_type type;
1698
+ if (lookup_row) {
1699
+ c = (st->how < 2) ? (size_t)st->spill_lookup_join_col : 0u;
1700
+ type = (st->how < 2) ? st->spill_lookup_schema_types[c] : st->lookup_key_type;
1701
+ } else {
1702
+ c = (size_t)st->spill_left_join_col;
1703
+ type = st->spill_schema_types[c];
1704
+ }
1705
+ char *buf = NULL;
1706
+ size_t len = 0, cap = 0;
1707
+ char tmp[96];
1708
+ if (row->nulls[c]) {
1709
+ snprintf(tmp, sizeof(tmp), "N:%d", (int)type);
1710
+ if (join_spill_append_key_str(&buf, &len, &cap, tmp) != TF_OK) goto fail;
1711
+ } else {
1712
+ switch (type) {
1713
+ case TF_TYPE_BOOL:
1714
+ if (join_spill_append_key_str(&buf, &len, &cap, row->cells[c].b ? "B:1" : "B:0") != TF_OK) goto fail;
1715
+ break;
1716
+ case TF_TYPE_INT64:
1717
+ snprintf(tmp, sizeof(tmp), "I:%lld", (long long)row->cells[c].i64);
1718
+ if (join_spill_append_key_str(&buf, &len, &cap, tmp) != TF_OK) goto fail;
1719
+ break;
1720
+ case TF_TYPE_FLOAT64: {
1721
+ int n = snprintf(tmp, sizeof(tmp), "F:%.17g", row->cells[c].f64);
1722
+ if (n < 0 || join_spill_append_key_bytes(&buf, &len, &cap, tmp, (size_t)n) != TF_OK) goto fail;
1723
+ break;
1724
+ }
1725
+ case TF_TYPE_STRING: {
1726
+ const char *str = row->cells[c].str ? row->cells[c].str : "";
1727
+ int n = snprintf(tmp, sizeof(tmp), "S:%zu:", strlen(str));
1728
+ if (n < 0 || join_spill_append_key_bytes(&buf, &len, &cap, tmp, (size_t)n) != TF_OK) goto fail;
1729
+ if (join_spill_append_key_str(&buf, &len, &cap, str) != TF_OK) goto fail;
1730
+ break;
1731
+ }
1732
+ case TF_TYPE_DATE:
1733
+ snprintf(tmp, sizeof(tmp), "D:%lld", (long long)row->cells[c].date);
1734
+ if (join_spill_append_key_str(&buf, &len, &cap, tmp) != TF_OK) goto fail;
1735
+ break;
1736
+ case TF_TYPE_TIMESTAMP:
1737
+ snprintf(tmp, sizeof(tmp), "T:%lld", (long long)row->cells[c].i64);
1738
+ if (join_spill_append_key_str(&buf, &len, &cap, tmp) != TF_OK) goto fail;
1739
+ break;
1740
+ default:
1741
+ if (join_spill_append_key_str(&buf, &len, &cap, "U") != TF_OK) goto fail;
1742
+ break;
1743
+ }
1744
+ }
1745
+ if (!buf) buf = strdup("");
1746
+ return buf;
1747
+ fail:
1748
+ free(buf);
1749
+ return NULL;
1750
+ }
1751
+
1752
+ static int join_spill_count_lookup_key(join_state *st, const join_spill_row *row, tf_side_channels *side) {
1753
+ char *key = join_spill_build_key(st, row, 1);
1754
+ if (!key) return TF_ERROR;
1755
+ if (!st->spill_last_lookup_key || strcmp(st->spill_last_lookup_key, key) != 0) {
1756
+ if (st->max_lookup_keys > 0 && st->spill_lookup_keys >= st->max_lookup_keys) {
1757
+ int err_rc = join_limit_error(side, "max_lookup_keys", st->max_lookup_keys,
1758
+ st->spill_lookup_keys + 1);
1759
+ free(key);
1760
+ if (err_rc != TF_OK) return TF_ERROR;
1761
+ return TF_ERROR;
1762
+ }
1763
+ size_t key_bytes_delta = 0;
1764
+ size_t new_spill_lookup_key_bytes = 0;
1765
+ if (tf_size_add(strlen(key), 1, &key_bytes_delta) != TF_OK ||
1766
+ tf_size_add(st->spill_lookup_key_bytes, key_bytes_delta,
1767
+ &new_spill_lookup_key_bytes) != TF_OK) {
1768
+ free(key);
1769
+ return TF_ERROR;
1770
+ }
1771
+ free(st->spill_last_lookup_key);
1772
+ st->spill_last_lookup_key = key;
1773
+ key = NULL;
1774
+ st->spill_lookup_keys++;
1775
+ st->spill_lookup_key_bytes = new_spill_lookup_key_bytes;
1776
+ }
1777
+ free(key);
1778
+ return TF_OK;
1779
+ }
1780
+
1781
+ static int join_spill_set_batch_cell(tf_batch *out, size_t dst_row, size_t dst_col,
1782
+ const join_spill_row *row, size_t src_col,
1783
+ const tf_type *types) {
1784
+ tf_type type = types[src_col];
1785
+ return tf_batch_set_owned_cell_value(out, dst_row, dst_col, type,
1786
+ row->nulls[src_col], &row->cells[src_col]);
1787
+ }
1788
+
1789
+ static int join_spill_row_to_batch_typed(tf_batch *out, size_t dst_row,
1790
+ const join_spill_row *row,
1791
+ const tf_type *types, size_t n_cols) {
1792
+ if (tf_batch_ensure_capacity(out, dst_row + 1) != TF_OK) return TF_ERROR;
1793
+ for (size_t c = 0; c < n_cols; c++) {
1794
+ if (join_spill_set_batch_cell(out, dst_row, c, row, c, types) != TF_OK) return TF_ERROR;
1795
+ }
1796
+ return TF_OK;
1797
+ }
1798
+
1799
+ static int join_spill_row_to_batch(const join_state *st, tf_batch *out, size_t dst_row,
1800
+ const join_spill_row *row) {
1801
+ return join_spill_row_to_batch_typed(out, dst_row, row,
1802
+ st->spill_schema_types, st->spill_n_cols);
1803
+ }
1804
+
1805
+ static int join_spill_append_selected_row(join_state *st, const join_spill_row *row,
1806
+ tf_side_channels *side) {
1807
+ if (!join_reserve_output_row(st, side)) return TF_ERROR;
1808
+ size_t dst = st->spill_out_buf->n_rows;
1809
+ if (join_ensure_ordinals(&st->spill_out_ordinals,
1810
+ &st->spill_out_ordinal_cap, dst + 1) != TF_OK) {
1811
+ return TF_ERROR;
1812
+ }
1813
+ if (join_spill_row_to_batch(st, st->spill_out_buf, dst, row) != TF_OK) return TF_ERROR;
1814
+ st->spill_out_ordinals[dst] = row->ordinal;
1815
+ if (tf_batch_expose_row(st->spill_out_buf, dst) != TF_OK) return TF_ERROR;
1816
+ st->spill_kept_rows++;
1817
+ if (st->spill_out_buf->n_rows >= st->run_rows) return join_spill_write_output_run(st);
1818
+ return TF_OK;
1819
+ }
1820
+
1821
+ static int join_spill_capture_lookup_schema(join_state *st, const tf_batch *batch,
1822
+ int lookup_ci, tf_side_channels *side) {
1823
+ if (st->how >= 2) return TF_OK;
1824
+ if (lookup_ci < 0) return TF_ERROR;
1825
+
1826
+ if (st->spill_lookup_n_cols > 0) {
1827
+ if (batch->n_cols != st->spill_lookup_n_cols || lookup_ci != st->spill_lookup_join_col) {
1828
+ if (join_write_error(side, "join spill: lookup schema changed across batches") != TF_OK)
1829
+ return TF_ERROR;
1830
+ return TF_ERROR;
1831
+ }
1832
+ for (size_t c = 0; c < batch->n_cols; c++) {
1833
+ if (batch->col_types[c] != st->spill_lookup_schema_types[c] ||
1834
+ strcmp(batch->col_names[c] ? batch->col_names[c] : "",
1835
+ st->spill_lookup_schema_names[c] ? st->spill_lookup_schema_names[c] : "") != 0) {
1836
+ if (join_write_error(side, "join spill: lookup schema changed across batches") != TF_OK)
1837
+ return TF_ERROR;
1838
+ return TF_ERROR;
1839
+ }
1840
+ }
1841
+ return TF_OK;
1842
+ }
1843
+
1844
+ st->spill_lookup_n_cols = batch->n_cols;
1845
+ st->spill_lookup_join_col = lookup_ci;
1846
+ st->spill_lookup_schema_names = tf_callocarray_checked(batch->n_cols ? batch->n_cols : 1, sizeof(char *));
1847
+ st->spill_lookup_schema_types = tf_callocarray_checked(batch->n_cols ? batch->n_cols : 1, sizeof(tf_type));
1848
+ if (!st->spill_lookup_schema_names || !st->spill_lookup_schema_types) return TF_ERROR;
1849
+ for (size_t c = 0; c < batch->n_cols; c++) {
1850
+ st->spill_lookup_schema_names[c] = strdup(batch->col_names[c] ? batch->col_names[c] : "");
1851
+ if (!st->spill_lookup_schema_names[c]) return TF_ERROR;
1852
+ st->spill_lookup_schema_types[c] = batch->col_types[c];
1853
+ }
1854
+
1855
+ st->lookup_key_type = batch->col_types[lookup_ci];
1856
+ st->have_lookup_key_type = 1;
1857
+
1858
+ st->lookup_out_cols = tf_mallocarray_checked(batch->n_cols ? batch->n_cols : 1, sizeof(int));
1859
+ if (!st->lookup_out_cols) return TF_ERROR;
1860
+ st->n_lookup_out = 0;
1861
+ for (size_t c = 0; c < batch->n_cols; c++) {
1862
+ if ((int)c != lookup_ci) st->lookup_out_cols[st->n_lookup_out++] = (int)c;
1863
+ }
1864
+
1865
+ st->spill_output_n_cols = st->spill_n_cols + st->n_lookup_out;
1866
+ st->spill_output_schema_names = tf_callocarray_checked(st->spill_output_n_cols ? st->spill_output_n_cols : 1, sizeof(char *));
1867
+ st->spill_output_schema_types = tf_callocarray_checked(st->spill_output_n_cols ? st->spill_output_n_cols : 1, sizeof(tf_type));
1868
+ if (!st->spill_output_schema_names || !st->spill_output_schema_types) return TF_ERROR;
1869
+ for (size_t c = 0; c < st->spill_n_cols; c++) {
1870
+ st->spill_output_schema_names[c] = strdup(st->spill_schema_names[c] ? st->spill_schema_names[c] : "");
1871
+ if (!st->spill_output_schema_names[c]) return TF_ERROR;
1872
+ st->spill_output_schema_types[c] = st->spill_schema_types[c];
1873
+ }
1874
+ for (size_t k = 0; k < st->n_lookup_out; k++) {
1875
+ int lc = st->lookup_out_cols[k];
1876
+ size_t oc = st->spill_n_cols + k;
1877
+ st->spill_output_schema_names[oc] = strdup(st->spill_lookup_schema_names[lc] ? st->spill_lookup_schema_names[lc] : "");
1878
+ if (!st->spill_output_schema_names[oc]) return TF_ERROR;
1879
+ st->spill_output_schema_types[oc] = st->spill_lookup_schema_types[lc];
1880
+ }
1881
+
1882
+ st->spill_lookup_buf = join_spill_create_lookup_batch(st, st->run_rows);
1883
+ st->spill_out_buf = join_spill_create_output_batch(st, st->run_rows);
1884
+ if (!st->spill_lookup_buf || !st->spill_out_buf) return TF_ERROR;
1885
+ return TF_OK;
1886
+ }
1887
+
1888
+ static int join_spill_process_lookup_batch(join_state *st, const tf_batch *batch,
1889
+ tf_side_channels *side) {
1890
+ int lookup_ci = tf_batch_col_index(batch, st->right_col);
1891
+ if (lookup_ci < 0) {
1892
+ if (join_write_error(side, "join spill: lookup file is missing join column") != TF_OK)
1893
+ return TF_ERROR;
1894
+ return TF_ERROR;
1895
+ }
1896
+ if (batch->col_types[lookup_ci] != st->spill_schema_types[st->spill_left_join_col]) {
1897
+ if (join_write_error(side, "join spill: left and lookup join key types differ") != TF_OK)
1898
+ return TF_ERROR;
1899
+ return TF_ERROR;
1900
+ }
1901
+ if (st->how < 2) {
1902
+ if (join_spill_capture_lookup_schema(st, batch, lookup_ci, side) != TF_OK) return TF_ERROR;
1903
+ } else if (!st->have_lookup_key_type) {
1904
+ st->lookup_key_type = batch->col_types[lookup_ci];
1905
+ st->have_lookup_key_type = 1;
1906
+ st->spill_lookup_buf = join_spill_create_lookup_batch(st, st->run_rows);
1907
+ if (!st->spill_lookup_buf) return TF_ERROR;
1908
+ }
1909
+ for (size_t r = 0; r < batch->n_rows; r++) {
1910
+ if (st->max_lookup_rows > 0 && st->lookup_rows >= st->max_lookup_rows) {
1911
+ if (join_limit_error(side, "max_lookup_rows", st->max_lookup_rows,
1912
+ st->lookup_rows + 1) != TF_OK)
1913
+ return TF_ERROR;
1914
+ return TF_ERROR;
1915
+ }
1916
+ size_t dst = st->spill_lookup_buf->n_rows;
1917
+ if (tf_batch_ensure_capacity(st->spill_lookup_buf, dst + 1) != TF_OK) return TF_ERROR;
1918
+ if (st->how < 2) {
1919
+ if (tf_batch_copy_row(st->spill_lookup_buf, dst, batch, r) != TF_OK) return TF_ERROR;
1920
+ } else {
1921
+ if (tf_batch_copy_cell_index(st->spill_lookup_buf, dst, 0, batch, r, lookup_ci) != TF_OK) return TF_ERROR;
1922
+ }
1923
+ if (join_ensure_ordinals(&st->spill_lookup_ordinals, &st->spill_lookup_ordinal_cap, dst + 1) != TF_OK) return TF_ERROR;
1924
+ st->spill_lookup_ordinals[dst] = st->spill_next_lookup_ordinal++;
1925
+ if (tf_batch_expose_row(st->spill_lookup_buf, dst) != TF_OK) return TF_ERROR;
1926
+ st->lookup_rows++;
1927
+ if (st->spill_lookup_buf->n_rows >= st->run_rows && join_spill_write_lookup_run(st) != TF_OK)
1928
+ return TF_ERROR;
1929
+ }
1930
+ return TF_OK;
1931
+ }
1932
+
1933
+ static int join_spill_load_lookup_runs(join_state *st, tf_side_channels *side) {
1934
+ if (st->spill_lookup_loaded) return TF_OK;
1935
+ st->spill_lookup_loaded = 1;
1936
+ if (!csv_file_header_has_column(st->file, st->validated_file, st->right_col)) {
1937
+ if (join_write_error(side, "join spill: lookup file is missing join column") != TF_OK)
1938
+ return TF_ERROR;
1939
+ return TF_ERROR;
1940
+ }
1941
+ FILE *f = tf_policy_fopen_read(st->file, st->validated_file);
1942
+ if (!f) {
1943
+ if (join_write_error(side, "join spill: cannot open lookup file") != TF_OK)
1944
+ return TF_ERROR;
1945
+ return TF_ERROR;
1946
+ }
1947
+ tf_decoder *dec = tf_csv_decoder_create(NULL);
1948
+ if (!dec) { fclose(f); return TF_ERROR; }
1949
+ size_t bytes_read = 0;
1950
+ int flushed = 0;
1951
+ int rc = TF_OK;
1952
+ while (!flushed) {
1953
+ uint8_t buf[64 * 1024];
1954
+ size_t n = fread(buf, 1, sizeof(buf), f);
1955
+ tf_batch **batches = NULL;
1956
+ size_t n_batches = 0;
1957
+ if (n > 0) {
1958
+ bytes_read += n;
1959
+ if (st->max_lookup_bytes > 0 && bytes_read > st->max_lookup_bytes) {
1960
+ if (join_limit_error(side, "max_lookup_bytes", st->max_lookup_bytes,
1961
+ bytes_read) != TF_OK)
1962
+ rc = TF_ERROR;
1963
+ rc = TF_ERROR;
1964
+ break;
1965
+ }
1966
+ rc = dec->decode(dec, buf, n, &batches, &n_batches, side);
1967
+ } else {
1968
+ if (ferror(f)) {
1969
+ if (join_write_error(side, "join spill: failed reading lookup file") != TF_OK)
1970
+ rc = TF_ERROR;
1971
+ rc = TF_ERROR;
1972
+ break;
1973
+ }
1974
+ flushed = 1;
1975
+ rc = dec->flush(dec, &batches, &n_batches, side);
1976
+ }
1977
+ if (rc != TF_OK) {
1978
+ tf_batch_array_free(batches, n_batches);
1979
+ break;
1980
+ }
1981
+ for (size_t i = 0; i < n_batches; i++) {
1982
+ if (join_spill_process_lookup_batch(st, batches[i], side) != TF_OK) rc = TF_ERROR;
1983
+ tf_batch_free(batches[i]);
1984
+ if (rc != TF_OK) {
1985
+ tf_batch_array_free_items(batches + i + 1, n_batches - i - 1);
1986
+ break;
1987
+ }
1988
+ }
1989
+ free(batches);
1990
+ if (rc != TF_OK) break;
1991
+ }
1992
+ dec->destroy(dec);
1993
+ fclose(f);
1994
+ if (rc != TF_OK) return TF_ERROR;
1995
+ if (st->spill_lookup_buf && st->spill_lookup_buf->n_rows > 0)
1996
+ return join_spill_write_lookup_run(st);
1997
+ if (st->spill_lookup_buf) {
1998
+ tf_batch_free(st->spill_lookup_buf);
1999
+ st->spill_lookup_buf = NULL;
2000
+ }
2001
+ return TF_OK;
2002
+ }
2003
+
2004
+ static int join_spill_output_ordinal(join_state *st, uint64_t left_ordinal,
2005
+ size_t match_idx, uint64_t *out,
2006
+ tf_side_channels *side) {
2007
+ if (st->how >= 2) {
2008
+ *out = left_ordinal;
2009
+ return TF_OK;
2010
+ }
2011
+ if (st->max_matches_per_row == 0) {
2012
+ if (join_write_error(side, "join spill: inner/left joins need max_matches_per_row to bound lookup key runs") != TF_OK)
2013
+ return TF_ERROR;
2014
+ return TF_ERROR;
2015
+ }
2016
+ if (match_idx >= st->max_matches_per_row) {
2017
+ if (join_limit_error_context(side, "max_matches_per_row", st->max_matches_per_row,
2018
+ match_idx + 1, "for spilled lookup key") != TF_OK)
2019
+ return TF_ERROR;
2020
+ return TF_ERROR;
2021
+ }
2022
+ uint64_t slots = (uint64_t)st->max_matches_per_row;
2023
+ if (slots == 0 || left_ordinal > (UINT64_MAX - (uint64_t)match_idx) / slots) {
2024
+ if (join_write_error(side, "join spill: output ordinal overflow") != TF_OK)
2025
+ return TF_ERROR;
2026
+ return TF_ERROR;
2027
+ }
2028
+ *out = left_ordinal * slots + (uint64_t)match_idx;
2029
+ return TF_OK;
2030
+ }
2031
+
2032
+ static int join_spill_append_joined_row(join_state *st, const join_spill_row *left,
2033
+ const tf_batch *lookup_run, size_t match_idx,
2034
+ tf_side_channels *side) {
2035
+ if (!st->spill_out_buf) {
2036
+ st->spill_out_buf = join_spill_create_output_batch(st, st->run_rows);
2037
+ if (!st->spill_out_buf) return TF_ERROR;
2038
+ }
2039
+ if (!join_reserve_output_row(st, side)) return TF_ERROR;
2040
+ size_t dst = st->spill_out_buf->n_rows;
2041
+ if (tf_batch_ensure_capacity(st->spill_out_buf, dst + 1) != TF_OK) return TF_ERROR;
2042
+ if (join_ensure_ordinals(&st->spill_out_ordinals, &st->spill_out_ordinal_cap, dst + 1) != TF_OK)
2043
+ return TF_ERROR;
2044
+ uint64_t output_ordinal = 0;
2045
+ if (join_spill_output_ordinal(st, left->ordinal, lookup_run ? match_idx : 0, &output_ordinal, side) != TF_OK)
2046
+ return TF_ERROR;
2047
+
2048
+ for (size_t c = 0; c < st->spill_n_cols; c++) {
2049
+ if (join_spill_set_batch_cell(st->spill_out_buf, dst, c, left, c,
2050
+ st->spill_schema_types) != TF_OK) return TF_ERROR;
2051
+ }
2052
+ if (lookup_run) {
2053
+ for (size_t k = 0; k < st->n_lookup_out; k++) {
2054
+ size_t src = (size_t)st->lookup_out_cols[k];
2055
+ if (tf_batch_is_null(lookup_run, match_idx, src)) {
2056
+ if (tf_batch_set_null(st->spill_out_buf, dst, st->spill_n_cols + k) != TF_OK) return TF_ERROR;
2057
+ } else {
2058
+ if (tf_batch_copy_cell_index(st->spill_out_buf, dst, st->spill_n_cols + k,
2059
+ lookup_run, match_idx, (int)src) != TF_OK) return TF_ERROR;
2060
+ }
2061
+ }
2062
+ } else {
2063
+ for (size_t k = 0; k < st->n_lookup_out; k++) {
2064
+ if (tf_batch_set_null(st->spill_out_buf, dst, st->spill_n_cols + k) != TF_OK) return TF_ERROR;
2065
+ }
2066
+ }
2067
+
2068
+ st->spill_out_ordinals[dst] = output_ordinal;
2069
+ if (tf_batch_expose_row(st->spill_out_buf, dst) != TF_OK) return TF_ERROR;
2070
+ st->spill_kept_rows++;
2071
+ if (st->spill_out_buf->n_rows >= st->run_rows) return join_spill_write_output_run(st);
2072
+ return TF_OK;
2073
+ }
2074
+
2075
+ static int join_spill_drain_lookup_before_left(join_state *st, const join_spill_row *left,
2076
+ tf_side_channels *side) {
2077
+ for (;;) {
2078
+ int lookup_idx = join_spill_best_lookup_reader(st);
2079
+ if (lookup_idx < 0) return TF_OK;
2080
+ join_spill_reader *lookup = &st->spill_lookup_readers[lookup_idx];
2081
+ if (join_spill_count_lookup_key(st, &lookup->row, side) != TF_OK) return TF_ERROR;
2082
+ int cmp = join_spill_compare_lookup_to_left(st, &lookup->row, left);
2083
+ if (cmp >= 0) return TF_OK;
2084
+ int adv = join_spill_reader_advance(lookup, st->spill_lookup_schema_types, st->spill_lookup_n_cols);
2085
+ if (adv < 0) return TF_ERROR;
2086
+ }
2087
+ }
2088
+
2089
+ static int join_spill_collect_lookup_run(join_state *st, const join_spill_row *left,
2090
+ tf_batch **run_out, tf_side_channels *side) {
2091
+ *run_out = NULL;
2092
+ if (join_spill_drain_lookup_before_left(st, left, side) != TF_OK) return TF_ERROR;
2093
+ int lookup_idx = join_spill_best_lookup_reader(st);
2094
+ if (lookup_idx < 0) return TF_OK;
2095
+ join_spill_reader *lookup = &st->spill_lookup_readers[lookup_idx];
2096
+ if (join_spill_count_lookup_key(st, &lookup->row, side) != TF_OK) return TF_ERROR;
2097
+ if (join_spill_compare_lookup_to_left(st, &lookup->row, left) != 0) return TF_OK;
2098
+
2099
+ size_t initial = st->max_matches_per_row < 16 ? st->max_matches_per_row : 16;
2100
+ tf_batch *run = join_spill_create_lookup_batch(st, initial ? initial : 16);
2101
+ if (!run) return TF_ERROR;
2102
+
2103
+ for (;;) {
2104
+ lookup_idx = join_spill_best_lookup_reader(st);
2105
+ if (lookup_idx < 0) break;
2106
+ lookup = &st->spill_lookup_readers[lookup_idx];
2107
+ int cmp = join_spill_compare_lookup_to_left(st, &lookup->row, left);
2108
+ if (cmp != 0) break;
2109
+ if (join_spill_count_lookup_key(st, &lookup->row, side) != TF_OK) { tf_batch_free(run); return TF_ERROR; }
2110
+ if (st->max_matches_per_row > 0 && run->n_rows >= st->max_matches_per_row) {
2111
+ int err_rc = join_limit_error_context(side, "max_matches_per_row",
2112
+ st->max_matches_per_row,
2113
+ run->n_rows + 1,
2114
+ "for spilled lookup key");
2115
+ tf_batch_free(run);
2116
+ if (err_rc != TF_OK) return TF_ERROR;
2117
+ return TF_ERROR;
2118
+ }
2119
+ size_t dst = run->n_rows;
2120
+ if (join_spill_row_to_batch_typed(run, dst, &lookup->row,
2121
+ st->spill_lookup_schema_types,
2122
+ st->spill_lookup_n_cols) != TF_OK) {
2123
+ tf_batch_free(run);
2124
+ return TF_ERROR;
2125
+ }
2126
+ if (tf_batch_expose_row(run, dst) != TF_OK) {
2127
+ tf_batch_free(run);
2128
+ return TF_ERROR;
2129
+ }
2130
+ int adv = join_spill_reader_advance(lookup, st->spill_lookup_schema_types, st->spill_lookup_n_cols);
2131
+ if (adv < 0) { tf_batch_free(run); return TF_ERROR; }
2132
+ }
2133
+ *run_out = run;
2134
+ return TF_OK;
2135
+ }
2136
+
2137
+ static int join_spill_produce_mutating_output_runs(join_state *st, tf_side_channels *side) {
2138
+ if (st->spill_left_buf && st->spill_left_buf->n_rows > 0 && join_spill_write_left_run(st) != TF_OK)
2139
+ return TF_ERROR;
2140
+ if (st->spill_left_buf) { tf_batch_free(st->spill_left_buf); st->spill_left_buf = NULL; }
2141
+ free(st->spill_left_ordinals); st->spill_left_ordinals = NULL; st->spill_left_ordinal_cap = 0;
2142
+
2143
+ if (join_spill_load_lookup_runs(st, side) != TF_OK) return TF_ERROR;
2144
+ if (st->spill_lookup_n_cols == 0) {
2145
+ if (join_write_error(side, "join spill: inner/left joins need a non-empty lookup to infer output schema") != TF_OK)
2146
+ return TF_ERROR;
2147
+ return TF_ERROR;
2148
+ }
2149
+ if (join_spill_open_readers(st, 0) != TF_OK) return TF_ERROR;
2150
+ if (join_spill_open_readers(st, 1) != TF_OK) return TF_ERROR;
2151
+
2152
+ for (;;) {
2153
+ int left_idx = join_spill_best_left_reader(st);
2154
+ if (left_idx < 0) break;
2155
+ join_spill_reader *left = &st->spill_left_readers[left_idx];
2156
+ char *left_key = join_spill_build_key(st, &left->row, 0);
2157
+ if (!left_key) return TF_ERROR;
2158
+
2159
+ tf_batch *lookup_run = NULL;
2160
+ if (join_spill_collect_lookup_run(st, &left->row, &lookup_run, side) != TF_OK) {
2161
+ free(left_key);
2162
+ return TF_ERROR;
2163
+ }
2164
+
2165
+ for (;;) {
2166
+ left_idx = join_spill_best_left_reader(st);
2167
+ if (left_idx < 0) break;
2168
+ left = &st->spill_left_readers[left_idx];
2169
+ char *cur_key = join_spill_build_key(st, &left->row, 0);
2170
+ if (!cur_key) { free(left_key); tf_batch_free(lookup_run); return TF_ERROR; }
2171
+ int same_key = strcmp(cur_key, left_key) == 0;
2172
+ free(cur_key);
2173
+ if (!same_key) break;
2174
+
2175
+ if (lookup_run && lookup_run->n_rows > 0) {
2176
+ for (size_t m = 0; m < lookup_run->n_rows; m++) {
2177
+ if (join_spill_append_joined_row(st, &left->row, lookup_run, m, side) != TF_OK) {
2178
+ free(left_key);
2179
+ tf_batch_free(lookup_run);
2180
+ return TF_ERROR;
2181
+ }
2182
+ }
2183
+ } else if (st->how == 1) {
2184
+ if (join_spill_append_joined_row(st, &left->row, NULL, 0, side) != TF_OK) {
2185
+ free(left_key);
2186
+ tf_batch_free(lookup_run);
2187
+ return TF_ERROR;
2188
+ }
2189
+ }
2190
+
2191
+ int adv_left = join_spill_reader_advance(left, st->spill_schema_types, st->spill_n_cols);
2192
+ if (adv_left < 0) { free(left_key); tf_batch_free(lookup_run); return TF_ERROR; }
2193
+ }
2194
+
2195
+ free(left_key);
2196
+ tf_batch_free(lookup_run);
2197
+ }
2198
+
2199
+ for (;;) {
2200
+ int lookup_idx = join_spill_best_lookup_reader(st);
2201
+ if (lookup_idx < 0) break;
2202
+ join_spill_reader *lookup = &st->spill_lookup_readers[lookup_idx];
2203
+ if (join_spill_count_lookup_key(st, &lookup->row, side) != TF_OK) return TF_ERROR;
2204
+ int adv = join_spill_reader_advance(lookup, st->spill_lookup_schema_types, st->spill_lookup_n_cols);
2205
+ if (adv < 0) return TF_ERROR;
2206
+ }
2207
+ if (st->spill_out_buf && st->spill_out_buf->n_rows > 0 && join_spill_write_output_run(st) != TF_OK)
2208
+ return TF_ERROR;
2209
+ join_spill_close_readers(st, 0);
2210
+ join_spill_close_readers(st, 1);
2211
+ join_spill_remove_paths(&st->spill_left_run_paths, &st->spill_n_left_runs, &st->spill_cap_left_runs);
2212
+ join_spill_remove_paths(&st->spill_lookup_run_paths, &st->spill_n_lookup_runs, &st->spill_cap_lookup_runs);
2213
+ return TF_OK;
2214
+ }
2215
+
2216
+ static int join_spill_produce_output_runs(join_state *st, tf_side_channels *side) {
2217
+ if (st->spill_key_merge_done) return TF_OK;
2218
+ if (!st->spill_has_schema) {
2219
+ st->spill_key_merge_done = 1;
2220
+ return TF_OK;
2221
+ }
2222
+ if (st->how < 2) {
2223
+ int rc = join_spill_produce_mutating_output_runs(st, side);
2224
+ if (rc != TF_OK) return rc;
2225
+ st->spill_key_merge_done = 1;
2226
+ return TF_OK;
2227
+ }
2228
+ if (st->spill_left_buf && st->spill_left_buf->n_rows > 0 && join_spill_write_left_run(st) != TF_OK)
2229
+ return TF_ERROR;
2230
+ if (st->spill_left_buf) { tf_batch_free(st->spill_left_buf); st->spill_left_buf = NULL; }
2231
+ free(st->spill_left_ordinals); st->spill_left_ordinals = NULL; st->spill_left_ordinal_cap = 0;
2232
+
2233
+ if (join_spill_load_lookup_runs(st, side) != TF_OK) return TF_ERROR;
2234
+ if (join_spill_open_readers(st, 0) != TF_OK) return TF_ERROR;
2235
+ if (join_spill_open_readers(st, 1) != TF_OK) return TF_ERROR;
2236
+
2237
+ for (;;) {
2238
+ int left_idx = join_spill_best_left_reader(st);
2239
+ if (left_idx < 0) break;
2240
+ join_spill_reader *left = &st->spill_left_readers[left_idx];
2241
+ int lookup_idx = join_spill_best_lookup_reader(st);
2242
+ while (lookup_idx >= 0) {
2243
+ join_spill_reader *lookup = &st->spill_lookup_readers[lookup_idx];
2244
+ if (join_spill_count_lookup_key(st, &lookup->row, side) != TF_OK) return TF_ERROR;
2245
+ int cmp = join_spill_compare_lookup_to_left(st, &lookup->row, &left->row);
2246
+ if (cmp >= 0) break;
2247
+ int adv = join_spill_reader_advance(lookup, &st->lookup_key_type, 1);
2248
+ if (adv < 0) return TF_ERROR;
2249
+ lookup_idx = join_spill_best_lookup_reader(st);
2250
+ }
2251
+ int has_match = 0;
2252
+ if (lookup_idx >= 0) {
2253
+ join_spill_reader *lookup = &st->spill_lookup_readers[lookup_idx];
2254
+ if (join_spill_count_lookup_key(st, &lookup->row, side) != TF_OK) return TF_ERROR;
2255
+ has_match = join_spill_compare_lookup_to_left(st, &lookup->row, &left->row) == 0;
2256
+ }
2257
+ int keep = (st->how == 2) ? has_match : !has_match;
2258
+ if (keep && join_spill_append_selected_row(st, &left->row, side) != TF_OK) return TF_ERROR;
2259
+ int adv_left = join_spill_reader_advance(left, st->spill_schema_types, st->spill_n_cols);
2260
+ if (adv_left < 0) return TF_ERROR;
2261
+ }
2262
+ for (;;) {
2263
+ int lookup_idx = join_spill_best_lookup_reader(st);
2264
+ if (lookup_idx < 0) break;
2265
+ join_spill_reader *lookup = &st->spill_lookup_readers[lookup_idx];
2266
+ if (join_spill_count_lookup_key(st, &lookup->row, side) != TF_OK) return TF_ERROR;
2267
+ int adv = join_spill_reader_advance(lookup, &st->lookup_key_type, 1);
2268
+ if (adv < 0) return TF_ERROR;
2269
+ }
2270
+ if (st->spill_out_buf && st->spill_out_buf->n_rows > 0 && join_spill_write_output_run(st) != TF_OK)
2271
+ return TF_ERROR;
2272
+ join_spill_close_readers(st, 0);
2273
+ join_spill_close_readers(st, 1);
2274
+ join_spill_remove_paths(&st->spill_left_run_paths, &st->spill_n_left_runs, &st->spill_cap_left_runs);
2275
+ join_spill_remove_paths(&st->spill_lookup_run_paths, &st->spill_n_lookup_runs, &st->spill_cap_lookup_runs);
2276
+ st->spill_key_merge_done = 1;
2277
+ return TF_OK;
2278
+ }
2279
+
2280
+ static int join_spill_begin_output_merge(join_state *st) {
2281
+ if (st->spill_output_merge_started) return TF_OK;
2282
+ st->spill_output_merge_started = 1;
2283
+ if (st->spill_out_buf) { tf_batch_free(st->spill_out_buf); st->spill_out_buf = NULL; }
2284
+ free(st->spill_out_ordinals); st->spill_out_ordinals = NULL; st->spill_out_ordinal_cap = 0;
2285
+ if (st->spill_n_out_runs == 0) {
2286
+ tf_spill_cleanup(st->spill);
2287
+ st->spill = NULL;
2288
+ st->spill_output_merge_done = 1;
2289
+ return TF_OK;
2290
+ }
2291
+ return join_spill_open_readers(st, 2);
2292
+ }
2293
+
2294
+ static int join_spill_output_next_batch(join_state *st, tf_batch **out, tf_side_channels *side) {
2295
+ *out = NULL;
2296
+ if (join_spill_produce_output_runs(st, side) != TF_OK) return TF_ERROR;
2297
+ if (join_spill_begin_output_merge(st) != TF_OK) return TF_ERROR;
2298
+ if (st->spill_output_merge_done) return TF_OK;
2299
+
2300
+ tf_batch *ob = join_spill_create_output_batch(st, st->output_batch_rows);
2301
+ if (!ob) return TF_ERROR;
2302
+ size_t out_n_cols = 0;
2303
+ const tf_type *out_types = join_spill_reader_types(st, 2, &out_n_cols);
2304
+ while (ob->n_rows < st->output_batch_rows) {
2305
+ int best = join_spill_best_output_reader(st);
2306
+ if (best < 0) break;
2307
+ join_spill_reader *reader = &st->spill_out_readers[best];
2308
+ size_t out_row = ob->n_rows;
2309
+ if (join_spill_row_to_batch_typed(ob, out_row, &reader->row,
2310
+ out_types, out_n_cols) != TF_OK) {
2311
+ tf_batch_free(ob);
2312
+ return TF_ERROR;
2313
+ }
2314
+ if (tf_batch_expose_row(ob, out_row) != TF_OK) {
2315
+ tf_batch_free(ob);
2316
+ return TF_ERROR;
2317
+ }
2318
+ int adv = join_spill_reader_advance(reader, out_types, out_n_cols);
2319
+ if (adv < 0) { tf_batch_free(ob); return TF_ERROR; }
2320
+ }
2321
+ if (ob->n_rows == 0) {
2322
+ tf_batch_free(ob);
2323
+ join_spill_close_readers(st, 2);
2324
+ join_spill_remove_paths(&st->spill_out_run_paths, &st->spill_n_out_runs, &st->spill_cap_out_runs);
2325
+ tf_spill_cleanup(st->spill);
2326
+ st->spill = NULL;
2327
+ st->spill_output_merge_done = 1;
2328
+ return TF_OK;
2329
+ }
2330
+ st->spill_output_batches++;
2331
+ st->spill_output_rows += ob->n_rows;
2332
+ *out = ob;
2333
+ return TF_OK;
2334
+ }
2335
+
2336
+ static int join_process_spill(join_state *st, tf_batch *in, tf_batch **out, tf_side_channels *side) {
2337
+ *out = NULL;
2338
+ if (join_spill_init_schema(st, in, side) != TF_OK) return TF_ERROR;
2339
+ for (size_t r = 0; r < in->n_rows; r++) {
2340
+ size_t dst = st->spill_left_buf->n_rows;
2341
+ if (tf_batch_copy_row(st->spill_left_buf, dst, in, r) != TF_OK) return TF_ERROR;
2342
+ if (join_ensure_ordinals(&st->spill_left_ordinals, &st->spill_left_ordinal_cap, dst + 1) != TF_OK) return TF_ERROR;
2343
+ st->spill_left_ordinals[dst] = st->spill_next_left_ordinal++;
2344
+ if (tf_batch_expose_row(st->spill_left_buf, dst) != TF_OK) return TF_ERROR;
2345
+ if (st->spill_left_buf->n_rows >= st->run_rows && join_spill_write_left_run(st) != TF_OK)
2346
+ return TF_ERROR;
2347
+ }
2348
+ return TF_OK;
2349
+ }
2350
+
2351
+ static size_t join_spill_retained_state_bytes(const join_state *st) {
2352
+ if (!st || !st->use_spill) return 0;
2353
+ size_t total = 0;
2354
+ total += join_batch_storage_bytes(st->spill_left_buf);
2355
+ total += join_batch_storage_bytes(st->spill_lookup_buf);
2356
+ total += join_batch_storage_bytes(st->spill_out_buf);
2357
+ total += st->spill_left_ordinal_cap * sizeof(uint64_t);
2358
+ total += st->spill_lookup_ordinal_cap * sizeof(uint64_t);
2359
+ total += st->spill_out_ordinal_cap * sizeof(uint64_t);
2360
+ total += st->spill_n_left_readers * sizeof(join_spill_reader);
2361
+ total += st->spill_n_lookup_readers * sizeof(join_spill_reader);
2362
+ total += st->spill_n_out_readers * sizeof(join_spill_reader);
2363
+ total += st->spill_last_lookup_key ? strlen(st->spill_last_lookup_key) + 1 : 0;
2364
+ return total;
2365
+ }
2366
+
2367
+ static int sorted_join_process(tf_step *self, tf_batch *in, tf_batch **out,
2368
+ tf_side_channels *side) {
2369
+ join_state *st = self->state;
2370
+ *out = NULL;
2371
+
2372
+ int left_ci = tf_batch_col_index(in, st->left_col);
2373
+ if (left_ci < 0) return TF_ERROR;
2374
+
2375
+ if (!st->loaded) {
2376
+ if (sorted_join_open(st, side) != TF_OK) return TF_ERROR;
2377
+ if (sorted_join_advance_lookup(st, side) != TF_OK) return TF_ERROR;
2378
+ if (st->how < 2) {
2379
+ if (!st->sorted_have_row) {
2380
+ if (join_write_error(side, "sorted join: mutating joins need a non-empty lookup to infer output schema") != TF_OK)
2381
+ return TF_ERROR;
2382
+ return TF_ERROR;
2383
+ }
2384
+ if (st->max_matches_per_row == 0) {
2385
+ if (join_write_error(side, "sorted join: inner/left joins need max_matches_per_row to bound the current lookup key run") != TF_OK)
2386
+ return TF_ERROR;
2387
+ return TF_ERROR;
2388
+ }
2389
+ }
2390
+ st->loaded = 1;
2391
+ }
2392
+
2393
+ size_t n_out_cols = (st->how >= 2) ? in->n_cols : in->n_cols + st->n_lookup_out;
2394
+ size_t out_cap = in->n_rows > 0 ? in->n_rows : 16;
2395
+ tf_batch *ob = tf_batch_create(n_out_cols, out_cap);
2396
+ if (!ob) return TF_ERROR;
2397
+
2398
+ for (size_t c = 0; c < in->n_cols; c++) {
2399
+ if (tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]) != TF_OK) {
2400
+ tf_batch_free(ob);
2401
+ return TF_ERROR;
2402
+ }
2403
+ }
2404
+ if (st->how < 2) {
2405
+ for (size_t k = 0; k < st->n_lookup_out; k++) {
2406
+ int lc = st->lookup_out_cols[k];
2407
+ if (tf_batch_set_schema(ob, in->n_cols + k,
2408
+ st->lookup_schema_names[lc],
2409
+ st->lookup_schema_types[lc]) != TF_OK) {
2410
+ tf_batch_free(ob);
2411
+ return TF_ERROR;
2412
+ }
2413
+ }
2414
+ }
2415
+
2416
+ for (size_t r = 0; r < in->n_rows; r++) {
2417
+ if (sorted_join_check_left_order(st, in, r, left_ci, side) != TF_OK) {
2418
+ tf_batch_free(ob);
2419
+ return TF_ERROR;
2420
+ }
2421
+ if (sorted_join_ensure_run_at_least(st, in, r, left_ci, side) != TF_OK) {
2422
+ tf_batch_free(ob);
2423
+ return TF_ERROR;
2424
+ }
2425
+
2426
+ int has_match = 0;
2427
+ if (st->have_right_run) {
2428
+ int cmp = 0;
2429
+ if (join_key_compare_value_to_cell(&st->right_run_key, in, r, left_ci, &cmp) != TF_OK) {
2430
+ int err_rc = join_write_error(side, "sorted join: left and lookup join key types differ");
2431
+ tf_batch_free(ob);
2432
+ if (err_rc != TF_OK) return TF_ERROR;
2433
+ return TF_ERROR;
2434
+ }
2435
+ has_match = (cmp == 0);
2436
+ }
2437
+
2438
+ if (st->how == 2 || st->how == 3) {
2439
+ int keep = (st->how == 2) ? has_match : !has_match;
2440
+ if (keep) {
2441
+ if (!join_reserve_output_row(st, side)) { tf_batch_free(ob); return TF_ERROR; }
2442
+ if (tf_batch_append_row(ob, in, r) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
2443
+ }
2444
+ } else if (has_match) {
2445
+ for (size_t m = 0; m < st->right_run->n_rows; m++) {
2446
+ if (!join_reserve_output_row(st, side)) { tf_batch_free(ob); return TF_ERROR; }
2447
+ if (tf_batch_append_row_with_selected_extra(ob, in, r, st->right_run, m,
2448
+ st->lookup_out_cols,
2449
+ st->n_lookup_out) != TF_OK) {
2450
+ tf_batch_free(ob);
2451
+ return TF_ERROR;
2452
+ }
2453
+ }
2454
+ } else if (st->how == 1) {
2455
+ if (!join_reserve_output_row(st, side)) { tf_batch_free(ob); return TF_ERROR; }
2456
+ if (tf_batch_append_row_with_null_extra(ob, in, r, st->n_lookup_out) != TF_OK) {
2457
+ tf_batch_free(ob);
2458
+ return TF_ERROR;
2459
+ }
2460
+ }
2461
+ }
2462
+
2463
+ if (ob->n_rows > 0)
2464
+ *out = ob;
2465
+ else
2466
+ tf_batch_free(ob);
2467
+
2468
+ return TF_OK;
2469
+ }
2470
+
2471
+ static int join_process(tf_step *self, tf_batch *in, tf_batch **out,
2472
+ tf_side_channels *side) {
2473
+ join_state *st = self->state;
2474
+ *out = NULL;
2475
+ if (st->sorted) return sorted_join_process(self, in, out, side);
2476
+ if (st->use_spill) return join_process_spill(st, in, out, side);
2477
+
2478
+ if (!st->loaded) {
2479
+ if (load_lookup(st, side) != TF_OK) return TF_ERROR;
2480
+ st->loaded = 1;
2481
+ }
2482
+
2483
+ int left_ci = tf_batch_col_index(in, st->left_col);
2484
+ if (left_ci < 0) return TF_ERROR;
2485
+ if (st->have_lookup_key_type && in->col_types[left_ci] != st->lookup_key_type) {
2486
+ if (join_write_error(side, "join: left and lookup join key types differ") != TF_OK)
2487
+ return TF_ERROR;
2488
+ return TF_ERROR;
2489
+ }
2490
+
2491
+ size_t n_out_cols = (st->how >= 2) ? in->n_cols : in->n_cols + st->n_lookup_out;
2492
+
2493
+ /* Filtering joins retain at most one output row per input row. Mutating
2494
+ * joins may grow if lookup keys have duplicate matches. */
2495
+ size_t out_cap = in->n_rows > 0 ? in->n_rows : 16;
2496
+ tf_batch *ob = tf_batch_create(n_out_cols, out_cap);
2497
+ if (!ob) return TF_ERROR;
2498
+
2499
+ /* Set schema: main columns, plus lookup columns for mutating joins. */
2500
+ int schema_rc = (st->how >= 2)
2501
+ ? tf_batch_clone_schema(ob, in)
2502
+ : tf_batch_clone_with_selected_extra_cols(ob, in, st->lookup,
2503
+ st->lookup_out_cols,
2504
+ st->n_lookup_out);
2505
+ if (schema_rc != TF_OK) {
2506
+ tf_batch_free(ob);
2507
+ return TF_ERROR;
2508
+ }
2509
+
2510
+ for (size_t r = 0; r < in->n_rows; r++) {
2511
+ char *key = format_join_key(in, r, left_ci);
2512
+ if (!key) {
2513
+ tf_batch_free(ob);
2514
+ return TF_ERROR;
2515
+ }
2516
+
2517
+ join_bucket *bucket = map_find(&st->map, key);
2518
+ free(key);
2519
+
2520
+ if (st->how == 2 || st->how == 3) {
2521
+ int keep = (st->how == 2) ? (bucket != NULL) : (bucket == NULL);
2522
+ if (keep) {
2523
+ if (!join_reserve_output_row(st, side)) { tf_batch_free(ob); return TF_ERROR; }
2524
+ if (tf_batch_append_row(ob, in, r) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
2525
+ }
2526
+ } else if (bucket) {
2527
+ if (st->max_matches_per_row > 0 && bucket->n_rows > st->max_matches_per_row) {
2528
+ int err_rc = join_limit_error_context(side, "max_matches_per_row",
2529
+ st->max_matches_per_row,
2530
+ bucket->n_rows,
2531
+ "for input row");
2532
+ tf_batch_free(ob);
2533
+ if (err_rc != TF_OK) return TF_ERROR;
2534
+ return TF_ERROR;
2535
+ }
2536
+ /* Emit one row per match */
2537
+ for (size_t m = 0; m < bucket->n_rows; m++) {
2538
+ size_t lr = bucket->rows[m];
2539
+ if (!join_reserve_output_row(st, side)) { tf_batch_free(ob); return TF_ERROR; }
2540
+ if (tf_batch_append_row_with_selected_extra(ob, in, r, st->lookup, lr,
2541
+ st->lookup_out_cols,
2542
+ st->n_lookup_out) != TF_OK) {
2543
+ tf_batch_free(ob);
2544
+ return TF_ERROR;
2545
+ }
2546
+ }
2547
+ } else if (st->how == 1) {
2548
+ /* Left join: emit main + nulls */
2549
+ if (!join_reserve_output_row(st, side)) { tf_batch_free(ob); return TF_ERROR; }
2550
+ if (tf_batch_append_row_with_null_extra(ob, in, r, st->n_lookup_out) != TF_OK) {
2551
+ tf_batch_free(ob);
2552
+ return TF_ERROR;
2553
+ }
2554
+ }
2555
+ /* Inner join + no match: skip */
2556
+ }
2557
+
2558
+ if (ob->n_rows > 0)
2559
+ *out = ob;
2560
+ else
2561
+ tf_batch_free(ob);
2562
+
2563
+ return TF_OK;
2564
+ }
2565
+
2566
+ static int join_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
2567
+ join_state *st = self ? self->state : NULL;
2568
+ *out = NULL;
2569
+ if (st && st->use_spill) return join_spill_output_next_batch(st, out, side);
2570
+ (void)side;
2571
+ return TF_OK;
2572
+ }
2573
+
2574
+ static int join_flush_next(tf_step *self, tf_batch **out, tf_side_channels *side) {
2575
+ join_state *st = self ? self->state : NULL;
2576
+ *out = NULL;
2577
+ if (st && st->use_spill) return join_spill_output_next_batch(st, out, side);
2578
+ (void)side;
2579
+ return TF_OK;
2580
+ }
2581
+
2582
+ static size_t join_type_storage_size(tf_type type) {
2583
+ switch (type) {
2584
+ case TF_TYPE_BOOL: return sizeof(uint8_t);
2585
+ case TF_TYPE_INT64: return sizeof(int64_t);
2586
+ case TF_TYPE_FLOAT64: return sizeof(double);
2587
+ case TF_TYPE_STRING: return sizeof(char *);
2588
+ case TF_TYPE_DATE: return sizeof(int32_t);
2589
+ case TF_TYPE_TIMESTAMP: return sizeof(int64_t);
2590
+ default: return 0;
2591
+ }
2592
+ }
2593
+
2594
+ static size_t join_batch_storage_bytes(const tf_batch *b) {
2595
+ if (!b) return 0;
2596
+ size_t total = sizeof(*b) +
2597
+ b->n_cols * (sizeof(char *) + sizeof(tf_type) +
2598
+ sizeof(void *) + sizeof(uint8_t *));
2599
+ for (size_t c = 0; c < b->n_cols; c++) {
2600
+ if (b->col_names && b->col_names[c]) total += strlen(b->col_names[c]) + 1;
2601
+ size_t cell_size = join_type_storage_size(b->col_types[c]);
2602
+ total += cell_size * b->capacity;
2603
+ total += b->capacity;
2604
+ if (b->col_types[c] == TF_TYPE_STRING) {
2605
+ for (size_t r = 0; r < b->n_rows; r++) {
2606
+ if (!tf_batch_is_null(b, r, c)) {
2607
+ const char *s = tf_batch_get_string(b, r, c);
2608
+ if (s) total += strlen(s) + 1;
2609
+ }
2610
+ }
2611
+ }
2612
+ }
2613
+ return total;
2614
+ }
2615
+
2616
+ static size_t join_hash_map_retained_bytes(const join_hash_map *map) {
2617
+ if (!map) return 0;
2618
+ return map->n_buckets * sizeof(join_bucket) +
2619
+ map->key_bytes +
2620
+ map->row_ref_capacity * sizeof(size_t);
2621
+ }
2622
+
2623
+ static size_t join_unsorted_retained_state_bytes(const join_hash_map *map,
2624
+ const tf_batch *lookup,
2625
+ size_t n_lookup_out) {
2626
+ return join_hash_map_retained_bytes(map) +
2627
+ join_batch_storage_bytes(lookup) +
2628
+ n_lookup_out * sizeof(int);
2629
+ }
2630
+
2631
+ static int join_check_unsorted_state_bytes(const join_state *st,
2632
+ const join_hash_map *map,
2633
+ const tf_batch *lookup,
2634
+ size_t n_lookup_out,
2635
+ tf_side_channels *side) {
2636
+ if (!st || st->max_state_bytes == 0) return TF_OK;
2637
+ size_t retained = join_unsorted_retained_state_bytes(map, lookup, n_lookup_out);
2638
+ if (retained <= st->max_state_bytes) return TF_OK;
2639
+ if (join_limit_error_context(side, "max_state_bytes", st->max_state_bytes, retained,
2640
+ "while tracking lookup state") != TF_OK) {
2641
+ return TF_ERROR;
2642
+ }
2643
+ return TF_ERROR;
2644
+ }
2645
+
2646
+ static size_t join_key_value_retained_bytes(const join_key_value *v) {
2647
+ if (!v || !v->valid || v->is_null) return 0;
2648
+ if (v->type == TF_TYPE_STRING) return v->v.str ? strlen(v->v.str) + 1 : 0;
2649
+ switch (v->type) {
2650
+ case TF_TYPE_BOOL: return sizeof(uint8_t);
2651
+ case TF_TYPE_INT64: return sizeof(int64_t);
2652
+ case TF_TYPE_FLOAT64: return sizeof(double);
2653
+ case TF_TYPE_DATE: return sizeof(int32_t);
2654
+ case TF_TYPE_TIMESTAMP: return sizeof(int64_t);
2655
+ default: return 0;
2656
+ }
2657
+ }
2658
+
2659
+ static size_t join_sorted_retained_state_bytes(const join_state *st) {
2660
+ if (!st) return 0;
2661
+ size_t total = join_batch_storage_bytes(st->right_run);
2662
+ total += join_key_value_retained_bytes(&st->right_run_key);
2663
+ total += join_key_value_retained_bytes(&st->prev_lookup_key);
2664
+ total += join_key_value_retained_bytes(&st->prev_left_key);
2665
+ total += st->n_lookup_out * sizeof(int);
2666
+ total += st->n_lookup_schema_cols * (sizeof(char *) + sizeof(tf_type));
2667
+ for (size_t i = 0; i < st->n_lookup_schema_cols; i++) {
2668
+ if (st->lookup_schema_names && st->lookup_schema_names[i]) {
2669
+ total += strlen(st->lookup_schema_names[i]) + 1;
2670
+ }
2671
+ }
2672
+ return total;
2673
+ }
2674
+
2675
+ static int join_append_stats(tf_step *self, tf_buffer *out) {
2676
+ if (!self || !self->state || !out) return TF_ERROR;
2677
+ join_state *st = self->state;
2678
+ size_t lookup_keys = 0;
2679
+ size_t lookup_key_bytes = 0;
2680
+ size_t lookup_row_refs = 0;
2681
+ size_t lookup_batch_bytes = 0;
2682
+ size_t retained = 0;
2683
+ if (st->use_spill) {
2684
+ lookup_keys = st->spill_lookup_keys;
2685
+ lookup_key_bytes = st->spill_lookup_key_bytes;
2686
+ retained = join_spill_retained_state_bytes(st);
2687
+ } else if (st->sorted) {
2688
+ lookup_keys = st->have_right_run ? 1u : 0u;
2689
+ lookup_key_bytes = join_key_value_retained_bytes(&st->right_run_key);
2690
+ lookup_row_refs = st->right_run ? st->right_run->capacity : 0u;
2691
+ lookup_batch_bytes = join_batch_storage_bytes(st->right_run);
2692
+ retained = join_sorted_retained_state_bytes(st);
2693
+ } else {
2694
+ lookup_keys = st->map.count;
2695
+ lookup_key_bytes = st->map.key_bytes;
2696
+ lookup_row_refs = st->map.row_ref_capacity;
2697
+ lookup_batch_bytes = join_batch_storage_bytes(st->lookup);
2698
+ retained = join_unsorted_retained_state_bytes(&st->map, st->lookup, st->n_lookup_out);
2699
+ }
2700
+ char buf[384];
2701
+ snprintf(buf, sizeof(buf),
2702
+ ",\"lookup_rows\":%zu,\"lookup_keys\":%zu,"
2703
+ "\"lookup_key_bytes\":%zu,\"lookup_row_refs\":%zu,"
2704
+ "\"lookup_batch_bytes\":%zu,\"retained_state_bytes\":%zu,"
2705
+ "\"max_state_bytes\":%zu",
2706
+ st->lookup_rows, lookup_keys, lookup_key_bytes, lookup_row_refs,
2707
+ lookup_batch_bytes, retained, st->max_state_bytes);
2708
+ if (tf_buffer_write_str(out, buf) != TF_OK) return TF_ERROR;
2709
+ if (st->use_spill) {
2710
+ snprintf(buf, sizeof(buf),
2711
+ ",\"spill_bytes\":%zu,\"spill_runs\":%zu,"
2712
+ "\"spill_output_batches\":%zu,\"spill_output_rows\":%zu,"
2713
+ "\"spill_kept_rows\":%zu",
2714
+ st->spill_bytes, st->spill_runs,
2715
+ st->spill_output_batches, st->spill_output_rows,
2716
+ st->spill_kept_rows);
2717
+ return tf_buffer_write_str(out, buf);
2718
+ }
2719
+ return TF_OK;
2720
+ }
2721
+
2722
+ static void join_state_free(join_state *st) {
2723
+ if (!st) return;
2724
+ free(st->file);
2725
+ free(st->validated_file);
2726
+ free(st->left_col);
2727
+ free(st->right_col);
2728
+ if (st->lookup) tf_batch_free(st->lookup);
2729
+ sorted_join_close(st);
2730
+ sorted_join_clear_right_run(st);
2731
+ join_key_value_clear(&st->prev_lookup_key);
2732
+ join_key_value_clear(&st->prev_left_key);
2733
+ sorted_join_free_schema(st);
2734
+ free(st->lookup_out_cols);
2735
+ map_free(&st->map);
2736
+ if (st->spill_left_buf) tf_batch_free(st->spill_left_buf);
2737
+ if (st->spill_lookup_buf) tf_batch_free(st->spill_lookup_buf);
2738
+ if (st->spill_out_buf) tf_batch_free(st->spill_out_buf);
2739
+ free(st->spill_left_ordinals);
2740
+ free(st->spill_lookup_ordinals);
2741
+ free(st->spill_out_ordinals);
2742
+ join_spill_close_readers(st, 0);
2743
+ join_spill_close_readers(st, 1);
2744
+ join_spill_close_readers(st, 2);
2745
+ join_spill_remove_paths(&st->spill_left_run_paths, &st->spill_n_left_runs, &st->spill_cap_left_runs);
2746
+ join_spill_remove_paths(&st->spill_lookup_run_paths, &st->spill_n_lookup_runs, &st->spill_cap_lookup_runs);
2747
+ join_spill_remove_paths(&st->spill_out_run_paths, &st->spill_n_out_runs, &st->spill_cap_out_runs);
2748
+ for (size_t i = 0; i < st->spill_n_cols; i++) free(st->spill_schema_names ? st->spill_schema_names[i] : NULL);
2749
+ free(st->spill_schema_names);
2750
+ free(st->spill_schema_types);
2751
+ for (size_t i = 0; i < st->spill_lookup_n_cols; i++) free(st->spill_lookup_schema_names ? st->spill_lookup_schema_names[i] : NULL);
2752
+ free(st->spill_lookup_schema_names);
2753
+ free(st->spill_lookup_schema_types);
2754
+ for (size_t i = 0; i < st->spill_output_n_cols; i++) free(st->spill_output_schema_names ? st->spill_output_schema_names[i] : NULL);
2755
+ free(st->spill_output_schema_names);
2756
+ free(st->spill_output_schema_types);
2757
+ free(st->spill_last_lookup_key);
2758
+ tf_spill_cleanup(st->spill);
2759
+ free(st->spill_dir);
2760
+ free(st);
2761
+ }
2762
+
2763
+ static void join_destroy(tf_step *self) {
2764
+ if (self) {
2765
+ join_state_free(self->state);
2766
+ free(self);
2767
+ }
2768
+ }
2769
+
2770
+ static int parse_positive_size_arg(const cJSON *args, const char *name,
2771
+ size_t *out, const char *op_name) {
2772
+ return tf_json_get_size_arg(args, name, 1, TF_MAX_COUNT_ARG, out, op_name);
2773
+ }
2774
+
2775
+ tf_step *tf_join_create(const cJSON *args) {
2776
+ if (!args) return NULL;
2777
+ cJSON *file_j = cJSON_GetObjectItemCaseSensitive(args, "file");
2778
+ cJSON *on_j = cJSON_GetObjectItemCaseSensitive(args, "on");
355
2779
  if (!cJSON_IsString(file_j) || !cJSON_IsString(on_j)) return NULL;
356
2780
 
357
2781
  join_state *st = calloc(1, sizeof(join_state));
358
2782
  if (!st) return NULL;
2783
+ st->output_batch_rows = JOIN_DEFAULT_OUTPUT_ROWS;
359
2784
 
360
2785
  st->file = strdup(file_j->valuestring);
2786
+ if (!st->file) { join_state_free(st); return NULL; }
2787
+ const char *validated = tf_policy_validated_path_arg(args, "file");
2788
+ if (validated) {
2789
+ st->validated_file = strdup(validated);
2790
+ if (!st->validated_file) { join_state_free(st); return NULL; }
2791
+ }
361
2792
 
362
2793
  /* Parse "on" field: "col" or "left_col=right_col" */
363
2794
  const char *on = on_j->valuestring;
@@ -369,16 +2800,103 @@ tf_step *tf_join_create(const cJSON *args) {
369
2800
  st->left_col = strdup(on);
370
2801
  st->right_col = strdup(on);
371
2802
  }
2803
+ if (!st->left_col || !st->right_col) { join_state_free(st); return NULL; }
2804
+
2805
+ cJSON *sorted_j = cJSON_GetObjectItemCaseSensitive(args, "sorted");
2806
+ if (sorted_j && !cJSON_IsBool(sorted_j)) {
2807
+ tf_set_last_error("join: sorted must be true or false");
2808
+ join_state_free(st);
2809
+ return NULL;
2810
+ }
2811
+ st->sorted = cJSON_IsTrue(sorted_j) ? 1 : 0;
372
2812
 
373
2813
  cJSON *how_j = cJSON_GetObjectItemCaseSensitive(args, "how");
374
- if (cJSON_IsString(how_j) && strcmp(how_j->valuestring, "left") == 0)
375
- st->how = 1;
2814
+ if (cJSON_IsString(how_j)) {
2815
+ if (strcmp(how_j->valuestring, "inner") == 0) st->how = 0;
2816
+ else if (strcmp(how_j->valuestring, "left") == 0) st->how = 1;
2817
+ else if (strcmp(how_j->valuestring, "semi") == 0) st->how = 2;
2818
+ else if (strcmp(how_j->valuestring, "anti") == 0) st->how = 3;
2819
+ else {
2820
+ tf_set_last_error("join: how must be inner, left, semi, or anti");
2821
+ join_state_free(st);
2822
+ return NULL;
2823
+ }
2824
+ }
2825
+
2826
+ if (parse_positive_size_arg(args, "max_lookup_rows", &st->max_lookup_rows, "join") < 0 ||
2827
+ parse_positive_size_arg(args, "max_lookup_keys", &st->max_lookup_keys, "join") < 0 ||
2828
+ parse_positive_size_arg(args, "max_lookup_bytes", &st->max_lookup_bytes, "join") < 0 ||
2829
+ parse_positive_size_arg(args, "max_state_bytes", &st->max_state_bytes, "join") < 0 ||
2830
+ parse_positive_size_arg(args, "max_matches_per_row", &st->max_matches_per_row, "join") < 0 ||
2831
+ parse_positive_size_arg(args, "max_output_rows", &st->max_output_rows, "join") < 0) {
2832
+ join_state_free(st);
2833
+ return NULL;
2834
+ }
376
2835
 
377
- tf_step *step = malloc(sizeof(tf_step));
378
- if (!step) { join_destroy(&(tf_step){.state = st}); return NULL; }
2836
+ cJSON *spill_dir_j = cJSON_GetObjectItemCaseSensitive(args, "spill_dir");
2837
+ if (cJSON_IsString(spill_dir_j) && spill_dir_j->valuestring && spill_dir_j->valuestring[0]) {
2838
+ st->use_spill = 1;
2839
+ st->spill_dir = strdup(spill_dir_j->valuestring);
2840
+ if (!st->spill_dir) { join_state_free(st); return NULL; }
2841
+ if (tf_spill_session_create(st->spill_dir, &st->spill) != TF_OK) { join_state_free(st); return NULL; }
2842
+ size_t parsed_size = 0;
2843
+ int has_spill_memory = tf_json_get_size_arg(args, "spill_memory_bytes",
2844
+ 1, TF_MAX_SPILL_MEMORY_BYTES,
2845
+ &parsed_size, "join");
2846
+ if (has_spill_memory < 0) { join_state_free(st); return NULL; }
2847
+ if (has_spill_memory > 0) st->spill_memory_bytes = parsed_size;
2848
+ int has_spill_rows = tf_json_get_size_arg(args, "spill_run_rows",
2849
+ 1, TF_MAX_SPILL_RUN_ROWS,
2850
+ &parsed_size, "join");
2851
+ if (has_spill_rows < 0) { join_state_free(st); return NULL; }
2852
+ if (has_spill_rows > 0) st->configured_run_rows = parsed_size;
2853
+ int has_output_rows = tf_json_get_size_arg(args, "spill_output_rows",
2854
+ 1, TF_MAX_SPILL_OUTPUT_ROWS,
2855
+ &parsed_size, "join");
2856
+ if (has_output_rows < 0) { join_state_free(st); return NULL; }
2857
+ if (has_output_rows > 0) st->output_batch_rows = parsed_size;
2858
+ }
2859
+
2860
+ if (st->use_spill && st->sorted) {
2861
+ tf_set_last_error("join: spill_dir and sorted=true are mutually exclusive");
2862
+ join_state_free(st);
2863
+ return NULL;
2864
+ }
2865
+ if (st->use_spill && st->how < 2 && st->max_matches_per_row == 0) {
2866
+ tf_set_last_error("join: spill_dir for inner/left joins needs max_matches_per_row");
2867
+ join_state_free(st);
2868
+ return NULL;
2869
+ }
2870
+
2871
+ tf_step *step = calloc(1, sizeof(tf_step));
2872
+ if (!step) { join_state_free(st); return NULL; }
379
2873
  step->process = join_process;
380
2874
  step->flush = join_flush;
2875
+ step->flush_next = st->use_spill ? join_flush_next : NULL;
2876
+ step->append_stats = join_append_stats;
381
2877
  step->destroy = join_destroy;
382
2878
  step->state = st;
383
2879
  return step;
384
2880
  }
2881
+
2882
+ static tf_step *tf_join_create_with_how(const cJSON *args, const char *how) {
2883
+ if (!args) return NULL;
2884
+ cJSON *copy = cJSON_Duplicate(args, 1);
2885
+ if (!copy) return NULL;
2886
+ cJSON_DeleteItemFromObjectCaseSensitive(copy, "how");
2887
+ if (tf_json_add_string(copy, "how", how) != TF_OK) {
2888
+ cJSON_Delete(copy);
2889
+ return NULL;
2890
+ }
2891
+ tf_step *step = tf_join_create(copy);
2892
+ cJSON_Delete(copy);
2893
+ return step;
2894
+ }
2895
+
2896
+ tf_step *tf_semi_join_create(const cJSON *args) {
2897
+ return tf_join_create_with_how(args, "semi");
2898
+ }
2899
+
2900
+ tf_step *tf_anti_join_create(const cJSON *args) {
2901
+ return tf_join_create_with_how(args, "anti");
2902
+ }