diff --git a/NEW/tests/JdeScoping.Database.Tests/Functions/SimpleTableFunctionTests.cs b/NEW/tests/JdeScoping.Database.Tests/Functions/SimpleTableFunctionTests.cs
new file mode 100644
index 0000000..b2c44d0
--- /dev/null
+++ b/NEW/tests/JdeScoping.Database.Tests/Functions/SimpleTableFunctionTests.cs
@@ -0,0 +1,618 @@
+using Dapper;
+using FluentAssertions;
+using JdeScoping.Core.Models.Search;
+using JdeScoping.Database.Tests.Infrastructure;
+
+namespace JdeScoping.Database.Tests.Functions;
+
+///
+/// Tests for simple table extraction functions that return single-column result sets.
+/// These inline TVFs extract arrays from Search.Criteria JSON.
+///
+[Collection("DatabaseTests")]
+public class SimpleTableFunctionTests : DatabaseTestBase
+{
+ #region fn_GetSearchWorkOrders Tests
+
+ [Fact]
+ public async Task fn_GetSearchWorkOrders_ValidArray_ReturnsAllValues()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { WorkOrderNumbers = [12345, 67890, 11111] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT WorkOrderNumber FROM dbo.fn_GetSearchWorkOrders(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEquivalentTo([12345L, 67890L, 11111L]);
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkOrders_EmptyArray_ReturnsEmpty()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { WorkOrderNumbers = [] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT WorkOrderNumber FROM dbo.fn_GetSearchWorkOrders(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkOrders_MissingProperty_ReturnsEmpty()
+ {
+ // Arrange - criteria without WorkOrderNumbers property set (uses default empty list)
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("{\"MinimumDt\":null}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT WorkOrderNumber FROM dbo.fn_GetSearchWorkOrders(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkOrders_SearchNotFound_ReturnsEmpty()
+ {
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT WorkOrderNumber FROM dbo.fn_GetSearchWorkOrders(@SearchId)",
+ new { SearchId = 99999 });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkOrders_NullCriteria_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(null);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT WorkOrderNumber FROM dbo.fn_GetSearchWorkOrders(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkOrders_InvalidJson_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("not valid json");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT WorkOrderNumber FROM dbo.fn_GetSearchWorkOrders(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkOrders_BadTypeValues_FilteredOut()
+ {
+ // Arrange - include null values that should be filtered out
+ // Note: OPENJSON...WITH BIGINT will filter out NULLs via WHERE clause,
+ // but will throw conversion error on incompatible types like strings.
+ // This test verifies NULL values are properly filtered.
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(
+ "{\"WorkOrderNumbers\":[12345, null, 67890]}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT WorkOrderNumber FROM dbo.fn_GetSearchWorkOrders(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert - null values should be filtered out
+ results.Should().BeEquivalentTo([12345L, 67890L]);
+ }
+
+ #endregion
+
+ #region fn_GetSearchItemNumbers Tests
+
+ [Fact]
+ public async Task fn_GetSearchItemNumbers_ValidArray_ReturnsAllValues()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { ItemNumbers = ["ITEM001", "ITEM002", "ITEM003"] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEquivalentTo(["ITEM001", "ITEM002", "ITEM003"]);
+ }
+
+ [Fact]
+ public async Task fn_GetSearchItemNumbers_EmptyArray_ReturnsEmpty()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { ItemNumbers = [] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchItemNumbers_MissingProperty_ReturnsEmpty()
+ {
+ // Arrange - criteria without ItemNumbers property
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("{\"MinimumDt\":null}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchItemNumbers_SearchNotFound_ReturnsEmpty()
+ {
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId)",
+ new { SearchId = 99999 });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchItemNumbers_NullCriteria_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(null);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchItemNumbers_InvalidJson_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("not valid json");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchItemNumbers_BadTypeValues_FilteredOut()
+ {
+ // Arrange - include null values that should be filtered out
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(
+ "{\"ItemNumbers\":[\"ITEM001\", null, \"ITEM002\"]}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert - null values should be filtered out
+ results.Should().BeEquivalentTo(["ITEM001", "ITEM002"]);
+ }
+
+ [Fact]
+ public async Task fn_GetSearchItemNumbers_SpecialCharacters_ReturnsCorrectly()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { ItemNumbers = ["ITEM-001", "ITEM_002", "ITEM.003"] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEquivalentTo(["ITEM-001", "ITEM_002", "ITEM.003"]);
+ }
+
+ #endregion
+
+ #region fn_GetSearchProfitCenters Tests
+
+ [Fact]
+ public async Task fn_GetSearchProfitCenters_ValidArray_ReturnsAllValues()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { ProfitCenters = ["PC01", "PC02", "PC03"] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchProfitCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEquivalentTo(["PC01", "PC02", "PC03"]);
+ }
+
+ [Fact]
+ public async Task fn_GetSearchProfitCenters_EmptyArray_ReturnsEmpty()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { ProfitCenters = [] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchProfitCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchProfitCenters_MissingProperty_ReturnsEmpty()
+ {
+ // Arrange - criteria without ProfitCenters property
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("{\"MinimumDt\":null}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchProfitCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchProfitCenters_SearchNotFound_ReturnsEmpty()
+ {
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchProfitCenters(@SearchId)",
+ new { SearchId = 99999 });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchProfitCenters_NullCriteria_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(null);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchProfitCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchProfitCenters_InvalidJson_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("not valid json");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchProfitCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchProfitCenters_BadTypeValues_FilteredOut()
+ {
+ // Arrange - include null values that should be filtered out
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(
+ "{\"ProfitCenters\":[\"PC01\", null, \"PC02\"]}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchProfitCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert - null values should be filtered out
+ results.Should().BeEquivalentTo(["PC01", "PC02"]);
+ }
+
+ #endregion
+
+ #region fn_GetSearchWorkCenters Tests
+
+ [Fact]
+ public async Task fn_GetSearchWorkCenters_ValidArray_ReturnsAllValues()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { WorkCenters = ["WC001", "WC002", "WC003"] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchWorkCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEquivalentTo(["WC001", "WC002", "WC003"]);
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkCenters_EmptyArray_ReturnsEmpty()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { WorkCenters = [] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchWorkCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkCenters_MissingProperty_ReturnsEmpty()
+ {
+ // Arrange - criteria without WorkCenters property
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("{\"MinimumDt\":null}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchWorkCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkCenters_SearchNotFound_ReturnsEmpty()
+ {
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchWorkCenters(@SearchId)",
+ new { SearchId = 99999 });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkCenters_NullCriteria_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(null);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchWorkCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkCenters_InvalidJson_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("not valid json");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchWorkCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchWorkCenters_BadTypeValues_FilteredOut()
+ {
+ // Arrange - include null values that should be filtered out
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(
+ "{\"WorkCenters\":[\"WC001\", null, \"WC002\"]}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT Code FROM dbo.fn_GetSearchWorkCenters(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert - null values should be filtered out
+ results.Should().BeEquivalentTo(["WC001", "WC002"]);
+ }
+
+ #endregion
+
+ #region fn_GetSearchOperatorIDs Tests
+
+ [Fact]
+ public async Task fn_GetSearchOperatorIDs_ValidArray_ReturnsAllValues()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { OperatorIDs = ["OP001", "OP002", "OP003"] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT OperatorID FROM dbo.fn_GetSearchOperatorIDs(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEquivalentTo(["OP001", "OP002", "OP003"]);
+ }
+
+ [Fact]
+ public async Task fn_GetSearchOperatorIDs_EmptyArray_ReturnsEmpty()
+ {
+ // Arrange
+ var criteria = new SearchCriteria { OperatorIDs = [] };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT OperatorID FROM dbo.fn_GetSearchOperatorIDs(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchOperatorIDs_MissingProperty_ReturnsEmpty()
+ {
+ // Arrange - criteria without OperatorIDs property
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("{\"MinimumDt\":null}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT OperatorID FROM dbo.fn_GetSearchOperatorIDs(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchOperatorIDs_SearchNotFound_ReturnsEmpty()
+ {
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT OperatorID FROM dbo.fn_GetSearchOperatorIDs(@SearchId)",
+ new { SearchId = 99999 });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchOperatorIDs_NullCriteria_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(null);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT OperatorID FROM dbo.fn_GetSearchOperatorIDs(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchOperatorIDs_InvalidJson_ReturnsEmpty()
+ {
+ // Arrange
+ var searchId = await InsertTestSearchWithRawCriteriaAsync("not valid json");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT OperatorID FROM dbo.fn_GetSearchOperatorIDs(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task fn_GetSearchOperatorIDs_BadTypeValues_FilteredOut()
+ {
+ // Arrange - include null values that should be filtered out
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(
+ "{\"OperatorIDs\":[\"OP001\", null, \"OP002\"]}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT OperatorID FROM dbo.fn_GetSearchOperatorIDs(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert - null values should be filtered out
+ results.Should().BeEquivalentTo(["OP001", "OP002"]);
+ }
+
+ #endregion
+
+ #region Additional Edge Case Tests
+
+ [Fact]
+ public async Task fn_GetSearchWorkOrders_LargeArray_ReturnsAll()
+ {
+ // Arrange
+ var workOrders = Enumerable.Range(1, 1000).Select(i => (long)i).ToList();
+ var criteria = new SearchCriteria { WorkOrderNumbers = workOrders };
+ var searchId = await InsertTestSearchAsync(criteria);
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT WorkOrderNumber FROM dbo.fn_GetSearchWorkOrders(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert
+ results.Should().HaveCount(1000);
+ results.Should().BeEquivalentTo(workOrders);
+ }
+
+ [Fact]
+ public async Task fn_GetSearchItemNumbers_LongValues_Truncated()
+ {
+ // Arrange - ItemNumber column is VARCHAR(128)
+ var longValue = new string('A', 200);
+ var searchId = await InsertTestSearchWithRawCriteriaAsync(
+ $"{{\"ItemNumbers\":[\"{longValue}\"]}}");
+
+ // Act
+ var results = await Connection.QueryAsync(
+ "SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId)",
+ new { SearchId = searchId });
+
+ // Assert - value should be truncated to 128 characters
+ results.Should().HaveCount(1);
+ results.First().Should().HaveLength(128);
+ }
+
+ #endregion
+}