Skip to content

Commit f4e1df6

Browse files
author
MPCoreDeveloper
committed
test: add JOIN coverage for single-file (.scdb) mode
Adds 4 new tests to SingleFileTests.cs covering: - INNER JOIN with matching rows - LEFT JOIN including unmatched rows (NULL/DBNull fill) - INNER JOIN with WHERE filter - Three-table chained INNER JOIN JOINs are already supported in single-file mode because SELECT is routed through the shared SqlParser. These tests lock in that behavior and guard against regressions.
1 parent 8e04ddc commit f4e1df6

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

tests/SharpCoreDB.Tests/SingleFileTests.cs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,4 +342,147 @@ public void ExecuteSQL_DropTable_WithQuotedTableName_DropsSuccessfully()
342342
(db as IDisposable)?.Dispose();
343343
}
344344
}
345+
346+
/// <summary>
347+
/// JOIN tests — single-file SELECT is routed through the shared SqlParser,
348+
/// so all join variants already work. These tests lock in that behaviour.
349+
/// </summary>
350+
[Fact]
351+
public void ExecuteQuery_InnerJoin_ReturnsMatchingRows()
352+
{
353+
// Arrange
354+
var options = DatabaseOptions.CreateSingleFileDefault();
355+
var db = _factory.CreateWithOptions(_testFilePath, "test_password", options);
356+
357+
try
358+
{
359+
db.ExecuteSQL("CREATE TABLE orders (order_id INTEGER, customer_id INTEGER, amount REAL)");
360+
db.ExecuteSQL("CREATE TABLE customers (customer_id INTEGER, name TEXT)");
361+
db.ExecuteBatchSQL([
362+
"INSERT INTO customers VALUES (1, 'Alice')",
363+
"INSERT INTO customers VALUES (2, 'Bob')",
364+
"INSERT INTO orders VALUES (10, 1, 99.50)",
365+
"INSERT INTO orders VALUES (11, 1, 45.00)",
366+
"INSERT INTO orders VALUES (12, 2, 200.00)"
367+
]);
368+
369+
// Act
370+
var results = db.ExecuteQuery(
371+
"SELECT orders.order_id, customers.name FROM orders " +
372+
"INNER JOIN customers ON orders.customer_id = customers.customer_id");
373+
374+
// Assert — all three orders have a matching customer
375+
Assert.Equal(3, results.Count);
376+
}
377+
finally
378+
{
379+
(db as IDisposable)?.Dispose();
380+
}
381+
}
382+
383+
[Fact]
384+
public void ExecuteQuery_LeftJoin_IncludesUnmatchedLeftRows()
385+
{
386+
// Arrange
387+
var options = DatabaseOptions.CreateSingleFileDefault();
388+
var db = _factory.CreateWithOptions(_testFilePath, "test_password", options);
389+
390+
try
391+
{
392+
db.ExecuteSQL("CREATE TABLE products (product_id INTEGER, name TEXT)");
393+
db.ExecuteSQL("CREATE TABLE reviews (review_id INTEGER, product_id INTEGER, score INTEGER)");
394+
db.ExecuteBatchSQL([
395+
"INSERT INTO products VALUES (1, 'Widget')",
396+
"INSERT INTO products VALUES (2, 'Gadget')",
397+
"INSERT INTO reviews VALUES (100, 1, 5)"
398+
// product 2 has no review
399+
]);
400+
401+
// Act
402+
var results = db.ExecuteQuery(
403+
"SELECT products.name, reviews.score FROM products " +
404+
"LEFT JOIN reviews ON products.product_id = reviews.product_id");
405+
406+
// Assert — both products appear; Gadget row has NULL score
407+
Assert.Equal(2, results.Count);
408+
var gadget = results.First(r => r["name"]?.ToString() == "Gadget");
409+
// SQL NULL is represented as DBNull.Value in dictionary results
410+
Assert.True(gadget["score"] is null or DBNull);
411+
}
412+
finally
413+
{
414+
(db as IDisposable)?.Dispose();
415+
}
416+
}
417+
418+
[Fact]
419+
public void ExecuteQuery_Join_WithWhereFilter_ReturnsFilteredRows()
420+
{
421+
// Arrange
422+
var options = DatabaseOptions.CreateSingleFileDefault();
423+
var db = _factory.CreateWithOptions(_testFilePath, "test_password", options);
424+
425+
try
426+
{
427+
db.ExecuteSQL("CREATE TABLE emp (emp_id INTEGER, dept_id INTEGER, name TEXT)");
428+
db.ExecuteSQL("CREATE TABLE dept (dept_id INTEGER, dept_name TEXT)");
429+
db.ExecuteBatchSQL([
430+
"INSERT INTO dept VALUES (1, 'Engineering')",
431+
"INSERT INTO dept VALUES (2, 'Marketing')",
432+
"INSERT INTO emp VALUES (1, 1, 'Alice')",
433+
"INSERT INTO emp VALUES (2, 1, 'Bob')",
434+
"INSERT INTO emp VALUES (3, 2, 'Carol')"
435+
]);
436+
437+
// Act — only Engineering employees
438+
var results = db.ExecuteQuery(
439+
"SELECT emp.name, dept.dept_name FROM emp " +
440+
"INNER JOIN dept ON emp.dept_id = dept.dept_id " +
441+
"WHERE dept.dept_name = 'Engineering'");
442+
443+
// Assert
444+
Assert.Equal(2, results.Count);
445+
Assert.All(results, r => Assert.Equal("Engineering", r["dept_name"]?.ToString()));
446+
}
447+
finally
448+
{
449+
(db as IDisposable)?.Dispose();
450+
}
451+
}
452+
453+
[Fact]
454+
public void ExecuteQuery_ThreeTableJoin_ReturnsCorrectRows()
455+
{
456+
// Arrange
457+
var options = DatabaseOptions.CreateSingleFileDefault();
458+
var db = _factory.CreateWithOptions(_testFilePath, "test_password", options);
459+
460+
try
461+
{
462+
db.ExecuteSQL("CREATE TABLE authors (author_id INTEGER, author_name TEXT)");
463+
db.ExecuteSQL("CREATE TABLE books (book_id INTEGER, author_id INTEGER, title TEXT)");
464+
db.ExecuteSQL("CREATE TABLE sales (sale_id INTEGER, book_id INTEGER, qty INTEGER)");
465+
db.ExecuteBatchSQL([
466+
"INSERT INTO authors VALUES (1, 'Tolkien')",
467+
"INSERT INTO books VALUES (10, 1, 'The Hobbit')",
468+
"INSERT INTO books VALUES (11, 1, 'LOTR')",
469+
"INSERT INTO sales VALUES (100, 10, 5)",
470+
"INSERT INTO sales VALUES (101, 11, 12)"
471+
]);
472+
473+
// Act
474+
var results = db.ExecuteQuery(
475+
"SELECT authors.author_name, books.title, sales.qty FROM authors " +
476+
"INNER JOIN books ON authors.author_id = books.author_id " +
477+
"INNER JOIN sales ON books.book_id = sales.book_id");
478+
479+
// Assert — both books sold by Tolkien appear
480+
Assert.Equal(2, results.Count);
481+
Assert.All(results, r => Assert.Equal("Tolkien", r["author_name"]?.ToString()));
482+
}
483+
finally
484+
{
485+
(db as IDisposable)?.Dispose();
486+
}
487+
}
345488
}

0 commit comments

Comments
 (0)