@@ -428,5 +428,145 @@ final class QueryViewModelTests: XCTestCase {
428428 XCTAssertFalse(csv.contains("secret"))
429429 XCTAssertFalse(csv.contains("hidden"))
430430 }
431+
432+ // MARK: - Empty column detection
433+
434+ func testEmptyColumnsDetectsAllNulls() {
435+ let vm = QueryViewModel()
436+ let records: [LogRecord] = [
437+ ["a": .string("val"), "b": .null],
438+ ["a": .string("val2"), "b": .null],
439+ ]
440+ let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
441+ XCTAssertEqual(empty, ["b"])
442+ }
443+
444+ func testEmptyColumnsDetectsMissingKeys() {
445+ let vm = QueryViewModel()
446+ let records: [LogRecord] = [
447+ ["a": .string("val")],
448+ ["a": .string("val2")],
449+ ]
450+ let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
451+ XCTAssertEqual(empty, ["b"])
452+ }
453+
454+ func testEmptyColumnsDetectsEmptyStrings() {
455+ let vm = QueryViewModel()
456+ let records: [LogRecord] = [
457+ ["a": .string("val"), "b": .string("")],
458+ ["a": .string("val2"), "b": .string("")],
459+ ]
460+ let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
461+ XCTAssertEqual(empty, ["b"])
462+ }
463+
464+ func testEmptyColumnsMixedNullAndMissing() {
465+ let vm = QueryViewModel()
466+ let records: [LogRecord] = [
467+ ["a": .string("val"), "b": .null],
468+ ["a": .string("val2")],
469+ ["a": .string("val3"), "b": .string("")],
470+ ]
471+ let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
472+ XCTAssertEqual(empty, ["b"])
473+ }
474+
475+ func testEmptyColumnsNotEmptyIfAnyRecordHasValue() {
476+ let vm = QueryViewModel()
477+ let records: [LogRecord] = [
478+ ["a": .string("val"), "b": .null],
479+ ["a": .string("val2"), "b": .string("data")],
480+ ]
481+ let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
482+ XCTAssertTrue(empty.isEmpty)
483+ }
484+
485+ func testEmptyColumnsNoRecords() {
486+ let vm = QueryViewModel()
487+ let empty = vm.emptyColumns(in: [], columns: ["a", "b"])
488+ XCTAssertEqual(empty, ["a", "b"])
489+ }
490+
491+ func testEmptyColumnsNoColumns() {
492+ let vm = QueryViewModel()
493+ let records: [LogRecord] = [["a": .string("val")]]
494+ let empty = vm.emptyColumns(in: records, columns: [])
495+ XCTAssertTrue(empty.isEmpty)
496+ }
497+
498+ func testAutoHiddenColumnsExcludedFromVisibleColumns() {
499+ let vm = QueryViewModel()
500+ vm.columns = ["a", "b", "c"]
501+ vm.columnOrder = ["a", "b", "c"]
502+ vm.autoHiddenColumns = ["b"]
503+ XCTAssertEqual(vm.visibleColumns, ["a", "c"])
504+ }
505+
506+ func testToggleAutoHiddenColumnShowsIt() {
507+ let vm = QueryViewModel()
508+ vm.columns = ["a", "b", "c"]
509+ vm.columnOrder = ["a", "b", "c"]
510+ vm.autoHiddenColumns = ["b"]
511+ vm.toggleColumnVisibility("b")
512+ XCTAssertFalse(vm.autoHiddenColumns.contains("b"))
513+ XCTAssertEqual(vm.visibleColumns, ["a", "b", "c"])
514+ }
515+
516+ func testShowAllColumnsClearsAutoHidden() {
517+ let vm = QueryViewModel()
518+ vm.columns = ["a", "b", "c"]
519+ vm.columnOrder = ["a", "b", "c"]
520+ vm.hiddenColumns = ["a"]
521+ vm.autoHiddenColumns = ["b"]
522+ vm.showAllColumns()
523+ XCTAssertTrue(vm.hiddenColumns.isEmpty)
524+ XCTAssertTrue(vm.autoHiddenColumns.isEmpty)
525+ XCTAssertEqual(vm.visibleColumns, ["a", "b", "c"])
526+ }
527+
528+ func testResetColumnConfigClearsAutoHidden() {
529+ let vm = QueryViewModel()
530+ vm.columns = ["a", "b", "c"]
531+ vm.columnOrder = ["c", "b", "a"]
532+ vm.autoHiddenColumns = ["b"]
533+ vm.resetColumnConfig()
534+ XCTAssertEqual(vm.columnOrder, ["a", "b", "c"])
535+ XCTAssertTrue(vm.autoHiddenColumns.isEmpty)
536+ }
537+
538+ func testClearResultsClearsAutoHidden() {
539+ let vm = QueryViewModel()
540+ vm.columns = ["a", "b"]
541+ vm.columnOrder = ["a", "b"]
542+ vm.autoHiddenColumns = ["b"]
543+ vm.clearResults()
544+ XCTAssertTrue(vm.autoHiddenColumns.isEmpty)
545+ }
546+
547+ func testCSVExportExcludesAutoHiddenColumns() {
548+ let vm = QueryViewModel()
549+ vm.results = [
550+ ["name": .string("Alice"), "age": .int(30), "empty_col": .null],
551+ ]
552+ vm.columns = ["name", "age", "empty_col"]
553+ vm.columnOrder = ["name", "age", "empty_col"]
554+ vm.autoHiddenColumns = ["empty_col"]
555+ let csv = vm.exportAsCSV()
556+
557+ let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
558+ XCTAssertEqual(lines[0], "name,age")
559+ XCTAssertFalse(csv.contains("empty_col"))
560+ }
561+
562+ func testBoolZeroValuesNotTreatedAsEmpty() {
563+ let vm = QueryViewModel()
564+ let records: [LogRecord] = [
565+ ["a": .bool(false)],
566+ ["a": .int(0)],
567+ ]
568+ let empty = vm.emptyColumns(in: records, columns: ["a"])
569+ XCTAssertTrue(empty.isEmpty)
570+ }
431571}
432572