Purpose: Core JOIN execution engine with streaming iterator pattern.
Key Features:
- Iterator-based execution (
IEnumerable<Dictionary<string, object>>) for streaming results - Hash join algorithm (O(n+m)) with automatic build/probe side selection
- Nested loop join fallback for small datasets
- Support for all JOIN types: INNER, LEFT, RIGHT, FULL, CROSS
Public API:
JoinExecutor.ExecuteInnerJoin(leftRows, rightRows, leftAlias, rightAlias, onCondition)
JoinExecutor.ExecuteLeftJoin(leftRows, rightRows, leftAlias, rightAlias, onCondition)
JoinExecutor.ExecuteRightJoin(leftRows, rightRows, leftAlias, rightAlias, onCondition)
JoinExecutor.ExecuteFullJoin(leftRows, rightRows, leftAlias, rightAlias, onCondition)
JoinExecutor.ExecuteCrossJoin(leftRows, rightRows, leftAlias, rightAlias)Purpose: Parse and evaluate JOIN ON conditions.
Key Features:
- Parses ON clause strings: "table1.col1 = table2.col2"
- Supports multi-column joins: "...AND table1.col3 = table2.col4"
- Evaluates conditions against row pairs
- Handles qualified column references (table.column)
Public API:
var evaluator = JoinConditionEvaluator.CreateEvaluator(
onClause: "users.id = orders.user_id",
leftAlias: "users",
rightAlias: "orders"
);
bool matches = evaluator(leftRow, rightRow);The AST infrastructure already exists:
JoinNodedefined inSqlAst.Nodes.csFromNode.Joinscollection for multiple JOINsEnhancedSqlParser.ParseJoin()parses JOIN syntaxSqlToStringVisitorconverts JOINs back to SQL
JOIN execution is not yet wired up in the query execution path. Here's how to integrate:
In a new visitor class or existing query executor:
// Example integration (pseudocode)
public class QueryExecutionVisitor : SqlVisitorBase<List<Dictionary<string, object>>>
{
protected override List<Dictionary<string, object>> VisitSelectCore(SelectNode node)
{
// Execute FROM clause
var results = ExecuteFrom(node.From);
// Apply WHERE
if (node.Where != null)
{
results = ApplyWhere(results, node.Where);
}
// Apply ORDER BY, LIMIT, etc.
return results;
}
private List<Dictionary<string, object>> ExecuteFrom(FromNode fromNode)
{
// Get left table rows
var leftRows = GetTableRows(fromNode.TableName);
var leftAlias = fromNode.Alias ?? fromNode.TableName;
// Process JOINs
foreach (var joinNode in fromNode.Joins)
{
var rightTable = joinNode.Table;
var rightRows = GetTableRows(rightTable.TableName);
var rightAlias = rightTable.Alias ?? rightTable.TableName;
// Convert ON condition AST to evaluator function
var onCondition = ConvertToEvaluator(joinNode.OnCondition, leftAlias, rightAlias);
// Execute JOIN
leftRows = joinNode.Type switch
{
JoinNode.JoinType.Inner =>
JoinExecutor.ExecuteInnerJoin(leftRows, rightRows, leftAlias, rightAlias, onCondition).ToList(),
JoinNode.JoinType.Left =>
JoinExecutor.ExecuteLeftJoin(leftRows, rightRows, leftAlias, rightAlias, onCondition).ToList(),
JoinNode.JoinType.Right =>
JoinExecutor.ExecuteRightJoin(leftRows, rightRows, leftAlias, rightAlias, onCondition).ToList(),
JoinNode.JoinType.Full =>
JoinExecutor.ExecuteFullJoin(leftRows, rightRows, leftAlias, rightAlias, onCondition).ToList(),
JoinNode.JoinType.Cross =>
JoinExecutor.ExecuteCrossJoin(leftRows, rightRows, leftAlias, rightAlias).ToList(),
_ => throw new NotSupportedException($"JOIN type {joinNode.Type} not supported")
};
}
return leftRows;
}
private Func<Dictionary<string, object>, Dictionary<string, object>, bool> ConvertToEvaluator(
ExpressionNode? onCondition,
string leftAlias,
string rightAlias)
{
if (onCondition == null)
{
return (left, right) => true; // CROSS JOIN
}
// Convert BinaryExpressionNode to string and use JoinConditionEvaluator
string onClause = ConvertExpressionToString(onCondition);
return JoinConditionEvaluator.CreateEvaluator(onClause, leftAlias, rightAlias);
}
}For backward compatibility with existing SQL parser:
// In SqlParser.DML.cs, modify ExecuteSelectQuery()
private List<Dictionary<string, object>> ExecuteSelectQuery(string sql, string[] parts, bool noEncrypt)
{
// ...existing code...
// Check for JOIN keyword
var joinIdx = Array.FindIndex(parts, p =>
p.Equals("JOIN", StringComparison.OrdinalIgnoreCase) ||
p.Equals("INNER", StringComparison.OrdinalIgnoreCase) ||
p.Equals("LEFT", StringComparison.OrdinalIgnoreCase) ||
p.Equals("RIGHT", StringComparison.OrdinalIgnoreCase) ||
p.Equals("FULL", StringComparison.OrdinalIgnoreCase));
if (joinIdx > 0)
{
// Route to JOIN executor
return ExecuteJoinQuery(sql, parts, noEncrypt);
}
// ...existing single-table SELECT logic...
}
private List<Dictionary<string, object>> ExecuteJoinQuery(string sql, string[] parts, bool noEncrypt)
{
// Parse JOIN syntax and route to JoinExecutor
// Implementation similar to ExecuteFrom() above
}Once integrated, users can execute:
-- INNER JOIN
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- LEFT JOIN
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- RIGHT JOIN
SELECT u.name, o.amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- FULL OUTER JOIN
SELECT u.name, o.amount
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
-- CROSS JOIN
SELECT u.name, p.title
FROM users u
CROSS JOIN products p;
-- Multi-column JOIN
SELECT *
FROM orders o
INNER JOIN shipments s ON o.id = s.order_id AND o.customer_id = s.customer_id;- Build phase: O(n) - smaller table
- Probe phase: O(m) - larger table
- Total: O(n + m)
- Memory: Hash table for smaller side only
- Complexity: O(n × m)
- Memory: Minimal (no intermediate structures)
- Use case: Small tables (<= 10 rows each)
- No materialization: Results yielded as they're produced
- Memory efficient: Only current row pairs in memory
- Scales: Can handle large result sets without OOM
Create unit tests in DatabaseTests.cs:
[Fact]
public void Database_Join_LeftJoin_IncludesUnmatchedLeftRows()
{
var db = _factory.Create(_testDbPath, "password");
db.ExecuteSQL("CREATE TABLE users (id INTEGER, name TEXT)");
db.ExecuteSQL("CREATE TABLE orders (id INTEGER, user_id INTEGER, amount DECIMAL)");
db.ExecuteSQL("INSERT INTO users VALUES (1, 'Alice')");
db.ExecuteSQL("INSERT INTO users VALUES (2, 'Bob')");
db.ExecuteSQL("INSERT INTO orders VALUES (1, 1, 100.00)");
var results = db.ExecuteQuery(
"SELECT users.name, orders.amount " +
"FROM users LEFT JOIN orders ON users.id = orders.user_id");
// Should have 2 rows: Alice with 100.00, Bob with NULL
Assert.Equal(2, results.Count);
Assert.Contains(results, r => r["name"].ToString() == "Bob" && r["amount"] == DBNull.Value);
}- Join order optimization: Cost-based optimizer to select join order
- Index-nested loop joins: Use indexes on join columns
- Merge joins: For sorted inputs
- Parallel hash joins: Multi-threaded build/probe phases
- Semi-joins/anti-joins: For EXISTS/NOT EXISTS subqueries
- Implementation is hot-path optimized: No LINQ, no async, minimal allocations
- Streaming execution: Uses
yield returnfor memory efficiency - Null handling: Outer joins properly emit null columns for unmatched rows
- Ready for integration: All core components implemented and tested