Skip to content

Commit b1b9d72

Browse files
author
MPCoreDeveloper
committed
current status
1 parent 307f4b1 commit b1b9d72

8 files changed

Lines changed: 316 additions & 15 deletions

File tree

run-join-debug.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Write-Host "Running JOIN validation with debug output..." -ForegroundColor Cyan
2+
dotnet run --project tests\SharpCoreDB.DemoJoinsSubQ 2>&1 | Select-String -Pattern "(JOIN-DEBUG|WHERE-DEBUG|Validating|Returned|FAIL|All rows:)" -Context 0,2

src/SharpCoreDB/Execution/JoinConditionEvaluator.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public static class JoinConditionEvaluator
2828
/// <summary>
2929
/// Creates a condition evaluator from ON clause expression.
3030
/// Parses simple equality conditions: "table1.col1 = table2.col2".
31+
/// ✅ FIXED: Added diagnostic logging to trace condition parsing.
3132
/// </summary>
3233
/// <param name="onClause">The ON clause string (e.g., "users.id = orders.user_id").</param>
3334
/// <param name="leftAlias">Left table alias.</param>
@@ -38,6 +39,11 @@ public static Func<Dictionary<string, object>, Dictionary<string, object>, bool>
3839
string? leftAlias,
3940
string? rightAlias)
4041
{
42+
#if DEBUG
43+
Console.WriteLine($"[JOIN-PARSE] Creating evaluator for ON clause: '{onClause}'");
44+
Console.WriteLine($"[JOIN-PARSE] Left alias: '{leftAlias}', Right alias: '{rightAlias}'");
45+
#endif
46+
4147
if (string.IsNullOrWhiteSpace(onClause))
4248
{
4349
// No condition - always true (for CROSS JOIN)
@@ -47,7 +53,17 @@ public static Func<Dictionary<string, object>, Dictionary<string, object>, bool>
4753
// Parse ON clause
4854
var conditions = ParseOnClause(onClause, leftAlias, rightAlias);
4955

50-
// Return evaluator function
56+
#if DEBUG
57+
Console.WriteLine($"[JOIN-PARSE] Parsed {conditions.Count} conditions:");
58+
foreach (var cond in conditions)
59+
{
60+
Console.WriteLine($"[JOIN-PARSE] - Left: ({cond.LeftColumn.table}.{cond.LeftColumn.column}, isLeft={cond.LeftColumn.isLeft})");
61+
Console.WriteLine($"[JOIN-PARSE] - Right: ({cond.RightColumn.table}.{cond.RightColumn.column}, isLeft={cond.RightColumn.isLeft})");
62+
Console.WriteLine($"[JOIN-PARSE] - Operator: {cond.Operator}");
63+
}
64+
#endif
65+
66+
// Return evaluator function
5167
return (leftRow, rightRow) => EvaluateConditions(conditions, leftRow, rightRow);
5268
}
5369

@@ -180,6 +196,7 @@ private static bool EvaluateSingleCondition(
180196

181197
/// <summary>
182198
/// Gets column value from appropriate row based on column reference.
199+
/// ✅ FIXED: Added diagnostic logging and fallback search for column names.
183200
/// </summary>
184201
[MethodImpl(MethodImplOptions.AggressiveInlining)]
185202
private static object? GetColumnValue(
@@ -200,9 +217,29 @@ private static bool EvaluateSingleCondition(
200217
}
201218

202219
// Try unqualified column name
203-
return row.TryGetValue(columnRef.column, out var unqualifiedValue)
204-
? unqualifiedValue
205-
: null;
220+
if (row.TryGetValue(columnRef.column, out var unqualifiedValue))
221+
{
222+
return unqualifiedValue;
223+
}
224+
225+
// ✅ NEW: Try finding any key that matches the column name (case-insensitive)
226+
// This handles cases where the row has qualified names but we're looking for unqualified
227+
var matchingKey = row.Keys.FirstOrDefault(k =>
228+
k.Equals(columnRef.column, StringComparison.OrdinalIgnoreCase) ||
229+
k.EndsWith($".{columnRef.column}", StringComparison.OrdinalIgnoreCase));
230+
231+
if (matchingKey is not null && row.TryGetValue(matchingKey, out var fallbackValue))
232+
{
233+
return fallbackValue;
234+
}
235+
236+
#if DEBUG
237+
// Diagnostic: Show what keys are available if we can't find the column
238+
Console.WriteLine($"[JOIN-DEBUG] Could not find column '{columnRef.column}' (table: {columnRef.table}, isLeft: {columnRef.isLeft})");
239+
Console.WriteLine($"[JOIN-DEBUG] Available keys in {'(' + (columnRef.isLeft ? "LEFT" : "RIGHT") + ')'} row: {string.Join(", ", row.Keys)}");
240+
#endif
241+
242+
return null;
206243
}
207244

208245
/// <summary>

src/SharpCoreDB/Execution/JoinExecutor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ namespace SharpCoreDB.Execution;
3636
/// </summary>
3737
public static class JoinExecutor
3838
{
39-
private const int HashJoinThreshold = 10; // Use hash join if either side > 10 rows
39+
private const int HashJoinThreshold = int.MaxValue; // Disable hash join until key hashing is implemented
4040

4141
#region Public API
4242

src/SharpCoreDB/Services/SqlParser.DML.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,7 @@ private static object ComputeAggregate(IGrouping<string, Dictionary<string, obje
11301130
/// <summary>
11311131
/// Projects columns from query results based on SELECT list.
11321132
/// ✅ MODERNIZED: Uses modern null handling and pattern matching.
1133+
/// ✅ FIXED: Strictly matches qualified column names in JOINs to prevent NULL mismatches.
11331134
/// </summary>
11341135
private List<Dictionary<string, object>> ProjectColumns(
11351136
List<Dictionary<string, object>> results,
@@ -1153,10 +1154,56 @@ private List<Dictionary<string, object>> ProjectColumns(
11531154
}
11541155
else if (!string.IsNullOrEmpty(columnNode.Name))
11551156
{
1156-
// Simple column reference
11571157
var columnName = columnNode.Name;
1158-
if (row.TryGetValue(columnName, out var value))
1158+
object? value = null;
1159+
bool found = false;
1160+
1161+
if (!string.IsNullOrEmpty(columnNode.TableAlias))
1162+
{
1163+
// ✅ STRICT MODE: When table alias is specified (e.g., "p.id"), ONLY match the qualified name
1164+
// This prevents "p.id" from incorrectly matching "o.id" when p.id is NULL in a LEFT JOIN
1165+
var qualifiedName = $"{columnNode.TableAlias}.{columnName}";
1166+
1167+
// Try exact match
1168+
if (row.TryGetValue(qualifiedName, out value))
1169+
{
1170+
found = true;
1171+
}
1172+
else
1173+
{
1174+
// Try case-insensitive match
1175+
var matchingKey = row.Keys.FirstOrDefault(k =>
1176+
k.Equals(qualifiedName, StringComparison.OrdinalIgnoreCase));
1177+
1178+
if (matchingKey is not null && row.TryGetValue(matchingKey, out value))
1179+
found = true;
1180+
}
1181+
1182+
// Do NOT fall back to unqualified names when qualified name specified
1183+
// This is the key fix for LEFT JOIN NULL handling
1184+
}
1185+
else
1186+
{
1187+
// ✅ RELAXED MODE: When no table alias, try multiple matches
1188+
if (row.TryGetValue(columnName, out value))
1189+
{
1190+
found = true;
1191+
}
1192+
else
1193+
{
1194+
// Try finding any qualified version of the column
1195+
var matchingKey = row.Keys.FirstOrDefault(k =>
1196+
k.Equals(columnName, StringComparison.OrdinalIgnoreCase) ||
1197+
k.EndsWith($".{columnName}", StringComparison.OrdinalIgnoreCase));
1198+
1199+
if (matchingKey is not null && row.TryGetValue(matchingKey, out value))
1200+
found = true;
1201+
}
1202+
}
1203+
1204+
if (found || !string.IsNullOrEmpty(columnNode.TableAlias))
11591205
{
1206+
// Include the column even if not found (preserves NULL for LEFT JOIN)
11601207
var alias = columnNode.Alias ?? columnName;
11611208
projectedRow[alias] = value ?? DBNull.Value;
11621209
}

test-join-debug.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Test query to debug LEFT JOIN with multiple matches
2+
-- Expected results:
3+
-- Order 1: 1 row (payment 1)
4+
-- Order 2: 2 rows (payments 2 and 3)
5+
-- Order 3: 1 row (NULL payment)
6+
-- Total: 4 rows
7+
8+
SELECT o.id as order_id, o.customer_id, p.id as payment_id, p.method
9+
FROM orders o
10+
LEFT JOIN payments p ON p.order_id = o.id
11+
WHERE o.id IN (1, 2, 3)
12+
ORDER BY o.id, p.id;

tests/SharpCoreDB.DemoJoinsSubQ/DemoRunner.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ public void Run()
2525
var setup = new SchemaSetup(db);
2626
setup.Seed();
2727

28+
// ✅ Run validation first to diagnose issues
29+
var validator = new JoinValidator(db);
30+
validator.RunAll();
31+
32+
// Run full demo scenarios
2833
var joins = new JoinScenarios(db);
2934
var subq = new SubqueryScenarios(db);
30-
3135
joins.RunAll();
3236
subq.RunAll();
3337

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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

Comments
 (0)