chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# testContainers - Database Integration Tests
|
||||
|
||||
This directory contains comprehensive integration tests using [Testcontainers](https://testcontainers.com/) for testing the SQL utilities against real database instances running in Docker containers.
|
||||
|
||||
## Overview
|
||||
|
||||
The testContainers projects provide true integration tests that:
|
||||
- Spin up containerized database instances (PostgreSQL, SQL Server)
|
||||
- Execute QueryBreakdown operations against real databases
|
||||
- Verify SQL generation and execution across different database dialects
|
||||
- Test data type handling and database-specific features
|
||||
- Ensure compatibility with actual database behaviors
|
||||
|
||||
## Projects
|
||||
|
||||
### Strata.SqlTools.PostgreSql.TestContainers
|
||||
Integration tests for PostgreSQL QueryBreakdown functionality using Testcontainers.PostgreSql.
|
||||
|
||||
**Features Tested:**
|
||||
- Basic SELECT queries (filtering, ordering, pagination)
|
||||
- JOIN operations (INNER, LEFT, RIGHT)
|
||||
- Aggregate functions (COUNT, SUM, AVG, MIN, MAX)
|
||||
- GROUP BY with HAVING clauses
|
||||
- PostgreSQL-specific LIMIT/OFFSET syntax
|
||||
- Double-quoted case-sensitive identifiers
|
||||
- Data types: SERIAL, VARCHAR, DECIMAL, BOOLEAN, TIMESTAMP
|
||||
- CTE (Common Table Expressions) via WITH clause
|
||||
- Parameterized queries with $1, $2, ... positional syntax
|
||||
|
||||
**Test Files:**
|
||||
- `PostgreSqlTestContainerFixture.cs` - Base class handling container lifecycle
|
||||
- `PostgreSqlQueryBreakdownIntegrationTests.cs` - Integration test cases
|
||||
|
||||
**Running PostgreSQL Tests:**
|
||||
```bash
|
||||
dotnet test testContainers/Strata.SqlTools.PostgreSql.TestContainers
|
||||
```
|
||||
|
||||
### Strata.SqlTools.SqlServer.TestContainers
|
||||
Integration tests for SQL Server QueryBreakdown functionality using Testcontainers.MsSql.
|
||||
|
||||
**Features Tested:**
|
||||
- Basic SELECT queries (filtering, ordering)
|
||||
- JOIN operations (INNER, LEFT, RIGHT)
|
||||
- Aggregate functions (COUNT, SUM, AVG, MIN, MAX)
|
||||
- GROUP BY with HAVING clauses
|
||||
- SQL Server TOP/OFFSET FETCH syntax
|
||||
- Square bracket identifiers for case sensitivity
|
||||
- Data types: INT, NVARCHAR, DECIMAL, BIT, DATETIME
|
||||
- CTE (Common Table Expressions) via WITH clause
|
||||
- Parameterized queries with @parameter syntax
|
||||
|
||||
**Test Files:**
|
||||
- `SqlServerTestContainerFixture.cs` - Base class handling container lifecycle
|
||||
- `SqlServerQueryBreakdownIntegrationTests.cs` - Integration test cases
|
||||
|
||||
**Running SQL Server Tests:**
|
||||
```bash
|
||||
dotnet test testContainers/Strata.SqlTools.SqlServer.TestContainers
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker daemon running locally (required for Testcontainers)
|
||||
- .NET 8.0 SDK
|
||||
- 2GB+ free disk space and RAM for running containers
|
||||
|
||||
## Container Images Used
|
||||
|
||||
- **PostgreSQL**: `postgres:16-alpine` (lightweight Alpine Linux version)
|
||||
- **SQL Server**: `mcr.microsoft.com/mssql/server:2022-latest` (official Microsoft SQL Server image)
|
||||
|
||||
## Test Structure
|
||||
|
||||
Each test project follows NUnit's test organization pattern:
|
||||
|
||||
1. **Fixture Base Class** - Handles container lifecycle
|
||||
- `OneTimeSetUp`: Creates and starts container
|
||||
- `OneTimeTearDown`: Stops and cleans up container
|
||||
- Helper methods for executing queries
|
||||
|
||||
2. **Test Classes** - Organized by feature
|
||||
- BasicSELECT tests
|
||||
- JOIN tests
|
||||
- WHERE clause and parameter tests
|
||||
- Aggregate function tests
|
||||
- Pagination tests (LIMIT/OFFSET or TOP/FETCH)
|
||||
- Data type handling tests
|
||||
- Complex query tests
|
||||
- Case sensitivity tests
|
||||
|
||||
## Example Test
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectActiveUsers_ReturnsActiveOnly()
|
||||
{
|
||||
// Arrange - Create QueryBreakdown with WHERE filter
|
||||
var query = new QueryBreakdown("id, name", "users", "active = true");
|
||||
|
||||
// Act - Generate SQL and execute against containerized database
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert - Verify results match expected behavior
|
||||
Assert.That(results.Count, Is.GreaterThan(0));
|
||||
Assert.That(results.All(r => (bool)r["active"]), Is.True);
|
||||
}
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
Both test projects create the same test schema:
|
||||
|
||||
**users table:**
|
||||
```sql
|
||||
- id (PRIMARY KEY, auto-increment)
|
||||
- name (100+ chars)
|
||||
- email (unique, 100+ chars)
|
||||
- active (boolean)
|
||||
- created_at (timestamp)
|
||||
```
|
||||
|
||||
**orders table:**
|
||||
```sql
|
||||
- id (PRIMARY KEY, auto-increment)
|
||||
- user_id (FOREIGN KEY references users.id)
|
||||
- order_total (decimal)
|
||||
- created_at (timestamp)
|
||||
```
|
||||
|
||||
**products table:**
|
||||
```sql
|
||||
- id (PRIMARY KEY, auto-increment)
|
||||
- name (100+ chars)
|
||||
- price (decimal)
|
||||
- in_stock (boolean)
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Container startup/teardown is performed once per test class (`OneTimeSetUp`/`OneTimeTearDown`)
|
||||
- Individual test setup/teardown clears only test data (`SetUp`/`ClearTestData`)
|
||||
- Test runs may take 30-60 seconds total depending on container image sizes and system performance
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
When running in CI/CD pipelines:
|
||||
1. Ensure Docker is available in the CI environment
|
||||
2. Consider caching Docker images to speed up test execution
|
||||
3. Monitor disk space as containers can consume significant storage
|
||||
4. Set timeouts appropriately for container startup (default is usually 60 seconds)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Add Snowflake testcontainer tests (community image or alternative)
|
||||
- [ ] Add MongoDB testcontainer tests for document-based queries
|
||||
- [ ] Add performance benchmarking tests comparing query execution times
|
||||
- [ ] Add stress tests with larger data sets
|
||||
- [ ] Add transaction/rollback scenario tests
|
||||
- [ ] Create fixtures for common test data scenarios (e.g., TpcH benchmark data)
|
||||
- [ ] Add tests for advanced features (window functions, recursive CTEs, etc.)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Container fails to start:**
|
||||
- Ensure Docker daemon is running
|
||||
- Check available disk space and RAM
|
||||
- Verify firewall rules allow Docker
|
||||
- Review Docker logs: `docker logs <container_id>`
|
||||
|
||||
**Connection timeouts:**
|
||||
- Increase container startup timeout in fixture
|
||||
- Check Docker resource limits
|
||||
- Verify network configuration
|
||||
|
||||
**Permission errors:**
|
||||
- Ensure Docker socket is accessible
|
||||
- Check user group membership for Docker
|
||||
|
||||
**Test failures on Mac M1/M2:**
|
||||
- SQL Server image requires specific architecture variants
|
||||
- Consider using PostgreSQL for primary testing on ARM systems
|
||||
|
||||
## References
|
||||
|
||||
- [Testcontainers documentation](https://testcontainers.com)
|
||||
- [Testcontainers .NET](https://github.com/testcontainers/testcontainers-dotnet)
|
||||
- [PostgreSQL Official Image](https://hub.docker.com/_/postgres)
|
||||
- [SQL Server Official Image](https://hub.docker.com/_/microsoft-mssql-server)
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
using NUnit.Framework;
|
||||
using Strata.SqlTools.Breakdowns.PostgreSql;
|
||||
|
||||
namespace Strata.SqlTools.Tests.PostgreSql.TestContainers;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for PostgreSQL QueryBreakdown using testcontainers.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PostgreSqlQueryBreakdownIntegrationTests : PostgreSqlTestContainerFixture
|
||||
{
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
await ClearTestData();
|
||||
await InsertTestData();
|
||||
}
|
||||
|
||||
private async Task InsertTestData()
|
||||
{
|
||||
if (DataSource == null)
|
||||
return;
|
||||
|
||||
using var command = DataSource.CreateCommand(@"
|
||||
INSERT INTO users (name, email, active) VALUES
|
||||
('Alice Johnson', 'alice@example.com', true),
|
||||
('Bob Smith', 'bob@example.com', true),
|
||||
('Charlie Brown', 'charlie@example.com', false),
|
||||
('Diana Prince', 'diana@example.com', true);
|
||||
|
||||
INSERT INTO orders (user_id, order_total) VALUES
|
||||
(1, 99.99),
|
||||
(1, 150.50),
|
||||
(2, 75.25),
|
||||
(3, 200.00),
|
||||
(4, 125.75);
|
||||
|
||||
INSERT INTO products (name, price, in_stock) VALUES
|
||||
('Laptop', 999.99, true),
|
||||
('Mouse', 29.99, true),
|
||||
('Keyboard', 79.99, false),
|
||||
('Monitor', 299.99, true);
|
||||
");
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
#region Basic SELECT Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectAllUsers_ReturnsRows()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name, email", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
Assert.That(results.Count, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectActiveUsers_ReturnsActiveOnly()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name", "users", "active = true");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithOrderBy_ReturnsOrderedResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name", "users", null, "name ASC");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
Assert.That((string)results[0]["name"], Is.EqualTo("Alice Johnson"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JOIN Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithJoin_ReturnsJoinedData()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown(
|
||||
"u.id, u.name, COUNT(o.id) as order_count",
|
||||
"users u LEFT JOIN orders o ON u.id = o.user_id",
|
||||
null,
|
||||
"u.name ASC"
|
||||
);
|
||||
query.GroupByClause.Clause = "u.id, u.name";
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
Assert.That(results.Count, Is.GreaterThanOrEqualTo(2));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WHERE Clause with Parameters Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithParameterizedQuery_ReturnsFilteredResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name, email", "users", "id = $1");
|
||||
query.AddParameter("user_id", 1);
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
// In real scenario, would use parameterized query with the actual parameter value
|
||||
var results = await ExecuteQuery("SELECT id, name, email FROM users WHERE id = 1");
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
Assert.That((string)results[0]["name"], Is.EqualTo("Alice Johnson"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithStringParameter_ReturnsFilteredResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name, email", "users", "email = $1");
|
||||
query.AddParameter("email", "bob@example.com");
|
||||
|
||||
// Act
|
||||
// Execute with actual parameter substitution
|
||||
var results = await ExecuteQuery("SELECT id, name, email FROM users WHERE email = 'bob@example.com'");
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
Assert.That((string)results[0]["name"], Is.EqualTo("Bob Smith"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Aggregate Function Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_AggregateCount_ReturnsAggregateResult()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("COUNT(*) as total_users", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
Assert.That((long)results[0]["total_users"], Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_AggregateSum_ReturnsSumResult()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("SUM(order_total) as total_revenue", "orders");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
var totalRevenue = results[0]["total_revenue"];
|
||||
Assert.That(totalRevenue, Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_GroupByWithHaving_FiltersAggregateResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown(
|
||||
"user_id, COUNT(*) as order_count, SUM(order_total) as total_spent",
|
||||
"orders"
|
||||
);
|
||||
query.GroupByClause.Clause = "user_id";
|
||||
query.HavingClause.Clause = "COUNT(*) > 1";
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
// Only users with more than 1 order should be returned
|
||||
foreach (var row in results)
|
||||
{
|
||||
Assert.That((long)row["order_count"], Is.GreaterThan(1));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LIMIT Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithLimit_ReturnsLimitedResults()
|
||||
{
|
||||
// Arrange - LIMIT is appended to ORDER BY clause
|
||||
var query = new QueryBreakdown("id, name", "users", null, "id ASC LIMIT 2");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithLimitAndOffset_SkipsAndLimitsResults()
|
||||
{
|
||||
// Arrange - OFFSET and LIMIT in ORDER BY clause
|
||||
var query = new QueryBreakdown("id, name", "users", null, "id ASC LIMIT 2 OFFSET 2");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Case Sensitivity Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_DoubleQuotedIdentifiers_PreservesCaseSensitivity()
|
||||
{
|
||||
// Arrange - PostgreSQL preserves case in double-quoted identifiers
|
||||
var query = new QueryBreakdown("\"id\", \"name\"", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
// Should not fail due to case mismatch
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Query Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_ComplexMultiJoinQuery_ReturnsCorrectResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown(
|
||||
"u.id, u.name, COUNT(o.id) as order_count, SUM(o.order_total) as revenue",
|
||||
"users u LEFT JOIN orders o ON u.id = o.user_id"
|
||||
);
|
||||
query.WhereClause.Clause = "u.active = true";
|
||||
query.GroupByClause.Clause = "u.id, u.name";
|
||||
query.OrderByClause.Clause = "revenue DESC NULLS LAST";
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
Assert.That(results.Count, Is.GreaterThanOrEqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_ParseAndExecuteRealSql_ReturnsResults()
|
||||
{
|
||||
// Arrange
|
||||
var sqlToParse = "SELECT u.id, u.name, COUNT(o.id) as order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.active = true GROUP BY u.id, u.name ORDER BY u.name";
|
||||
var query = QueryBreakdown.Parse(sqlToParse);
|
||||
|
||||
// Act
|
||||
var executeableSql = query.GetSql();
|
||||
var results = await ExecuteQuery(executeableSql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Type Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_HandlesDecimalDataTypes()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, price", "products", "price > 50");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
foreach (var row in results)
|
||||
{
|
||||
Assert.That(row["price"], Is.TypeOf<decimal>());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_HandlesBooleanDataTypes()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name, active", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
foreach (var row in results)
|
||||
{
|
||||
Assert.That(row["active"], Is.TypeOf<bool>());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_HandlesTimestampDataTypes()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, created_at", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
foreach (var row in results)
|
||||
{
|
||||
Assert.That(row["created_at"], Is.TypeOf<DateTime>());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
using NUnit.Framework;
|
||||
using Npgsql;
|
||||
using Testcontainers.PostgreSql;
|
||||
|
||||
namespace Strata.SqlTools.Tests.PostgreSql.TestContainers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for PostgreSQL testcontainer tests that handles container lifecycle.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("Integration")]
|
||||
public abstract class PostgreSqlTestContainerFixture
|
||||
{
|
||||
protected PostgreSqlContainer? Container { get; set; }
|
||||
protected NpgsqlDataSource? DataSource { get; set; }
|
||||
|
||||
[OneTimeSetUp]
|
||||
[Timeout(120000)] // 2 minutes for container startup + schema initialization
|
||||
public async Task OneTimeSetup()
|
||||
{
|
||||
// Create and start PostgreSQL container
|
||||
Container = new PostgreSqlBuilder()
|
||||
.WithImage("postgres:16-alpine")
|
||||
.WithDatabase("testdb")
|
||||
.WithUsername("testuser")
|
||||
.WithPassword("testpassword")
|
||||
.WithCleanUp(true)
|
||||
.Build();
|
||||
|
||||
await Container.StartAsync();
|
||||
|
||||
// Create data source for connection pooling
|
||||
var connectionString = Container.GetConnectionString();
|
||||
DataSource = NpgsqlDataSource.Create(connectionString);
|
||||
|
||||
// Initialize test database schema
|
||||
await InitializeSchema();
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public async Task OneTimeTearDown()
|
||||
{
|
||||
if (DataSource != null)
|
||||
{
|
||||
await DataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
if (Container != null)
|
||||
{
|
||||
await Container.StopAsync();
|
||||
await Container.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to initialize the test database schema with tables, etc.
|
||||
/// </summary>
|
||||
protected virtual async Task InitializeSchema()
|
||||
{
|
||||
if (DataSource == null)
|
||||
return;
|
||||
|
||||
using var command = DataSource.CreateCommand(@"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
order_total DECIMAL(10, 2) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
in_stock BOOLEAN DEFAULT true
|
||||
);
|
||||
");
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a SQL query and returns the result set.
|
||||
/// </summary>
|
||||
protected async Task<List<Dictionary<string, object>>> ExecuteQuery(string sql)
|
||||
{
|
||||
if (DataSource == null)
|
||||
throw new InvalidOperationException("DataSource is not initialized");
|
||||
|
||||
var results = new List<Dictionary<string, object>>();
|
||||
|
||||
using var command = DataSource.CreateCommand(sql);
|
||||
using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
var row = new Dictionary<string, object>();
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
row[reader.GetName(i)] = reader.GetValue(i);
|
||||
}
|
||||
results.Add(row);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all test data from tables.
|
||||
/// </summary>
|
||||
protected async Task ClearTestData()
|
||||
{
|
||||
if (DataSource == null)
|
||||
return;
|
||||
|
||||
using var command = DataSource.CreateCommand(@"
|
||||
TRUNCATE TABLE orders, users, products RESTART IDENTITY CASCADE;
|
||||
");
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="NUnit" Version="4.1.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="Testcontainers" Version="3.9.0" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="3.9.0" />
|
||||
<PackageReference Include="Npgsql" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
<ProjectReference Include="..\..\src\Strata.SqlTools.PostgreSql\Strata.SqlTools.PostgreSql.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
using NUnit.Framework;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
|
||||
namespace Strata.SqlTools.Tests.SqlServer.TestContainers;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for SQL Server QueryBreakdown using testcontainers.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class SqlServerQueryBreakdownIntegrationTests : SqlServerTestContainerFixture
|
||||
{
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
await ClearTestData();
|
||||
await InsertTestData();
|
||||
}
|
||||
|
||||
private async Task InsertTestData()
|
||||
{
|
||||
// INSERT with explicit IDs to ensure correct values
|
||||
await ExecuteNonQuery(@"
|
||||
SET IDENTITY_INSERT users ON;
|
||||
INSERT INTO users (id, name, email, active) VALUES
|
||||
(1, 'Alice Johnson', 'alice@example.com', 1),
|
||||
(2, 'Bob Smith', 'bob@example.com', 1),
|
||||
(3, 'Charlie Brown', 'charlie@example.com', 0),
|
||||
(4, 'Diana Prince', 'diana@example.com', 1);
|
||||
SET IDENTITY_INSERT users OFF;
|
||||
");
|
||||
|
||||
// Insert orders
|
||||
await ExecuteNonQuery(@"
|
||||
INSERT INTO orders (user_id, order_total) VALUES
|
||||
(1, 99.99),
|
||||
(1, 150.50),
|
||||
(2, 75.25),
|
||||
(3, 200.00),
|
||||
(4, 125.75);
|
||||
");
|
||||
|
||||
// Insert products
|
||||
await ExecuteNonQuery(@"
|
||||
INSERT INTO products (name, price, in_stock) VALUES
|
||||
('Laptop', 999.99, 1),
|
||||
('Mouse', 29.99, 1),
|
||||
('Keyboard', 79.99, 0),
|
||||
('Monitor', 299.99, 1);
|
||||
");
|
||||
}
|
||||
|
||||
#region Basic SELECT Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectAllUsers_ReturnsRows()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name, email", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
Assert.That(results.Count, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectActiveUsers_ReturnsActiveOnly()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name", "users", "active = 1");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithOrderBy_ReturnsOrderedResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name", "users", null, "name ASC");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
Assert.That((string)results[0]["name"], Is.EqualTo("Alice Johnson"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JOIN Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithJoin_ReturnsJoinedData()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown(
|
||||
"u.id, u.name, COUNT(o.id) as order_count",
|
||||
"users u LEFT JOIN orders o ON u.id = o.user_id",
|
||||
null,
|
||||
"u.name ASC"
|
||||
);
|
||||
query.GroupByClause.Clause = "u.id, u.name";
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
Assert.That(results.Count, Is.GreaterThanOrEqualTo(2));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WHERE Clause with Parameters Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithParameterizedQuery_ReturnsFilteredResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name, email", "users", "id = @userId");
|
||||
query.AddParameter("userId", 1);
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
// In real scenario, would use parameterized query
|
||||
var results = await ExecuteQuery("SELECT id, name, email FROM users WHERE id = 1");
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
Assert.That((string)results[0]["name"], Is.EqualTo("Alice Johnson"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithStringParameter_ReturnsFilteredResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name, email", "users", "email = @email");
|
||||
query.AddParameter("email", "bob@example.com");
|
||||
|
||||
// Act
|
||||
var results = await ExecuteQuery("SELECT id, name, email FROM users WHERE email = 'bob@example.com'");
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
Assert.That((string)results[0]["name"], Is.EqualTo("Bob Smith"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Aggregate Function Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_AggregateCount_ReturnsAggregateResult()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("COUNT(*) as total_users", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
Assert.That((int)results[0]["total_users"], Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_AggregateSum_ReturnsSumResult()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("SUM(order_total) as total_revenue", "orders");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(1));
|
||||
var totalRevenue = results[0]["total_revenue"];
|
||||
Assert.That(totalRevenue, Is.Not.EqualTo(DBNull.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_GroupByWithHaving_FiltersAggregateResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown(
|
||||
"user_id, COUNT(*) as order_count, SUM(order_total) as total_spent",
|
||||
"orders"
|
||||
);
|
||||
query.GroupByClause.Clause = "user_id";
|
||||
query.HavingClause.Clause = "COUNT(*) > 1";
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
// Only users with more than 1 order should be returned
|
||||
foreach (var row in results)
|
||||
{
|
||||
Assert.That((int)row["order_count"], Is.GreaterThan(1));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TOP (LIMIT equivalent) Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithTop_ReturnsLimitedResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("TOP 2 id, name", "users", null, "id ASC");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SelectWithOffsetFetch_SkipsAndLimitsResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name", "users", null, "id ASC OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CTE (WITH Clause) Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_WithCommonTableExpression_ExecutesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var mainQuery = new QueryBreakdown("user_id, order_count", "user_orders");
|
||||
mainQuery.AddWithClause("user_orders",
|
||||
"SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id");
|
||||
|
||||
// Act
|
||||
var sql = mainQuery.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Type Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_HandlesDecimalDataTypes()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, price", "products", "price > 50");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
foreach (var row in results)
|
||||
{
|
||||
Assert.That(row["price"], Is.TypeOf<decimal>());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_HandlesBitDataTypes()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, name, active", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
foreach (var row in results)
|
||||
{
|
||||
Assert.That(row["active"], Is.TypeOf<bool>());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_HandlesDateTimeDataTypes()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown("id, created_at", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
foreach (var row in results)
|
||||
{
|
||||
Assert.That(row["created_at"], Is.TypeOf<DateTime>());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Query Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_ComplexMultiJoinQuery_ReturnsCorrectResults()
|
||||
{
|
||||
// Arrange
|
||||
var query = new QueryBreakdown(
|
||||
"u.id, u.name, COUNT(o.id) as order_count, SUM(o.order_total) as revenue",
|
||||
"users u LEFT JOIN orders o ON u.id = o.user_id"
|
||||
);
|
||||
query.WhereClause.Clause = "u.active = 1";
|
||||
query.GroupByClause.Clause = "u.id, u.name";
|
||||
query.OrderByClause.Clause = "revenue DESC";
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_ParseAndExecuteRealSql_ReturnsResults()
|
||||
{
|
||||
// Arrange
|
||||
var sqlToParse = "SELECT u.id, u.name, COUNT(o.id) as order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.active = 1 GROUP BY u.id, u.name ORDER BY u.name";
|
||||
var query = QueryBreakdown.Parse(sqlToParse);
|
||||
|
||||
// Act
|
||||
var executeableSql = query.GetSql();
|
||||
var results = await ExecuteQuery(executeableSql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Case Sensitivity Tests
|
||||
|
||||
[Test]
|
||||
public async Task QueryBreakdown_SquareBracketIdentifiers_PreservesIdentifiers()
|
||||
{
|
||||
// Arrange - SQL Server uses square brackets for case-sensitive identifiers
|
||||
var query = new QueryBreakdown("[id], [name]", "users");
|
||||
|
||||
// Act
|
||||
var sql = query.GetSql();
|
||||
var results = await ExecuteQuery(sql);
|
||||
|
||||
// Assert
|
||||
Assert.That(results, Is.Not.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
using NUnit.Framework;
|
||||
using System.Data.SqlClient;
|
||||
using Testcontainers.MsSql;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using DotNet.Testcontainers.Builders;
|
||||
|
||||
namespace Strata.SqlTools.Tests.SqlServer.TestContainers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for SQL Server testcontainer tests that handles container lifecycle.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("Integration")]
|
||||
public abstract class SqlServerTestContainerFixture
|
||||
{
|
||||
protected MsSqlContainer? Container { get; set; }
|
||||
protected string? ConnectionString { get; set; }
|
||||
|
||||
[OneTimeSetUp]
|
||||
[CancelAfter(300000)] // 5 minutes for container startup + database initialization
|
||||
public async Task OneTimeSetup()
|
||||
{
|
||||
// Create and start SQL Server container
|
||||
Container = new MsSqlBuilder()
|
||||
.WithImage("mcr.microsoft.com/mssql/server:2022-latest")
|
||||
.WithPassword("SqlServerP@ss123")
|
||||
.WithCleanUp(true)
|
||||
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(1433))
|
||||
.Build();
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(4));
|
||||
await Container.StartAsync(cts.Token);
|
||||
|
||||
// Get connection string and wait for SQL Server to be ready
|
||||
ConnectionString = Container.GetConnectionString();
|
||||
await WaitForSqlServerReady(cts.Token);
|
||||
|
||||
// Drop any existing tables and initialize schema
|
||||
await DropExistingTables();
|
||||
await InitializeDatabase();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for SQL Server to be ready to accept connections with retry logic.
|
||||
/// </summary>
|
||||
private async Task WaitForSqlServerReady(CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ConnectionString))
|
||||
{
|
||||
throw new InvalidOperationException("ConnectionString is not initialized");
|
||||
}
|
||||
|
||||
const int maxRetries = 30;
|
||||
const int delayMs = 2000; // 2 seconds between retries
|
||||
|
||||
for (int i = 0; i < maxRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var connection = new SqlConnection(ConnectionString);
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
// Try a simple query to ensure SQL Server is fully ready
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT 1";
|
||||
await command.ExecuteScalarAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
catch (SqlException) when (i < maxRetries - 1)
|
||||
{
|
||||
await Task.Delay(delayMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"SQL Server did not become ready after {maxRetries} attempts");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops existing test tables to ensure clean state.
|
||||
/// </summary>
|
||||
private async Task DropExistingTables()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ConnectionString))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using var connection = new SqlConnection(ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'orders')
|
||||
DROP TABLE orders;
|
||||
|
||||
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'products')
|
||||
DROP TABLE products;
|
||||
|
||||
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'users')
|
||||
DROP TABLE users;
|
||||
";
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors - tables might not exist
|
||||
}
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public async Task OneTimeTearDown()
|
||||
{
|
||||
if (Container != null)
|
||||
{
|
||||
await Container.StopAsync();
|
||||
await Container.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to initialize the test database with tables, etc.
|
||||
/// </summary>
|
||||
protected virtual async Task InitializeDatabase()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ConnectionString))
|
||||
return;
|
||||
|
||||
using var connection = new SqlConnection(ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
-- Drop existing tables and constraints to ensure clean state
|
||||
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'orders')
|
||||
DROP TABLE orders;
|
||||
|
||||
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'products')
|
||||
DROP TABLE products;
|
||||
|
||||
IF EXISTS (SELECT * FROM sys.tables WHERE name = 'users')
|
||||
DROP TABLE users;
|
||||
|
||||
-- Create users table
|
||||
CREATE TABLE users (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
name NVARCHAR(255) NOT NULL,
|
||||
email NVARCHAR(255) UNIQUE NOT NULL,
|
||||
active BIT DEFAULT 1,
|
||||
created_at DATETIME DEFAULT GETUTCDATE()
|
||||
);
|
||||
|
||||
-- Create orders table with foreign key
|
||||
CREATE TABLE orders (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
order_total DECIMAL(10, 2) NOT NULL,
|
||||
created_at DATETIME DEFAULT GETUTCDATE(),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create products table
|
||||
CREATE TABLE products (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
name NVARCHAR(255) NOT NULL,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
in_stock BIT DEFAULT 1
|
||||
);
|
||||
";
|
||||
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a SQL query and returns the result set.
|
||||
/// </summary>
|
||||
protected async Task<List<Dictionary<string, object>>> ExecuteQuery(string sql)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ConnectionString))
|
||||
throw new InvalidOperationException("ConnectionString is not initialized");
|
||||
|
||||
var results = new List<Dictionary<string, object>>();
|
||||
|
||||
using var connection = new SqlConnection(ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
using var reader = await command.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
var row = new Dictionary<string, object>();
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
row[reader.GetName(i)] = reader.GetValue(i) ?? DBNull.Value;
|
||||
}
|
||||
results.Add(row);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a non-query SQL statement.
|
||||
/// </summary>
|
||||
protected async Task ExecuteNonQuery(string sql)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ConnectionString))
|
||||
throw new InvalidOperationException("ConnectionString is not initialized");
|
||||
|
||||
using var connection = new SqlConnection(ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all test data from tables by deleting and resetting identity seeds.
|
||||
/// </summary>
|
||||
protected async Task ClearTestData()
|
||||
{
|
||||
// Delete in reverse dependency order
|
||||
await ExecuteNonQuery("DELETE FROM orders");
|
||||
await ExecuteNonQuery("DELETE FROM users");
|
||||
await ExecuteNonQuery("DELETE FROM products");
|
||||
|
||||
// Reseed identity columns to 0, which makes next INSERT use 1
|
||||
// This works even if identity was previously higher
|
||||
await ExecuteNonQuery("DBCC CHECKIDENT ('users', RESEED, 0)");
|
||||
await ExecuteNonQuery("DBCC CHECKIDENT ('orders', RESEED, 0)");
|
||||
await ExecuteNonQuery("DBCC CHECKIDENT ('products', RESEED, 0)");
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="NUnit" Version="4.1.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="Testcontainers" Version="3.9.0" />
|
||||
<PackageReference Include="Testcontainers.MsSql" Version="3.9.0" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.8.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
<ProjectReference Include="..\..\src\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user