From 55591f72c771aab6a828349c5f63080682a3764d Mon Sep 17 00:00:00 2001 From: Andrew Stewart Gibson Date: Wed, 12 Nov 2025 20:53:43 +0000 Subject: [PATCH 1/3] feat: step 1 --- cwl_tsql/sql/DDL/001-create_table_product.sql | 4 ++ cwl_tsql/sql/DDL/002-create_table_basket.sql | 5 +++ .../003-alter_table_basket_customer_id.sql | 3 ++ cwl_tsql/sql/DML/generate_receipt.sql | 10 +++++ cwl_tsql/sql/DML/scan_product.sql | 2 + cwl_tsql/sql/fixtures/_begin_test.sql | 4 ++ cwl_tsql/sql/fixtures/_end_test.sql | 10 +++++ .../fixtures/assert_actual_eq_expected.sql | 5 +++ .../fixtures/assert_actual_like_expected.sql | 5 +++ cwl_tsql/sql/run_tests.sh | 16 ++++++++ cwl_tsql/sql/seed/merge_products.sql | 22 +++++++++++ ..._receipt_should_include_added_products.sql | 32 ++++++++++++++++ ...rate_receipt_should_count_each_product.sql | 38 +++++++++++++++++++ 13 files changed, 156 insertions(+) create mode 100644 cwl_tsql/sql/DDL/001-create_table_product.sql create mode 100644 cwl_tsql/sql/DDL/002-create_table_basket.sql create mode 100644 cwl_tsql/sql/DDL/003-alter_table_basket_customer_id.sql create mode 100644 cwl_tsql/sql/DML/generate_receipt.sql create mode 100644 cwl_tsql/sql/DML/scan_product.sql create mode 100644 cwl_tsql/sql/fixtures/_begin_test.sql create mode 100644 cwl_tsql/sql/fixtures/_end_test.sql create mode 100644 cwl_tsql/sql/fixtures/assert_actual_eq_expected.sql create mode 100644 cwl_tsql/sql/fixtures/assert_actual_like_expected.sql create mode 100755 cwl_tsql/sql/run_tests.sh create mode 100644 cwl_tsql/sql/seed/merge_products.sql create mode 100644 cwl_tsql/sql/tests/001 - generate_receipt_should_include_added_products.sql create mode 100644 cwl_tsql/sql/tests/002 - generate_receipt_should_count_each_product.sql diff --git a/cwl_tsql/sql/DDL/001-create_table_product.sql b/cwl_tsql/sql/DDL/001-create_table_product.sql new file mode 100644 index 0000000..8314970 --- /dev/null +++ b/cwl_tsql/sql/DDL/001-create_table_product.sql @@ -0,0 +1,4 @@ +CREATE TABLE Product ( + Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY, + [Name] NVARCHAR(50) +) \ No newline at end of file diff --git a/cwl_tsql/sql/DDL/002-create_table_basket.sql b/cwl_tsql/sql/DDL/002-create_table_basket.sql new file mode 100644 index 0000000..5521db8 --- /dev/null +++ b/cwl_tsql/sql/DDL/002-create_table_basket.sql @@ -0,0 +1,5 @@ +CREATE TABLE Basket ( + Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY, + ProductId INT NOT NULL + CONSTRAINT FK_Basket_Product FOREIGN KEY (ProductId) REFERENCES Product(Id) +) \ No newline at end of file diff --git a/cwl_tsql/sql/DDL/003-alter_table_basket_customer_id.sql b/cwl_tsql/sql/DDL/003-alter_table_basket_customer_id.sql new file mode 100644 index 0000000..cc369bb --- /dev/null +++ b/cwl_tsql/sql/DDL/003-alter_table_basket_customer_id.sql @@ -0,0 +1,3 @@ +ALTER TABLE basket +ADD CustomerId VARCHAR(100) NOT NULL +; diff --git a/cwl_tsql/sql/DML/generate_receipt.sql b/cwl_tsql/sql/DML/generate_receipt.sql new file mode 100644 index 0000000..affa020 --- /dev/null +++ b/cwl_tsql/sql/DML/generate_receipt.sql @@ -0,0 +1,10 @@ +SELECT + CONCAT(i, ') ', [Name], '. ', quantity) +FROM ( + SELECT + p.[Name], + COUNT(1) as quantity, + DENSE_RANK() OVER (ORDER BY p.[Name]) AS i + FROM dbo.Basket b JOIN dbo.Product p ON b.ProductId = p.Id + GROUP BY p.[Name] +) x \ No newline at end of file diff --git a/cwl_tsql/sql/DML/scan_product.sql b/cwl_tsql/sql/DML/scan_product.sql new file mode 100644 index 0000000..a142605 --- /dev/null +++ b/cwl_tsql/sql/DML/scan_product.sql @@ -0,0 +1,2 @@ +INSERT INTO dbo.Basket (ProductId, CustomerId) +VALUES (@ProductId, @CustomerId) \ No newline at end of file diff --git a/cwl_tsql/sql/fixtures/_begin_test.sql b/cwl_tsql/sql/fixtures/_begin_test.sql new file mode 100644 index 0000000..f89c5f6 --- /dev/null +++ b/cwl_tsql/sql/fixtures/_begin_test.sql @@ -0,0 +1,4 @@ +DECLARE @msg NVARCHAR(MAX); + +BEGIN TRANSACTION test; +BEGIN TRY \ No newline at end of file diff --git a/cwl_tsql/sql/fixtures/_end_test.sql b/cwl_tsql/sql/fixtures/_end_test.sql new file mode 100644 index 0000000..0836a84 --- /dev/null +++ b/cwl_tsql/sql/fixtures/_end_test.sql @@ -0,0 +1,10 @@ + + ROLLBACK TRANSACTION test; + +END TRY +BEGIN CATCH + + ROLLBACK TRANSACTION test; + THROW; + +END CATCH \ No newline at end of file diff --git a/cwl_tsql/sql/fixtures/assert_actual_eq_expected.sql b/cwl_tsql/sql/fixtures/assert_actual_eq_expected.sql new file mode 100644 index 0000000..8978211 --- /dev/null +++ b/cwl_tsql/sql/fixtures/assert_actual_eq_expected.sql @@ -0,0 +1,5 @@ +IF (@actual <> @expected) +BEGIN + PRINT FORMATMESSAGE(N'Expected: %s Actual: %s', COALESCE(@expected, ''), COALESCE(@actual, '')); + THROW 50000, @msg, 1; +END \ No newline at end of file diff --git a/cwl_tsql/sql/fixtures/assert_actual_like_expected.sql b/cwl_tsql/sql/fixtures/assert_actual_like_expected.sql new file mode 100644 index 0000000..8e6d270 --- /dev/null +++ b/cwl_tsql/sql/fixtures/assert_actual_like_expected.sql @@ -0,0 +1,5 @@ +IF NOT(@actual LIKE @expected) +BEGIN + PRINT FORMATMESSAGE(N'Expected: %s Actual: %s', COALESCE(@expected, ''), COALESCE(@actual, '')); + THROW 50000, @msg, 1; +END \ No newline at end of file diff --git a/cwl_tsql/sql/run_tests.sh b/cwl_tsql/sql/run_tests.sh new file mode 100755 index 0000000..3906261 --- /dev/null +++ b/cwl_tsql/sql/run_tests.sh @@ -0,0 +1,16 @@ +PASSWORD='Password1!' +TEST_DIR="./tests" + +for test_file in "$TEST_DIR"/*.sql; do + test_name=$(basename "$test_file") + + # Run test + if sqlcmd -S ::1 -d supermarket -C -U sa -P "$PASSWORD" -b -i "$test_file" > /tmp/sql_test_output 2>&1; then + echo "_/ PASS: $test_name" + else + echo " X FAIL: $test_name" + sed 's/^/ /' /tmp/sql_test_output | tail -n 10 # show last few lines of output + fi + + echo +done \ No newline at end of file diff --git a/cwl_tsql/sql/seed/merge_products.sql b/cwl_tsql/sql/seed/merge_products.sql new file mode 100644 index 0000000..5bc37a7 --- /dev/null +++ b/cwl_tsql/sql/seed/merge_products.sql @@ -0,0 +1,22 @@ +DECLARE @SummaryOfChanges TABLE (Change VARCHAR(20)) +; + +MERGE INTO + dbo.Product AS tgt +USING + (VALUES + ('Milk'), + ('Bread') + ) AS src([Name]) +ON tgt.[Name] = src.[Name] +WHEN NOT MATCHED BY TARGET THEN + INSERT ([Name]) + VALUES (src.[Name]) +OUTPUT $action +INTO @SummaryOfChanges +; + +SELECT Change, COUNT(*) as ChangeCount +FROM @SummaryOfChanges +GROUP BY Change +; diff --git a/cwl_tsql/sql/tests/001 - generate_receipt_should_include_added_products.sql b/cwl_tsql/sql/tests/001 - generate_receipt_should_include_added_products.sql new file mode 100644 index 0000000..fc95ff6 --- /dev/null +++ b/cwl_tsql/sql/tests/001 - generate_receipt_should_include_added_products.sql @@ -0,0 +1,32 @@ +-- ARRANGE +SET NOCOUNT ON; + +DECLARE @CustomerId UNIQUEIDENTIFIER = NEWID() +DECLARE @MilkId INT +DECLARE @BreadId INT +DECLARE @ProductId INT + +SELECT @MilkId = p.Id FROM dbo.Product p WHERE p.[Name] = 'Milk' +SELECT @BreadId = p.Id FROM dbo.Product p WHERE p.[Name] = 'Bread' + +DECLARE @actual NVARCHAR(MAX), @expected NVARCHAR(MAX) +CREATE TABLE #result ( x NVARCHAR(MAX) ) + +:r ./fixtures/_begin_test.sql + +-- ACT +SET @ProductId = @MilkId +:r ./DML/scan_product.sql + +SET @ProductId = @BreadId +:r ./DML/scan_product.sql + +INSERT INTO #result +:r ./DML/generate_receipt.sql + +-- ASSERT +SELECT @actual = STRING_AGG(x, '\n') FROM #result +SELECT @expected = '%1) Bread.%2) Milk.%' +:r ./fixtures/assert_actual_like_expected.sql + +:r ./fixtures/_end_test.sql \ No newline at end of file diff --git a/cwl_tsql/sql/tests/002 - generate_receipt_should_count_each_product.sql b/cwl_tsql/sql/tests/002 - generate_receipt_should_count_each_product.sql new file mode 100644 index 0000000..5395a1a --- /dev/null +++ b/cwl_tsql/sql/tests/002 - generate_receipt_should_count_each_product.sql @@ -0,0 +1,38 @@ +-- ARRANGE +SET NOCOUNT ON; + +DECLARE @CustomerId UNIQUEIDENTIFIER = NEWID() +DECLARE @MilkId INT +DECLARE @BreadId INT +DECLARE @ProductId INT + +SELECT @MilkId = p.Id FROM dbo.Product p WHERE p.[Name] = 'Milk' +SELECT @BreadId = p.Id FROM dbo.Product p WHERE p.[Name] = 'Bread' + +DECLARE @actual NVARCHAR(MAX), @expected NVARCHAR(MAX) +CREATE TABLE #result ( x NVARCHAR(MAX) ) + +:r ./fixtures/_begin_test.sql + +-- ACT +SET @ProductId = @MilkId +:r ./DML/scan_product.sql +:r ./DML/scan_product.sql + +SET @ProductId = @BreadId +:r ./DML/scan_product.sql + +INSERT INTO #result +:r ./DML/generate_receipt.sql + +-- ASSERT +SELECT @actual = STRING_AGG(x, '\n') FROM #result +SELECT @expected = '%Bread. 1%' +:r ./fixtures/assert_actual_like_expected.sql + + +SELECT @expected = '%Milk. 2%' +:r ./fixtures/assert_actual_like_expected.sql + + +:r ./fixtures/_end_test.sql \ No newline at end of file From 39282e6c7a90cfbbfef424747f11e92b50ce25b8 Mon Sep 17 00:00:00 2001 From: Andrew Stewart Gibson Date: Wed, 12 Nov 2025 21:05:27 +0000 Subject: [PATCH 2/3] feat: add product price --- cwl_tsql/README.md | 7 ++++ .../DDL/004-alter_table_product_add_price.sql | 2 + cwl_tsql/sql/DML/generate_receipt.sql | 5 ++- cwl_tsql/sql/run_tests.sh | 3 +- cwl_tsql/sql/seed/merge_products.sql | 12 +++--- ... generate_receipt_should_include_price.sql | 38 +++++++++++++++++++ 6 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 cwl_tsql/README.md create mode 100644 cwl_tsql/sql/DDL/004-alter_table_product_add_price.sql create mode 100644 cwl_tsql/sql/tests/003 - generate_receipt_should_include_price.sql diff --git a/cwl_tsql/README.md b/cwl_tsql/README.md new file mode 100644 index 0000000..8c3667e --- /dev/null +++ b/cwl_tsql/README.md @@ -0,0 +1,7 @@ +Assumes a local server (see the run_tests.sh to configure server and password). Make sure you don't use sa for this very important data. + +To run the tests: +``` +cd sql +./run_tests.sh +``` \ No newline at end of file diff --git a/cwl_tsql/sql/DDL/004-alter_table_product_add_price.sql b/cwl_tsql/sql/DDL/004-alter_table_product_add_price.sql new file mode 100644 index 0000000..c83807e --- /dev/null +++ b/cwl_tsql/sql/DDL/004-alter_table_product_add_price.sql @@ -0,0 +1,2 @@ +ALTER TABLE dbo.Product +ADD Price DECIMAL(10,2) NOT NULL DEFAULT(0) \ No newline at end of file diff --git a/cwl_tsql/sql/DML/generate_receipt.sql b/cwl_tsql/sql/DML/generate_receipt.sql index affa020..af81c0b 100644 --- a/cwl_tsql/sql/DML/generate_receipt.sql +++ b/cwl_tsql/sql/DML/generate_receipt.sql @@ -1,10 +1,11 @@ SELECT - CONCAT(i, ') ', [Name], '. ', quantity) + CONCAT(i, ') ', [Name], '. ', quantity, ' @ £', price) FROM ( SELECT p.[Name], + p.Price, COUNT(1) as quantity, DENSE_RANK() OVER (ORDER BY p.[Name]) AS i FROM dbo.Basket b JOIN dbo.Product p ON b.ProductId = p.Id - GROUP BY p.[Name] + GROUP BY p.[Name], p.Price ) x \ No newline at end of file diff --git a/cwl_tsql/sql/run_tests.sh b/cwl_tsql/sql/run_tests.sh index 3906261..c987c78 100755 --- a/cwl_tsql/sql/run_tests.sh +++ b/cwl_tsql/sql/run_tests.sh @@ -1,11 +1,12 @@ PASSWORD='Password1!' +SERVER='::1' TEST_DIR="./tests" for test_file in "$TEST_DIR"/*.sql; do test_name=$(basename "$test_file") # Run test - if sqlcmd -S ::1 -d supermarket -C -U sa -P "$PASSWORD" -b -i "$test_file" > /tmp/sql_test_output 2>&1; then + if sqlcmd -S "$SERVER" -d supermarket -C -U sa -P "$PASSWORD" -b -i "$test_file" > /tmp/sql_test_output 2>&1; then echo "_/ PASS: $test_name" else echo " X FAIL: $test_name" diff --git a/cwl_tsql/sql/seed/merge_products.sql b/cwl_tsql/sql/seed/merge_products.sql index 5bc37a7..58e1037 100644 --- a/cwl_tsql/sql/seed/merge_products.sql +++ b/cwl_tsql/sql/seed/merge_products.sql @@ -5,13 +5,15 @@ MERGE INTO dbo.Product AS tgt USING (VALUES - ('Milk'), - ('Bread') - ) AS src([Name]) + ('Milk', 1.5), + ('Bread', 3.5) + ) AS src([Name], Price) ON tgt.[Name] = src.[Name] +WHEN MATCHED THEN + UPDATE SET Price = src.Price WHEN NOT MATCHED BY TARGET THEN - INSERT ([Name]) - VALUES (src.[Name]) + INSERT ([Name], Price) + VALUES (src.[Name], src.Price) OUTPUT $action INTO @SummaryOfChanges ; diff --git a/cwl_tsql/sql/tests/003 - generate_receipt_should_include_price.sql b/cwl_tsql/sql/tests/003 - generate_receipt_should_include_price.sql new file mode 100644 index 0000000..7fb6e47 --- /dev/null +++ b/cwl_tsql/sql/tests/003 - generate_receipt_should_include_price.sql @@ -0,0 +1,38 @@ +-- ARRANGE +SET NOCOUNT ON; + +DECLARE @CustomerId UNIQUEIDENTIFIER = NEWID() +DECLARE @MilkId INT +DECLARE @BreadId INT +DECLARE @ProductId INT + +SELECT @MilkId = p.Id FROM dbo.Product p WHERE p.[Name] = 'Milk' +SELECT @BreadId = p.Id FROM dbo.Product p WHERE p.[Name] = 'Bread' + +DECLARE @actual NVARCHAR(MAX), @expected NVARCHAR(MAX) +CREATE TABLE #result ( x NVARCHAR(MAX) ) + +:r ./fixtures/_begin_test.sql + +-- ACT +SET @ProductId = @MilkId +:r ./DML/scan_product.sql +:r ./DML/scan_product.sql + +SET @ProductId = @BreadId +:r ./DML/scan_product.sql + +INSERT INTO #result +:r ./DML/generate_receipt.sql + +-- ASSERT +SELECT @actual = STRING_AGG(x, '\n') FROM #result +SELECT @expected = '%Bread. 1 @ £3.50%' +:r ./fixtures/assert_actual_like_expected.sql + + +SELECT @expected = '%Milk. 2 @ £1.50%' +:r ./fixtures/assert_actual_like_expected.sql + + +:r ./fixtures/_end_test.sql \ No newline at end of file From bb65e7a1dfc0f0c424c7074332dc5bf98fb279cd Mon Sep 17 00:00:00 2001 From: Andrew Stewart Gibson Date: Wed, 12 Nov 2025 21:07:19 +0000 Subject: [PATCH 3/3] feat: line totals --- cwl_tsql/sql/DML/generate_receipt.sql | 2 +- ...rate_receipt_should_include_line_total.sql | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 cwl_tsql/sql/tests/004 - generate_receipt_should_include_line_total.sql diff --git a/cwl_tsql/sql/DML/generate_receipt.sql b/cwl_tsql/sql/DML/generate_receipt.sql index af81c0b..17fbe06 100644 --- a/cwl_tsql/sql/DML/generate_receipt.sql +++ b/cwl_tsql/sql/DML/generate_receipt.sql @@ -1,5 +1,5 @@ SELECT - CONCAT(i, ') ', [Name], '. ', quantity, ' @ £', price) + CONCAT(i, ') ', [Name], '. ', quantity, ' @ £', price, " = £", price * quantity) FROM ( SELECT p.[Name], diff --git a/cwl_tsql/sql/tests/004 - generate_receipt_should_include_line_total.sql b/cwl_tsql/sql/tests/004 - generate_receipt_should_include_line_total.sql new file mode 100644 index 0000000..0f21112 --- /dev/null +++ b/cwl_tsql/sql/tests/004 - generate_receipt_should_include_line_total.sql @@ -0,0 +1,38 @@ +-- ARRANGE +SET NOCOUNT ON; + +DECLARE @CustomerId UNIQUEIDENTIFIER = NEWID() +DECLARE @MilkId INT +DECLARE @BreadId INT +DECLARE @ProductId INT + +SELECT @MilkId = p.Id FROM dbo.Product p WHERE p.[Name] = 'Milk' +SELECT @BreadId = p.Id FROM dbo.Product p WHERE p.[Name] = 'Bread' + +DECLARE @actual NVARCHAR(MAX), @expected NVARCHAR(MAX) +CREATE TABLE #result ( x NVARCHAR(MAX) ) + +:r ./fixtures/_begin_test.sql + +-- ACT +SET @ProductId = @MilkId +:r ./DML/scan_product.sql +:r ./DML/scan_product.sql + +SET @ProductId = @BreadId +:r ./DML/scan_product.sql + +INSERT INTO #result +:r ./DML/generate_receipt.sql + +-- ASSERT +SELECT @actual = STRING_AGG(x, '\n') FROM #result +SELECT @expected = '%Bread. 1 @ £3.50 = £3.50%' +:r ./fixtures/assert_actual_like_expected.sql + + +SELECT @expected = '%Milk. 2 @ £1.50 = £3.00%' +:r ./fixtures/assert_actual_like_expected.sql + + +:r ./fixtures/_end_test.sql \ No newline at end of file