|
| 1 | +using Microsoft.Extensions.DependencyInjection; |
| 2 | +using SharpCoreDB; |
| 3 | +using SharpCoreDB.Services; |
| 4 | + |
| 5 | +namespace SharpCoreDB.DemoJoinsSubQ; |
| 6 | + |
| 7 | +/// <summary> |
| 8 | +/// Validates JOIN query results to diagnose issues. |
| 9 | +/// </summary> |
| 10 | +internal sealed class JoinValidator |
| 11 | +{ |
| 12 | + private readonly Interfaces.IDatabase db; |
| 13 | + |
| 14 | + public JoinValidator(Interfaces.IDatabase db) |
| 15 | + { |
| 16 | + this.db = db; |
| 17 | + } |
| 18 | + |
| 19 | + public void ValidateLeftJoinNoMatches() |
| 20 | + { |
| 21 | + Console.WriteLine("\n═══ Validating LEFT JOIN (no matches) ═══"); |
| 22 | + |
| 23 | + // ✅ FIXED: Test with a condition that will never match |
| 24 | + // customer.id (1-5) will never equal payment.order_id (1,2,2,5,999) |
| 25 | + // when we filter to just customer IDs that don't have matching order_ids |
| 26 | + var sql = @"SELECT c.id, c.name, p.id as payment_id |
| 27 | + FROM customers c |
| 28 | + LEFT JOIN payments p ON c.id = p.order_id |
| 29 | + WHERE c.id IN (3, 4)"; // These customers have no matching payments |
| 30 | + |
| 31 | + var results = db.ExecuteQuery(sql); |
| 32 | + |
| 33 | + Console.WriteLine($"✓ Returned {results.Count} rows"); |
| 34 | + if (results.Count > 0) |
| 35 | + Console.WriteLine($"✓ Columns: {string.Join(", ", results[0].Keys)}"); |
| 36 | + |
| 37 | + // Validate expectations |
| 38 | + bool hasPaymentIdColumn = results.Count > 0 && results[0].ContainsKey("payment_id"); |
| 39 | + bool allPaymentIdsNull = results.All(r => !r.ContainsKey("payment_id") || r["payment_id"] is DBNull || r["payment_id"] == null); |
| 40 | + bool correctRowCount = results.Count == 2; // Should be customers 3 and 4 |
| 41 | + |
| 42 | + Console.WriteLine($"✓ Has payment_id column: {hasPaymentIdColumn}"); |
| 43 | + Console.WriteLine($"✓ All payment_ids are NULL: {allPaymentIdsNull}"); |
| 44 | + Console.WriteLine($"✓ Correct row count (2): {correctRowCount}"); |
| 45 | + |
| 46 | + if (!hasPaymentIdColumn) |
| 47 | + Console.WriteLine("❌ FAIL: Missing payment_id column!"); |
| 48 | + if (!allPaymentIdsNull) |
| 49 | + Console.WriteLine("❌ FAIL: payment_id should be NULL for all rows!"); |
| 50 | + if (!correctRowCount) |
| 51 | + Console.WriteLine($"❌ FAIL: Expected 2 rows (customers 3,4), got {results.Count}!"); |
| 52 | + |
| 53 | + // Show all rows |
| 54 | + Console.WriteLine("\nAll rows:"); |
| 55 | + foreach (var row in results) |
| 56 | + { |
| 57 | + Console.WriteLine($" {string.Join(", ", row.Select(kv => $"{kv.Key}={kv.Value ?? "NULL"}"))}"); |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + public void ValidateLeftJoinMultipleMatches() |
| 62 | + { |
| 63 | + Console.WriteLine("\n═══ Validating LEFT JOIN (multiple matches) ═══"); |
| 64 | + |
| 65 | + // ✅ DIAGNOSTIC: First try WITHOUT ORDER BY to see raw JOIN results |
| 66 | + Console.WriteLine("\n[DIAGNOSTIC] Testing LEFT JOIN WITHOUT ORDER BY:"); |
| 67 | + var sqlNoOrder = @"SELECT o.id as order_id, o.customer_id, p.id as payment_id, p.method |
| 68 | + FROM orders o |
| 69 | + LEFT JOIN payments p ON p.order_id = o.id"; |
| 70 | + |
| 71 | + var resultsNoOrder = db.ExecuteQuery(sqlNoOrder); |
| 72 | + var orders123NoOrder = resultsNoOrder.Where(r => |
| 73 | + { |
| 74 | + var orderId = r["order_id"].ToString(); |
| 75 | + return orderId == "1" || orderId == "2" || orderId == "3"; |
| 76 | + }).ToList(); |
| 77 | + |
| 78 | + Console.WriteLine($"[DIAGNOSTIC] Without ORDER BY - Orders 1-3: {orders123NoOrder.Count} rows"); |
| 79 | + foreach (var row in orders123NoOrder) |
| 80 | + { |
| 81 | + Console.WriteLine($" {string.Join(", ", row.Select(kv => $"{kv.Key}={kv.Value ?? "NULL"}"))}"); |
| 82 | + } |
| 83 | + |
| 84 | + // ✅ Now test WITH ORDER BY |
| 85 | + Console.WriteLine("\n[DIAGNOSTIC] Testing LEFT JOIN WITH ORDER BY:"); |
| 86 | + var sql = @"SELECT o.id as order_id, o.customer_id, p.id as payment_id, p.method |
| 87 | + FROM orders o |
| 88 | + LEFT JOIN payments p ON p.order_id = o.id |
| 89 | + ORDER BY o.id, p.id"; |
| 90 | + |
| 91 | + var results = db.ExecuteQuery(sql); |
| 92 | + |
| 93 | + Console.WriteLine($"✓ Returned {results.Count} rows"); |
| 94 | + if (results.Count > 0) |
| 95 | + Console.WriteLine($"✓ Columns: {string.Join(", ", results[0].Keys)}"); |
| 96 | + |
| 97 | + // Filter to just orders 1, 2, 3 for validation |
| 98 | + var orders123 = results.Where(r => |
| 99 | + { |
| 100 | + var orderId = r["order_id"].ToString(); |
| 101 | + return orderId == "1" || orderId == "2" || orderId == "3"; |
| 102 | + }).ToList(); |
| 103 | + |
| 104 | + Console.WriteLine($"✓ Orders 1-3: {orders123.Count} rows"); |
| 105 | + |
| 106 | + // Validate expectations |
| 107 | + // Order 1 has 1 payment (1 row) |
| 108 | + var order1Rows = orders123.Where(r => r["order_id"].ToString() == "1").ToList(); |
| 109 | + // Order 2 has 2 payments (2 rows) |
| 110 | + var order2Rows = orders123.Where(r => r["order_id"].ToString() == "2").ToList(); |
| 111 | + // Order 3 has 0 payments (1 row with NULL payment) |
| 112 | + var order3Rows = orders123.Where(r => r["order_id"].ToString() == "3").ToList(); |
| 113 | + |
| 114 | + bool order1Correct = order1Rows.Count == 1; |
| 115 | + bool order2Correct = order2Rows.Count == 2; |
| 116 | + bool order3Correct = order3Rows.Count >= 1 && (order3Rows[0]["payment_id"] is DBNull || order3Rows[0]["payment_id"] == null || string.IsNullOrEmpty(order3Rows[0]["payment_id"].ToString())); |
| 117 | + // Total should be 4 rows for orders 1-3 |
| 118 | + bool totalCorrect = orders123.Count == 4; |
| 119 | + |
| 120 | + Console.WriteLine($"✓ Order 1 has 1 payment row: {order1Correct} (actual: {order1Rows.Count})"); |
| 121 | + Console.WriteLine($"✓ Order 2 has 2 payment rows: {order2Correct} (actual: {order2Rows.Count})"); |
| 122 | + Console.WriteLine($"✓ Order 3 has NULL payment: {order3Correct} (actual: {order3Rows.Count} rows)"); |
| 123 | + Console.WriteLine($"✓ Total rows for orders 1-3: {totalCorrect} (actual: {orders123.Count}, expected: 4)"); |
| 124 | + |
| 125 | + if (!order1Correct) |
| 126 | + Console.WriteLine($"❌ FAIL: Order 1 should have 1 row, got {order1Rows.Count}!"); |
| 127 | + if (!order2Correct) |
| 128 | + Console.WriteLine($"❌ FAIL: Order 2 should have 2 rows (2 payments), got {order2Rows.Count}!"); |
| 129 | + if (!order3Correct) |
| 130 | + Console.WriteLine($"❌ FAIL: Order 3 should have 1 row with NULL payment!"); |
| 131 | + if (!totalCorrect) |
| 132 | + Console.WriteLine($"❌ FAIL: Expected 4 total rows for orders 1-3, got {orders123.Count}!"); |
| 133 | + |
| 134 | + // Show first 10 rows |
| 135 | + Console.WriteLine("\nFirst 10 rows:"); |
| 136 | + foreach (var row in results.Take(10)) |
| 137 | + { |
| 138 | + Console.WriteLine($" {string.Join(", ", row.Select(kv => $"{kv.Key}={kv.Value ?? "NULL"}"))}"); |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + public void ValidateDataSetup() |
| 143 | + { |
| 144 | + Console.WriteLine("\n═══ Validating Data Setup ═══"); |
| 145 | + |
| 146 | + var customers = db.ExecuteQuery("SELECT COUNT(*) as cnt FROM customers"); |
| 147 | + var orders = db.ExecuteQuery("SELECT COUNT(*) as cnt FROM orders"); |
| 148 | + var payments = db.ExecuteQuery("SELECT COUNT(*) as cnt FROM payments"); |
| 149 | + |
| 150 | + Console.WriteLine($"✓ Customers: {customers[0]["cnt"]}"); |
| 151 | + Console.WriteLine($"✓ Orders: {orders[0]["cnt"]}"); |
| 152 | + Console.WriteLine($"✓ Payments: {payments[0]["cnt"]}"); |
| 153 | + |
| 154 | + // ✅ NEW: Check if order 2 actually exists! |
| 155 | + var order1 = db.ExecuteQuery("SELECT * FROM orders WHERE id = 1"); |
| 156 | + var order2 = db.ExecuteQuery("SELECT * FROM orders WHERE id = 2"); |
| 157 | + var order3 = db.ExecuteQuery("SELECT * FROM orders WHERE id = 3"); |
| 158 | + |
| 159 | + Console.WriteLine($"\n✓ Order 1 exists: {order1.Count > 0}"); |
| 160 | + if (order1.Count > 0) |
| 161 | + { |
| 162 | + Console.WriteLine($" Order 1: customer_id={order1[0]["customer_id"]}, amount={order1[0]["amount"]}, status={order1[0]["status"]}"); |
| 163 | + } |
| 164 | + |
| 165 | + Console.WriteLine($"✓ Order 2 exists: {order2.Count > 0}"); |
| 166 | + if (order2.Count > 0) |
| 167 | + { |
| 168 | + Console.WriteLine($" Order 2: customer_id={order2[0]["customer_id"]}, amount={order2[0]["amount"]}, status={order2[0]["status"]}"); |
| 169 | + } |
| 170 | + |
| 171 | + Console.WriteLine($"✓ Order 3 exists: {order3.Count > 0}"); |
| 172 | + if (order3.Count > 0) |
| 173 | + { |
| 174 | + Console.WriteLine($" Order 3: customer_id={order3[0]["customer_id"]}, amount={order3[0]["amount"]}, status={order3[0]["status"]}"); |
| 175 | + } |
| 176 | + |
| 177 | + // Check payments for orders 1 and 2 |
| 178 | + var paymentsFor1 = db.ExecuteQuery("SELECT * FROM payments WHERE order_id = 1"); |
| 179 | + var paymentsFor2 = db.ExecuteQuery("SELECT * FROM payments WHERE order_id = 2"); |
| 180 | + |
| 181 | + Console.WriteLine($"\n✓ Payments for order 1: {paymentsFor1.Count}"); |
| 182 | + foreach (var p in paymentsFor1) |
| 183 | + { |
| 184 | + Console.WriteLine($" Payment ID={p["id"]}, method={p["method"]}"); |
| 185 | + } |
| 186 | + |
| 187 | + Console.WriteLine($"✓ Payments for order 2: {paymentsFor2.Count}"); |
| 188 | + foreach (var p in paymentsFor2) |
| 189 | + { |
| 190 | + Console.WriteLine($" Payment ID={p["id"]}, method={p["method"]}"); |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + public void RunAll() |
| 195 | + { |
| 196 | + ValidateDataSetup(); |
| 197 | + ValidateLeftJoinNoMatches(); |
| 198 | + ValidateLeftJoinMultipleMatches(); |
| 199 | + } |
| 200 | +} |
0 commit comments