280
PY
281
fi
282
}
283
+
284
+# Assert column visibility rules:
285
+# - If fewer than 5 columns exist, ALL must be visible
286
+# - Otherwise, at least 5 columns must be visible
287
+assert_column_visibility() {
288
+ local input="$1"
289
+ local context="${2:-response}"
290
+
291
+ if command -v python3 >/dev/null 2>&1; then
292
+ python3 - "$input" "$context" <<'PY'
293
+import json
294
+import sys
295
+
296
+path = sys.argv[1]
297
+context = sys.argv[2]
298
+
299
+with open(path, "r", encoding="utf-8") as fh:
300
+ doc = json.load(fh)
301
+
302
+columns = doc.get("columns") or {}
303
+
304
+# Build list of columns with their visibility
305
+col_list = []
306
+if isinstance(columns, dict):
307
+ for field, col in columns.items():
308
+ if isinstance(col, dict):
309
+ col_list.append({"field": field, "visible": col.get("visible", False)})
310
+else:
311
+ for col in columns:
312
+ if isinstance(col, dict):
313
+ col_list.append({"field": col.get("field", ""), "visible": col.get("visible", False)})
314
+
315
+total = len(col_list)
316
+visible_count = sum(1 for c in col_list if c["visible"] is True)
317
+
318
+if total < 5:
319
+ # All columns must be visible
320
+ if visible_count != total:
321
+ invisible = [c["field"] for c in col_list if c["visible"] is not True]
322
+ raise SystemExit(
323
+ f"{context}: All {total} columns must be visible (fewer than 5 total), "
324
+ f"but only {visible_count} are visible. Invisible columns: {invisible}"
325
+ )
326
+else:
327
+ # At least 5 columns must be visible
328
+ if visible_count < 5:
329
+ invisible = [c["field"] for c in col_list if c["visible"] is not True]
330
+ raise SystemExit(
331
+ f"{context}: At least 5 columns must be visible, "
332
+ f"but only {visible_count} of {total} are visible. Invisible columns: {invisible}"
333
+ )
334
+PY
335
+ else
336
+ python - "$input" "$context" <<'PY'
337
+import json
338
+import sys
339
+
340
+path = sys.argv[1]
341
+context = sys.argv[2]
342
+
343
+with open(path, "r") as fh:
344
+ doc = json.load(fh)
345
+
346
+columns = doc.get("columns") or {}
347
+
348
+col_list = []
349
+if isinstance(columns, dict):
350
+ for field, col in columns.items():
351
+ if isinstance(col, dict):
352
+ col_list.append({"field": field, "visible": col.get("visible", False)})
353
+else:
354
+ for col in columns:
355
+ if isinstance(col, dict):
356
+ col_list.append({"field": col.get("field", ""), "visible": col.get("visible", False)})
357
+
358
+total = len(col_list)
359
+visible_count = sum(1 for c in col_list if c["visible"] is True)
360
+
361
+if total < 5:
362
+ if visible_count != total:
363
+ invisible = [c["field"] for c in col_list if c["visible"] is not True]
364
+ raise SystemExit(
365
+ "%s: All %d columns must be visible (fewer than 5 total), "
366
+ "but only %d are visible. Invisible columns: %s" % (context, total, visible_count, invisible)
367
+ )
368
+else:
369
+ if visible_count < 5:
370
+ invisible = [c["field"] for c in col_list if c["visible"] is not True]
371
+ raise SystemExit(
372
+ "%s: At least 5 columns must be visible, "
373
+ "but only %d of %d are visible. Invisible columns: %s" % (context, visible_count, total, invisible)
374
+ )
375
+PY
376
+ fi
377
+}
378
+
379
+# Assert UniqueKey column has non-empty values in all rows
380
+assert_unique_key_populated() {
381
+ local input="$1"
382
+ local context="${2:-response}"
383
+
384
+ if command -v python3 >/dev/null 2>&1; then
385
+ python3 - "$input" "$context" <<'PY'
386
+import json
387
+import sys
388
+
389
+path = sys.argv[1]
390
+context = sys.argv[2]
391
+
392
+with open(path, "r", encoding="utf-8") as fh:
393
+ doc = json.load(fh)
394
+
395
+columns = doc.get("columns") or {}
396
+data = doc.get("data") or []
397
+
398
+# Find UniqueKey column index
399
+unique_key_idx = None
400
+unique_key_field = None
401
+
402
+if isinstance(columns, dict):
403
+ for field, col in columns.items():
404
+ if isinstance(col, dict) and col.get("unique_key") is True:
405
+ unique_key_idx = col.get("index")
406
+ unique_key_field = field
407
+ break
408
+else:
409
+ for idx, col in enumerate(columns):
410
+ if isinstance(col, dict) and col.get("unique_key") is True:
411
+ unique_key_idx = idx
412
+ unique_key_field = col.get("field", f"column_{idx}")
413
+ break
414
+
415
+if unique_key_idx is None:
416
+ # No UniqueKey column defined, skip check
417
+ sys.exit(0)
418
+
419
+# Check all rows have non-empty UniqueKey
420
+for i, row in enumerate(data):
421
+ if unique_key_idx >= len(row):
422
+ raise SystemExit(f"{context}: Row {i} missing UniqueKey column (index {unique_key_idx})")
423
+ val = row[unique_key_idx]
424
+ if val is None or str(val).strip() == "":
425
+ raise SystemExit(
426
+ f"{context}: Row {i} has empty UniqueKey ({unique_key_field}) - "
427
+ f"deduplication will fail. This may indicate NULL digest handling is broken."
428
+ )
429
+PY
430
+ else
431
+ python - "$input" "$context" <<'PY'
432
+import json
433
+import sys
434
+
435
+path = sys.argv[1]
436
+context = sys.argv[2]
437
+
438
+with open(path, "r") as fh:
439
+ doc = json.load(fh)
440
+
441
+columns = doc.get("columns") or {}
442
+data = doc.get("data") or []
443
+
444
+unique_key_idx = None
445
+unique_key_field = None
446
+
447
+if isinstance(columns, dict):
448
+ for field, col in columns.items():
449
+ if isinstance(col, dict) and col.get("unique_key") is True:
450
+ unique_key_idx = col.get("index")
451
+ unique_key_field = field
452
+ break
453
+else:
454
+ for idx, col in enumerate(columns):
455
+ if isinstance(col, dict) and col.get("unique_key") is True:
456
+ unique_key_idx = idx
457
+ unique_key_field = col.get("field", "column_%d" % idx)
458
+ break
459
+
460
+if unique_key_idx is None:
461
+ sys.exit(0)
462
+
463
+for i, row in enumerate(data):
464
+ if unique_key_idx >= len(row):
465
+ raise SystemExit("%s: Row %d missing UniqueKey column (index %d)" % (context, i, unique_key_idx))
466
+ val = row[unique_key_idx]
467
+ if val is None or str(val).strip() == "":
468
+ raise SystemExit(
469
+ "%s: Row %d has empty UniqueKey (%s) - "
470
+ "deduplication will fail. This may indicate NULL digest handling is broken." % (context, i, unique_key_field)
471
+ )
472
+PY
473
+ fi
474
+}