diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index 5538a500..7b02d927 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -1,3 +1,8 @@ +### 1.5.29 - 02.11.2026 +- Fix: parameters of a sub-query nested in another sub-query are bound (IN, EXISTS, UNION and the like) +- Fix: groupBy keys with option or value-option columns, and with x.IsSome / x.IsNone in a tuple key +- PostgreSQL: identity columns count as generated, so the typed ``Create(...)`` no longer takes them. Breaking for code that passed a value to a BY DEFAULT identity column: use Create() and set it, or Create(data). A saved ContextSchemaPath cache keeps the old signature until regenerated. + ### 1.5.28 - 17.10.2026 - Fix for IndexOutOfRangeException when invoking a stored procedure, #878 - Code cleanups with fsharp-refactor tool diff --git a/docs/content/core/composable.fsx b/docs/content/core/composable.fsx index 9e79b76d..896474d2 100644 --- a/docs/content/core/composable.fsx +++ b/docs/content/core/composable.fsx @@ -170,8 +170,9 @@ query { for (i,n,e) in qry1 do ### Nested Select Where Queries You can create a query like `SELECT * FROM xs WHERE xs.x IN (SELECT y FROM ys))` -with either LINQ Contains or custom operators: in `|=|` and not-in `|<>|` -This is done by not saying `|> Seq.toArray` to the first query: +in the purist way with the F# operators `x |=| qry` (NOT IN: `x |<>| qry`), or in the pragmatist way with LINQ's +`.Contains x` (NOT IN: `not (ys.Contains x)`), which also runs in the mock context, so the query can be +unit-tested. Both give the same SQL. The example takes the pragmatist way: *) diff --git a/docs/content/core/crud.fsx b/docs/content/core/crud.fsx index 981daf56..15d2711c 100644 --- a/docs/content/core/crud.fsx +++ b/docs/content/core/crud.fsx @@ -34,10 +34,24 @@ let now = DateTime.Now (** Create() has various overloads to make inserting records simple. + +Prefer the typed \`\`Create(...)\`\`: its name lists the required columns (not nullable, not generated, sorted by +name), so adding a required column to the table breaks the build where rows are created instead of failing at +runtime. Set the optional columns afterwards. Call it with the backticked name, so two arguments of the same type +can't swap places unnoticed when the column list changes. +*) + +let emp = ctx.Main.Employees.``Create(FirstName, LastName)``("Don", "Syme") + +(** +Optional columns are set on the returned row, e.g. `emp.Title <- "Founder"` (or `ValueSome "Founder"` with +`UseOptionTypes = NullableColumnType.VALUE_OPTION`). A key the application generates, like a UUID, is set the same +way: `row.Id <- Guid.CreateVersion7()`. *) (** -Create a new row +The untyped `Create()` with property sets is for when you build rows dynamically. It compiles even when a required +column is left out: *) let row = orders.Create() @@ -61,14 +75,19 @@ ctx.SubmitUpdates() (** After updating, your item (row) will have the Id property. +To update a row without reading it first, create an entity with its key, set the columns, and mark only those +columns as modified: - -You can also create with the longer \`\`Create(...)\`\`(parameters)-method like this: +```fsharp +let update = ctx.Main.Orders.Create() +update.OrderId <- orderId +update.ShipCity <- "Leeds" +update._State <- Common.EntityState.Modified [ "ShipCity" ] +ctx.SubmitUpdates() +``` *) -let emp = ctx.Main.Employees.``Create(FirstName, LastName)``("Don", "Syme") - (** Delete the row diff --git a/docs/content/core/general.fsx b/docs/content/core/general.fsx index d34abcce..23ce12b7 100644 --- a/docs/content/core/general.fsx +++ b/docs/content/core/general.fsx @@ -144,6 +144,10 @@ a relevant query using the entity's primary key. ## Basic Querying The SQL provider supports LINQ queries using F#'s *query expression* syntax. + +The examples here select whole entities to keep them short. In application code, select only the columns you use, +into a tuple, an anonymous record or your domain type, and filter, sort and aggregate in the query: see +[recommended query style](querying.html#Recommended-query-style). *) let customersQuery = diff --git a/docs/content/core/querying.fsx b/docs/content/core/querying.fsx index 1a3faead..efc29ebf 100644 --- a/docs/content/core/querying.fsx +++ b/docs/content/core/querying.fsx @@ -54,10 +54,73 @@ let example = sortBy order.ShipPostalCode skip 3 take 4 - select order + select (order.OrderId, order.ShipPostalCode, order.Freight) } -let test = example |> Seq.toArray |> Array.map(fun i -> i.ColumnValues |> Map.ofSeq) +let test = example |> Seq.toArray + +//If you would need the full-width order as map, you could select it and then do |> Array.map(fun i -> i.ColumnValues |> Map.ofSeq) + +(** +### Recommended query style + +The query is translated to SQL, so the database does the filtering, sorting, joining, grouping and counting. Write +the business rule in the query, and keep what comes back small: + +- **Select what you use, typed.** Project to a tuple, an anonymous record `{| ... |}` or a domain record, not the + whole entity (`select order`). Use a tuple or an anonymous record when the result is used on the spot; when it + leaves the function (returned from an API, stored, shared) build your real domain type in the `select`. Whole + entities are for rows you are about to modify. Don't pass `ColumnValues` maps around as application data. +- **Filter and sort in the query, not after it.** `where (x.Removed.IsNone)` and `sortBy x.Created` in the query + instead of `Array.filter` / `Array.sortBy` on the fetched rows. Counts, minimums, maximums and existence are + queries too: `Seq.lengthAsync`, `groupBy ... into g; select (g.Key, g.Max(...))`, or `Seq.tryHeadAsync` on a + query that selects one column. +- **"IN" over a sub-query.** When the ids come from the database, don't fetch them first: leave the inner query + unexecuted (no `Seq.toArray`) and use it directly, so one SQL statement runs (see "Nested Select Where Queries" in + [composable queries](composable.html)). There are two ways to write the IN, and they give the same SQL: + + - The purist way, F# only: the operators `|=|` (IN) and `|<>|` (NOT IN). + - The pragmatist way: LINQ's `.Contains` extension method (`open System.Linq`), and `not (xs.Contains x)` for NOT + IN. It is familiar to anyone who knows LINQ, and it runs in the [mock data context](unittest.html), so the query + can be unit-tested; the operators don't run there. + +*) + +let activeCustomers = query { for c in ctx.Main.Customers do where (c.Country = "UK"); select c.CustomerId } + +// The purist way, no LINQ reference +let xs1 = + query { for o in ctx.Main.Orders do + where (o.CustomerId |=| activeCustomers) + select (o.OrderId, o.Freight) } + +// The pragmatist way, easeier to read, easier to unit-test +open System.Linq +let xs2 = + query { for o in ctx.Main.Orders do + where (activeCustomers.Contains o.CustomerId) + select (o.OrderId, o.Freight) } +(** + + F#'s own `Seq.contains` is not translated to SQL. +- **One query for every case of a filter.** Compute flags outside the query and test them inside it, so the same + query can serve "all" and "one": + +*) +// query parameters: +let allCountries = false // or true +let onlyCountry = "Finland" // or "" + +query { for c in ctx.Main.Customers do + where (allCountries || c.Country = onlyCountry) + select c.CustomerId } +(** + +- **No N+1.** Don't run a query per row of another query. Join, or read the children of all the parents in one + query (`parentIds.Contains child.ParentId`, with `parentIds` a sub-query) and group them in memory. +- **Pass the data context in.** Functions that take the context as a parameter can be tested with the mock context. + +*) let item = query { @@ -416,8 +479,8 @@ let bergs = ctx.Main.Customers.Individuals.BERGS You can find some custom operators `using FSharp.Data.Sql`: -* `|=|` (In set) -* `|<>|` (Not in set) +* `|=|` (In set: the purist way; LINQ's `xs.Contains x` is the pragmatist way, gives the same SQL and also runs in the mock context) +* `|<>|` (Not in set; with LINQ, `not (xs.Contains x)`) * `=%` (Like) * `<>%` (Not like) * `!!` (Left join: the joined entity is populated with default values when there is no match) diff --git a/docs/content/core/unittest.fsx b/docs/content/core/unittest.fsx index 95f7271a..c2c1a553 100644 --- a/docs/content/core/unittest.fsx +++ b/docs/content/core/unittest.fsx @@ -151,6 +151,28 @@ There are some limitations with the SQLProvider mock a DB-context: - You can call database-table `.Create` methods to create new instances (it doesn't connect the database). You can call update entity columns `x.Col <- Some "hi"`, but it doesn't really do anything. - You cannot call stored procedures. - Names are database names, and they are case-sensitive. If you miss a table, in your mock, there will be a clear error. If you mistyped the anonymous record column name, you will probably just get a zero-result or ValueNone.Value-error or some other unwanted behaviour. +- Every table the code under test reads must be in the map, with at least one row: the mock learns a table's columns from its rows. Give a table that should match nothing a row that belongs to something else. + +Test the paths through your parametrised queries this way: one mock, and a call per value of each flag +(`all || x.Id = one`, `not openOnly || ...`), checking which rows come back. + +### Where the mock lives + +Keep the mock in the test project: the production code holds no mock database. Writes need nothing special, since +each one makes a new context inside its own transaction. For reads, give the code the mock in one of two ways: + +- **Pass the context in.** Read functions take the read-only context as a parameter; production passes the shared + one and the test passes the mock. +- **Swap the shared holder.** When read functions take the shared read-only context from a module-level holder (see + [CRUD](crud.html) on sharing it and rebuilding it after a connection failure), put an empty marker interface in + that module (`type ContextHolderMarker = interface end`). The test finds the module through + `typeof.DeclaringType`, replaces the holder with a mock by reflection, and puts the original + back when it finishes. + +A test project that names the provided context type directly (`sql.readDataContext`) can get type mismatch errors +such as FS0193, because a provided type named from another assembly is a different type. Name it through a type +abbreviation in the assembly that declares the provider (`type ReadDataContext = sql.readDataContext`) and use that +in the test. If you are running an off-line solution like SSDT or ContextSchemaPath, you should be able to run also these unit-tests with your CI. diff --git a/docs/content/index.fsx b/docs/content/index.fsx index efb67ab6..5b7f38c9 100644 --- a/docs/content/index.fsx +++ b/docs/content/index.fsx @@ -125,9 +125,10 @@ let mattisOrderDetails = // nullable columns can be represented as option types; the following generates IS NOT NULL where o.ShipCountry.IsSome // standard operators will work as expected; the following shows the like operator and IN operator + // ...or you can use "open System.Linq" and do .Contains if that feels more intuitive where (c.ContactName =% ("Matti%") && c.CompanyName |=| [|"Squirrelcomapny";"DaveCompant"|] ) sortBy o.ShipName - // arbitrarily complex projections are supported + // select only what you use: arbitrarily complex projections are supported select (c.ContactName,o.ShipAddress,o.ShipCountry,prod.ProductName,prod.UnitPrice) } |> Seq.toArray diff --git a/src/SQLProvider.Common/SqlRuntime.Common.fs b/src/SQLProvider.Common/SqlRuntime.Common.fs index 03afd620..cb0a82a6 100644 --- a/src/SQLProvider.Common/SqlRuntime.Common.fs +++ b/src/SQLProvider.Common/SqlRuntime.Common.fs @@ -1015,6 +1015,15 @@ module CommonTasks = // manually and in that case scope_identity would bring back 0 "" or whatever | false, _ -> () + /// The select-list entry of a group-key column. In a key of several columns, a computed one (a CASE for x.IsSome or + /// x.IsNone) is aliased key: left unaliased, two of them come back under one result name (PostgreSQL and + /// SQL Server do not name them by their text), and the key tuple cannot be rebuilt. Every other key column keeps + /// `plain`, the provider's usual entry. + let groupKeyColumn (keys:(alias*SqlColumnType) list) (index:int) (column:SqlColumnType) (notation:string) (plain:string) = + match column with + | CanonicalOperation _ when keys.Length > 1 -> sprintf "%s as key%i" notation index + | _ -> plain + let parseHaving fieldNotation (keys:(alias*SqlColumnType) list) (conditionList : Condition list) = if keys.Length <> 1 then failwithf "Unsupported having. Expected 1 key column, found: %i" keys.Length diff --git a/src/SQLProvider.Common/SqlRuntime.Linq.fs b/src/SQLProvider.Common/SqlRuntime.Linq.fs index 1d738bb9..13b64577 100644 --- a/src/SQLProvider.Common/SqlRuntime.Linq.fs +++ b/src/SQLProvider.Common/SqlRuntime.Linq.fs @@ -44,7 +44,34 @@ module internal QueryImplementation = abstract GetAsyncEnumerator : unit -> System.Threading.Tasks.Task> type IAsyncEnumerable = abstract EvaluateQuery : unit -> System.Threading.Tasks.Task - + + let private memberwiseClone = typeof.GetMethod("MemberwiseClone", BindingFlags.Instance ||| BindingFlags.NonPublic) + + /// Copy of a command parameter under a new name. The source parameter is not modified. + let private copyParameterWithName (name:string) (p:IDbDataParameter) : IDbDataParameter = + let copy = + match p with + | :? ICloneable as c -> c.Clone() :?> IDbDataParameter + | _ -> + // Field copy, not property copy: getters such as Size report defaults for unset values, + // and setting them explicitly changes the binding (e.g. Size = 0 truncates a SqliteParameter value). + memberwiseClone.Invoke(p, [||]) :?> IDbDataParameter + copy.ParameterName <- name + copy + + /// Renames the parameters of a sub-query so they cannot collide with the parameters of the enclosing query. + /// Parameter objects stored in the sub-query (including those of its own nested sub-queries) are shared by + /// every query that embeds it, so renamed copies are returned instead of mutating them. + let internal renameNestedParameters (nestedPrefix:string) (query:string) (parameters:IDbDataParameter seq) = + let renamed = + parameters + |> Seq.map(fun p -> copyParameterWithName (p.ParameterName.Replace("@param", nestedPrefix)) p) + |> Seq.toArray + let subquery = + let paramfixed = query.Replace("@param", nestedPrefix) + if paramfixed.EndsWith ";" then paramfixed.Substring(0, paramfixed.Length-1) else paramfixed + subquery, renamed + /// Try to detect if IQueryable is a database query. /// It can be e.g. direct SQLProvider IWithSqlService (great) /// or annoyingly a source inside a wrapper like WhereSelectEnumerableIterator, where it needs to be updated @@ -649,17 +676,8 @@ module internal QueryImplementation = let ``nested param names`` = $"@param{abs(query.GetHashCode())}{nestCount}nested" nestCount <- nestCount + 1 - let modified = - parameters - |> Seq.filter(fun p -> not(p.ParameterName.StartsWith ``nested param names``)) - |> Seq.map(fun p -> - p.ParameterName <- p.ParameterName.Replace("@param", ``nested param names``) - p - ) |> Seq.toArray - let subquery = - let paramfixed = query.Replace("@param", ``nested param names``) - if paramfixed.EndsWith ";" then paramfixed.Substring(0, paramfixed.Length-1) else paramfixed - + let subquery, modified = renameNestedParameters ``nested param names`` query parameters + Some(ti,key,op,Some (box (subquery, modified))) | SqlExistsClause(meth,op,src,qual) | SqlNotExistsClause(meth,op,src,qual) -> @@ -731,17 +749,8 @@ module internal QueryImplementation = let ``nested param names`` = $"@param{abs(query.GetHashCode())}{nestCount}nested" nestCount <- nestCount + 1 - let modified = - parameters - |> Seq.filter(fun p -> not(p.ParameterName.StartsWith ``nested param names``)) - |> Seq.map(fun p -> - p.ParameterName <- p.ParameterName.Replace("@param", ``nested param names``) - p - ) |> Seq.toArray - let subquery = - let paramfixed = query.Replace("@param", ``nested param names``) - if paramfixed.EndsWith ";" then paramfixed.Substring(0, paramfixed.Length-1) else paramfixed - + let subquery, modified = renameNestedParameters ``nested param names`` query parameters + Some("",KeyColumn(""),op,Some (box (subquery, modified))) | SimpleCondition ((ti,key,op,c) as x) -> match c with @@ -847,10 +856,25 @@ module internal QueryImplementation = | "", _ -> "" | _ -> resolveTuplePropertyName ti source.TupleIndex - let keycols = + let keycols = match exp with | SqlColumnGet(sourceTi,sourceKey,_) -> [getAlias sourceTi, sourceKey] - | TupleSqlColumnsGet itms -> itms |> List.map(fun (ti,key,typ) -> getAlias ti, key) + | OptionalFSharpOptionValue(NewExpr(cons, args)) + when (cons.DeclaringType.Name.StartsWith("Tuple") || cons.DeclaringType.Name.StartsWith("AnonymousObject")) -> + // Every tuple item becomes one key column, in order: the key tuple is rebuilt from them. + let keyItem = function + | SqlColumnGet(ti,key,_) -> Some (getAlias ti, key) + | SimpleCondition (ti,key,op,None) -> + let al = getAlias ti + let cond = And ([al, key, op, None], None) + Some (al, CanonicalOperation(CaseSqlPlain(cond, 1, 0), KeyColumn "Key")) + | _ -> None + let items = args |> List.map(fun a -> a, keyItem a) + if items |> List.forall (snd >> Option.isNone) then [] + else + items |> List.map(function + | _, Some col -> col + | item, None -> raise (NotSupportedException $"groupBy: the key item '{item}' is not supported. A group key item must be a column or a column test without a value such as x.IsSome or x.IsNone.")) | SimpleCondition ((ti,key,op,None) as x) -> // Because you can do in F# but not in SQL: GroupBy (x.IsSome) let al = getAlias sourceTi let cond = And ([ti, key, op,None], None) @@ -1397,16 +1421,7 @@ module internal QueryImplementation = let ``nested param names`` = $"@param{abs(query.GetHashCode())}nested" - let modified = - parameters - |> Seq.filter(fun p -> not(p.ParameterName.StartsWith ``nested param names``)) - |> Seq.map(fun p -> - p.ParameterName <- p.ParameterName.Replace("@param", ``nested param names``) - p - ) |> Seq.toArray - let subquery = - let paramfixed = query.Replace("@param", ``nested param names``) - if paramfixed.EndsWith ";" then paramfixed.Substring(0, paramfixed.Length-1) else paramfixed + let subquery, modified = renameNestedParameters ``nested param names`` query parameters //let ty = typedefof>.MakeGenericType(meth.GetGenericArguments().[0]) let utyp = diff --git a/src/SQLProvider.Common/Utils.fs b/src/SQLProvider.Common/Utils.fs index 0aab91c6..e472b0eb 100644 --- a/src/SQLProvider.Common/Utils.fs +++ b/src/SQLProvider.Common/Utils.fs @@ -88,10 +88,14 @@ module Utilities = let getType typename = Type.GetType typename + /// Some/ValueSome case of the given option or voption type holding the value. + let private makeSomeOf (optionType:Type) (value:obj) = + Microsoft.FSharp.Reflection.FSharpValue.MakeUnion(Microsoft.FSharp.Reflection.FSharpType.GetUnionCases(optionType).[1], [|value|]) + let rec internal convertTypes (itm:obj) (returnType:Type) = if not(isNull itm) && Type.(=) (itm.GetType(), returnType) then itm elif isCOpt returnType && returnType.GenericTypeArguments.Length = 1 then - if isNull itm then None |> box + if isNull itm || itm :? DBNull then None |> box else match convertTypes itm (returnType.GenericTypeArguments.[0]) with | :? String as t -> Option.Some t |> box @@ -115,9 +119,11 @@ module Utilities = | :? Guid as t -> Option.Some t |> box | t -> if isCOpt (t.GetType()) then t |> box + elif returnType.GenericTypeArguments.[0].IsInstanceOfType t then makeSomeOf returnType t else Option.Some t |> box elif isVOpt returnType && returnType.GenericTypeArguments.Length = 1 then - if isNull itm then ValueNone |> box + // ValueNone of the requested type: a generic ValueNone would box as ValueOption + if isNull itm || itm :? DBNull then Activator.CreateInstance returnType else match convertTypes itm (returnType.GenericTypeArguments.[0]) with | :? String as t -> ValueOption.Some t |> box @@ -141,6 +147,7 @@ module Utilities = | :? Guid as t -> ValueOption.Some t |> box | t -> if isVOpt (t.GetType()) then t|> box + elif returnType.GenericTypeArguments.[0].IsInstanceOfType t then makeSomeOf returnType t else ValueOption.Some t |> box elif returnType.Name.StartsWith("Nullable") && returnType.GenericTypeArguments.Length = 1 then diff --git a/src/SQLProvider.Runtime/Providers.DuckDb.fs b/src/SQLProvider.Runtime/Providers.DuckDb.fs index 9b4b57cf..aeb3948f 100644 --- a/src/SQLProvider.Runtime/Providers.DuckDb.fs +++ b/src/SQLProvider.Runtime/Providers.DuckDb.fs @@ -916,12 +916,13 @@ type internal DuckDbProvider(resolutionPath, contextSchemaPath, owner:string, re match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation DuckDb.fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as '%s{fn}'") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as '%s{fn}'" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation DuckDb.fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.Firebird.fs b/src/SQLProvider.Runtime/Providers.Firebird.fs index 448ede79..ec4df856 100644 --- a/src/SQLProvider.Runtime/Providers.Firebird.fs +++ b/src/SQLProvider.Runtime/Providers.Firebird.fs @@ -1010,12 +1010,13 @@ type internal FirebirdProvider(resolutionPath, contextSchemaPath, owner, referen match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation Firebird.fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as [%s{fn}]") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as [%s{fn}]" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation Firebird.fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.MSAccess.fs b/src/SQLProvider.Runtime/Providers.MSAccess.fs index 690e4f7c..80f994bd 100644 --- a/src/SQLProvider.Runtime/Providers.MSAccess.fs +++ b/src/SQLProvider.Runtime/Providers.MSAccess.fs @@ -549,12 +549,13 @@ type internal MSAccessProvider(contextSchemaPath) = match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as [%s{fn}]") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as [%s{fn}]" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.MsSqlServer.Dynamic.fs b/src/SQLProvider.Runtime/Providers.MsSqlServer.Dynamic.fs index 60555afe..1e012c9b 100644 --- a/src/SQLProvider.Runtime/Providers.MsSqlServer.Dynamic.fs +++ b/src/SQLProvider.Runtime/Providers.MsSqlServer.Dynamic.fs @@ -1045,12 +1045,13 @@ type internal MSSqlServerDynamicProvider(resolutionPath, contextSchemaPath, refe match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as '%s{fn}'") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as '%s{fn}'" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.MsSqlServer.Ssdt.fs b/src/SQLProvider.Runtime/Providers.MsSqlServer.Ssdt.fs index 471acad2..50a8f09e 100644 --- a/src/SQLProvider.Runtime/Providers.MsSqlServer.Ssdt.fs +++ b/src/SQLProvider.Runtime/Providers.MsSqlServer.Ssdt.fs @@ -704,12 +704,13 @@ type internal MSSqlServerProviderSsdt(tableNames: string, ssdtPath: string) = match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation MSSqlServer.fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as '%s{fn}'") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as '%s{fn}'" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation MSSqlServer.fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.MsSqlServer.fs b/src/SQLProvider.Runtime/Providers.MsSqlServer.fs index 12a6e6d3..8025740d 100644 --- a/src/SQLProvider.Runtime/Providers.MsSqlServer.fs +++ b/src/SQLProvider.Runtime/Providers.MsSqlServer.fs @@ -965,12 +965,13 @@ type internal MSSqlServerProvider(contextSchemaPath, tableNames:string) = match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation MSSqlServer.fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as '%s{fn}'") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as '%s{fn}'" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation MSSqlServer.fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.MySql.fs b/src/SQLProvider.Runtime/Providers.MySql.fs index fc71a679..f25747b8 100644 --- a/src/SQLProvider.Runtime/Providers.MySql.fs +++ b/src/SQLProvider.Runtime/Providers.MySql.fs @@ -989,12 +989,13 @@ type internal MySqlProvider(resolutionPath, contextSchemaPath, owner:string, ref match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation MySql.fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as '%s{fn}'") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as '%s{fn}'" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation MySql.fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.Odbc.fs b/src/SQLProvider.Runtime/Providers.Odbc.fs index 5fe5d834..c3579eca 100644 --- a/src/SQLProvider.Runtime/Providers.Odbc.fs +++ b/src/SQLProvider.Runtime/Providers.Odbc.fs @@ -588,12 +588,13 @@ type internal OdbcProvider(contextSchemaPath, quotechar : OdbcQuoteCharacter) = match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as '%s{fn}'") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as '%s{fn}'" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.Oracle.fs b/src/SQLProvider.Runtime/Providers.Oracle.fs index d9bd823d..d294e4ca 100644 --- a/src/SQLProvider.Runtime/Providers.Oracle.fs +++ b/src/SQLProvider.Runtime/Providers.Oracle.fs @@ -1037,12 +1037,13 @@ type internal OracleProvider(resolutionPath, contextSchemaPath, owner, reference match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation Oracle.fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as \"%s{fn}\"") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as \"%s{fn}\"" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation Oracle.fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.Postgresql.fs b/src/SQLProvider.Runtime/Providers.Postgresql.fs index d0eeeddb..72820e1d 100644 --- a/src/SQLProvider.Runtime/Providers.Postgresql.fs +++ b/src/SQLProvider.Runtime/Providers.Postgresql.fs @@ -801,6 +801,7 @@ type internal PostgresqlProvider(resolutionPath, contextSchemaPath, owner, refer ,coalesce(pg_class.relkind = 'S', false) AS is_sequence ,pg_attribute.atthasdef AS has_default ,coalesce(pg_attribute.attgenerated = 's', false) AS is_generated + ,coalesce(pg_attribute.attidentity <> '', false) AS is_identity FROM pg_attribute LEFT JOIN pg_index ON pg_attribute.attrelid = pg_index.indrelid @@ -872,7 +873,8 @@ type internal PostgresqlProvider(resolutionPath, contextSchemaPath, owner, refer TypeMapping = m IsNullable = Sql.dbUnbox reader.["is_nullable"] IsPrimaryKey = isPk - IsAutonumber = isPk + // GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY columns are assigned by the database + IsAutonumber = isPk || Sql.dbUnbox reader.["is_identity"] IsComputed = Sql.dbUnbox reader.["is_generated"] HasDefault = Sql.dbUnbox reader.["has_default"] TypeInfo = ValueSome fullTypeName @@ -1209,12 +1211,13 @@ type internal PostgresqlProvider(resolutionPath, contextSchemaPath, owner, refer match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation PostgreSQL.fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else sprintf "%s as \"%s\"" fn (fn.Replace("\"", ""))) + let plain = if sqlQuery.Aliases.Count < 2 then fn else sprintf "%s as \"%s\"" fn (fn.Replace("\"", "")) + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation PostgreSQL.fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/src/SQLProvider.Runtime/Providers.SQLite.fs b/src/SQLProvider.Runtime/Providers.SQLite.fs index 0f1e9040..3fa8f298 100644 --- a/src/SQLProvider.Runtime/Providers.SQLite.fs +++ b/src/SQLProvider.Runtime/Providers.SQLite.fs @@ -898,12 +898,13 @@ type internal SQLiteProvider(resolutionPath, contextSchemaPath, referencedAssemb match sqlQuery.Grouping with | [] -> Utilities.parseAggregates fieldNotation fieldNotationAlias sqlQuery.AggregateOp | g -> - let keys = g |> List.collect fst |> List.map(fun (a,c) -> + let keyColumns = g |> List.collect fst + let keys = keyColumns |> List.mapi(fun i (a,c) -> let fn = fieldNotation a c if not (tmpGrpParams.ContainsKey (a,c)) then tmpGrpParams.Add((a,c), fn) - if sqlQuery.Aliases.Count < 2 then fn - else $"%s{fn} as '%s{fn}'") + let plain = if sqlQuery.Aliases.Count < 2 then fn else $"%s{fn} as '%s{fn}'" + CommonTasks.groupKeyColumn keyColumns i c fn plain) let aggs = g |> List.collect snd let res2 = Utilities.parseAggregates fieldNotation fieldNotationAlias aggs |> List.toSeq [String.Join(", ", keys) + (if List.isEmpty aggs || List.isEmpty keys then "" else ", ") + String.Join(", ", res2)] diff --git a/tests/SqlProvider.Tests/CrudTests.fs b/tests/SqlProvider.Tests/CrudTests.fs index 3f4f3d13..e228aab8 100644 --- a/tests/SqlProvider.Tests/CrudTests.fs +++ b/tests/SqlProvider.Tests/CrudTests.fs @@ -213,7 +213,7 @@ let ``Conflict resolution is correctly applied``() = module UtilsTests = - [] + [] let ``List.evaluateOneByOne test``() = // This is a helper for executing Tasks as one-by-one to not mess data connections or contexts. // If you use Aync, use rateher Async.Sequential @@ -228,7 +228,7 @@ module UtilsTests = Assert.AreEqual(initList, processList.Result) } :> Task - [] + [] let ``List.evaluateOneByOne test, no stackoverflow``() = task { let initList = [1 .. 5000] diff --git a/tests/SqlProvider.Tests/PostgreSqlSchemaTests.fs b/tests/SqlProvider.Tests/PostgreSqlSchemaTests.fs new file mode 100644 index 00000000..38ee4ff4 --- /dev/null +++ b/tests/SqlProvider.Tests/PostgreSqlSchemaTests.fs @@ -0,0 +1,63 @@ +module PostgreSqlSchemaTests + +// Schema-reading tests against a live PostgreSQL (10 or newer). +// Set SQLPROVIDER_POSTGRES_TEST_CONNECTION to an Npgsql connection string of a scratch database +// where the tests may create and drop schemas; without it the tests are ignored. + +open System +open System.Data +open FSharp.Data.Sql +open FSharp.Data.Sql.Common +open FSharp.Data.Sql.Schema +open NUnit.Framework + +let private connectionString () = +#if NETFRAMEWORK + // Npgsql through the dynamic provider does not complete in the net48 test host + Assert.Ignore "PostgreSQL schema tests run on .NET only" +#endif + match Environment.GetEnvironmentVariable "SQLPROVIDER_POSTGRES_TEST_CONNECTION" with + | null | "" -> Assert.Ignore "SQLPROVIDER_POSTGRES_TEST_CONNECTION is not set"; "" + | cs -> cs + +let private createProvider () = + let providerType = typeof.Assembly.GetType("FSharp.Data.Sql.Providers.PostgresqlProvider", true) + let resolutionPath = AppContext.BaseDirectory + Activator.CreateInstance(providerType, Reflection.BindingFlags.Instance ||| Reflection.BindingFlags.Public ||| Reflection.BindingFlags.NonPublic, null, + [| box resolutionPath; box ""; box "public"; box Array.empty |], null) :?> ISqlProvider + +// the provider's schema reads close the connection when they finish +let private execute (con:IDbConnection) (sql:string) = + if con.State <> ConnectionState.Open then con.Open() + use cmd = con.CreateCommand() + cmd.CommandText <- sql + cmd.ExecuteNonQuery() |> ignore + +[] +let ``identity columns are autonumber and left out of the required columns``() = + let cs = connectionString () + let provider = createProvider () + let schema = "sqlprovider_identity_" + Guid.NewGuid().ToString("N").Substring(0, 8) + use con = provider.CreateConnection cs + con.Open() + provider.CreateTypeMappings con + try + execute con $"CREATE SCHEMA {schema}" + execute con $"""CREATE TABLE {schema}.enrolment ( + id uuid PRIMARY KEY, + number bigint GENERATED ALWAYS AS IDENTITY (START WITH 1000), + alt_number integer GENERATED BY DEFAULT AS IDENTITY, + member_sub text NOT NULL, + note text NULL)""" + let columns = provider.GetColumns(con, { Schema = schema; Name = "enrolment"; Type = "BASE TABLE" }) + Assert.IsTrue(columns.["number"].IsAutonumber, "GENERATED ALWAYS AS IDENTITY") + Assert.IsTrue(columns.["alt_number"].IsAutonumber, "GENERATED BY DEFAULT AS IDENTITY") + Assert.IsFalse(columns.["member_sub"].IsAutonumber, "plain column") + // the typed Create(...) takes the columns that are not nullable, autonumber or computed + let required = + columns |> Map.toList |> List.map snd + |> List.filter (fun c -> not (c.IsNullable || c.IsAutonumber || c.IsComputed)) + |> List.map (fun c -> c.Name) + Assert.AreEqual(["member_sub"], required) + finally + execute con $"DROP SCHEMA IF EXISTS {schema} CASCADE" diff --git a/tests/SqlProvider.Tests/QueryTests.fs b/tests/SqlProvider.Tests/QueryTests.fs index 75f6896d..3a592482 100644 --- a/tests/SqlProvider.Tests/QueryTests.fs +++ b/tests/SqlProvider.Tests/QueryTests.fs @@ -60,7 +60,7 @@ let ``simple select with contains query``() = } Assert.IsTrue(qry) -[] +[] let ``simple select with tuple by the new projections``() = let dc = sql.GetDataContext() let qry = @@ -72,7 +72,7 @@ let ``simple select with tuple by the new projections``() = let firstId = qry |> List.head |> fst Assert.AreEqual("ALFKI", firstId) -[] +[] let ``simple select with anon-record by the new projections``() = let dc = sql.GetDataContext() let qry = @@ -84,7 +84,7 @@ let ``simple select with anon-record by the new projections``() = let firstItm = qry |> List.head Assert.AreEqual("ALFKI", firstItm.A) -[] +[] let ``simple select with contains query with where``() = let dc = sql.GetDataContext() let qry = @@ -107,7 +107,7 @@ let ``simple select with contains query when not exists``() = } Assert.IsFalse(qry) -[] +[] let ``simple select with count``() = let dc = sql.GetDataContext() let qry = @@ -118,7 +118,7 @@ let ``simple select with count``() = } Assert.AreEqual(91, qry) -[] // Generates COUNT(DISTINCT CustomerId) +[] // Generates COUNT(DISTINCT CustomerId) let ``simple select with distinct count``() = let dc = sql.GetDataContext() let qry = @@ -141,7 +141,7 @@ let ``simple select with distinct avg``() = } Assert.AreEqual(91, qry) -[] +[] let ``simple select with read only context``() = let dc2 = sql.GetReadOnlyDataContext() @@ -162,7 +162,7 @@ let ``simple select with read only context``() = | _ -> Assert.True true -[] +[] let ``simple select with read only context should contain the same types for easier code share``() = let getCust customers = let res = @@ -241,7 +241,7 @@ let ``simple select with last or default when exists``() = } Assert.AreEqual("WOLZA", qry) -[] +[] let ``simple select with exactly one``() = let dc = sql.GetDataContext() let qry = @@ -253,7 +253,7 @@ let ``simple select with exactly one``() = } Assert.AreEqual("ALFKI", qry) -[] +[] let ``option from join select with exactly one``() = let dc = sql.GetDataContext() let qry = @@ -274,7 +274,7 @@ let ``option from join select with exactly one``() = | None -> Assert.Fail() -[] +[] let ``over 8 joins test``() = let dc = sql.GetDataContext() let qry = @@ -307,7 +307,7 @@ let ``over 8 joins test``() = | None -> Assert.Fail() -[] +[] let ``outer join with inner join test``() = let dc = sql.GetDataContext() let qry = @@ -327,7 +327,7 @@ let ``outer join with inner join test``() = Assert.AreNotEqual(None,qry) -[] +[] let ``leftOuterJoin yields null entity for unmatched row``() = let dc = sql.GetDataContext() // When the whole joined entity is selected, it must be null for customers with no orders. @@ -346,7 +346,7 @@ let ``leftOuterJoin yields null entity for unmatched row``() = Assert.AreEqual(6, alfki.Length) Assert.IsTrue(alfki |> Array.forall (fun (_,o) -> not (obj.ReferenceEquals(o, null)))) -[] +[] let ``leftOuterJoin chained over three tables``() = let dc = sql.GetDataContext() // Two chained left joins: Customers -> Orders -> OrderDetails. Equivalent to a SQL @@ -366,7 +366,7 @@ let ``leftOuterJoin chained over three tables``() = // ALFKI's 6 orders have 12 order-detail lines in total. Assert.AreEqual(12, res |> Array.filter (fun (c,_,_) -> c = "ALFKI") |> Array.length) -[] +[] let ``leftOuterJoin after an inner join``() = let dc = sql.GetDataContext() let res = @@ -382,7 +382,7 @@ let ``leftOuterJoin after an inner join``() = Assert.AreEqual(823, res.Length) Assert.AreEqual(72, res |> Array.filter (fun (_,_,e) -> obj.ReferenceEquals(e, null)) |> Array.length) -[] +[] let ``leftOuterJoin with groupBy over the outer table``() = let dc = sql.GetDataContext() let res = @@ -400,7 +400,7 @@ let ``leftOuterJoin with groupBy over the outer table``() = // so a customer with no orders has count 1, not 0. Assert.AreEqual(1, res.["FISSA"]) -[] +[] let ``groupJoin flattened without DefaultIfEmpty behaves as an inner join``() = let dc = sql.GetDataContext() let res = @@ -415,7 +415,7 @@ let ``groupJoin flattened without DefaultIfEmpty behaves as an inner join``() = Assert.AreEqual(6, res.Length) Assert.IsTrue(res |> Array.forall (fun (c,_) -> c = "ALFKI")) -[] +[] let ``groupJoin with a group aggregate fails loudly``() = let dc = sql.GetDataContext() // Aggregating the join group can't be modelled as a flat join; it must fail with a clear @@ -429,7 +429,7 @@ let ``groupJoin with a group aggregate fails loudly``() = } |> Seq.toArray |> ignore) StringAssert.Contains("groupJoin", ex.Message) -[] +[] let ``simple select two queries test``() = let dc = sql.GetDataContext SelectOperations.DatabaseSide // Works also with: let dc = sql.GetDataContext(SelectOperations.DotNetSide) @@ -449,7 +449,7 @@ let ``simple select two queries test``() = Assert.AreEqual(itm1.CustomerId, itm2.CustomerId) Assert.IsTrue isOk -[] +[] let ``option from simple select with exactly one``() = let dc = sql.GetDataContext() let qry = @@ -473,7 +473,7 @@ let ``option from simple select with exactly one``() = Assert.Fail() -[] +[] let ``simple select with exactly one when not exists``() = let dc = sql.GetDataContext() let qry = @@ -487,7 +487,7 @@ let ``simple select with exactly one when not exists``() = Assert.AreEqual(null, qry) -[] +[] let ``simple select with head``() = let dc = sql.GetDataContext() let qry = @@ -499,7 +499,7 @@ let ``simple select with head``() = } Assert.AreEqual("ALFKI", qry) -[] +[] let ``simple select with head or Default``() = let dc = sql.GetDataContext() let qry = @@ -511,7 +511,7 @@ let ``simple select with head or Default``() = } Assert.AreEqual("ALFKI", qry) -[] +[] let ``simple select with head or Default when not exists``() = let dc = sql.GetDataContext() let qry = @@ -560,7 +560,7 @@ let ``simplest select query into temp``() = CollectionAssert.IsNotEmpty qry Assert.AreEqual("Germany", fst qry.[0]) -[] +[] let ``simplest select into where``() = let dc = sql.GetDataContext() let qry = @@ -618,7 +618,7 @@ let ``simple select query let temp``() = //Assert.AreEqual("Berlintest", qry.[0]) -[] +[] let ``simple select query let where``() = let dc = sql.GetDataContext() let qry = @@ -632,7 +632,7 @@ let ``simple select query let where``() = CollectionAssert.IsNotEmpty qry CollectionAssert.Contains(qry, "Berlintest") -[] +[] let ``simple select query let temp used in where``() = let dc = sql.GetDataContext() let qry = @@ -671,7 +671,7 @@ let ``simple select query with operations in select``() = CollectionAssert.IsNotEmpty qry CollectionAssert.Contains(qry, "Germany Berliner Platz 431") -[] +[] let ``simple select where query``() = let dc = sql.GetDataContext() let qry = @@ -686,7 +686,7 @@ let ``simple select where query``() = Assert.AreEqual("Berlin", qry.[0].City) -[] +[] let ``simple select where null query``() = let dc = sql.GetDataContext() let qry = @@ -701,7 +701,7 @@ let ``simple select where null query``() = Assert.AreEqual("Berlin", qry.[0].City) -[] +[] let ``simple select where query right side``() = let dc = sql.GetDataContext() let qry = @@ -715,7 +715,7 @@ let ``simple select where query right side``() = Assert.AreEqual(1, qry.Length) Assert.AreEqual("Berlin", qry.[0].City) -[] +[] let ``simple select where query between col properties``() = let dc = sql.GetDataContext() let qry = @@ -740,7 +740,7 @@ let ``simple nth query``() = Assert.AreEqual("London", qry.City) -[] +[] let ``simple select where not query``() = let dc = sql.GetDataContext() let qry = @@ -753,7 +753,7 @@ let ``simple select where not query``() = CollectionAssert.IsNotEmpty qry Assert.AreEqual(90, qry.Length) -[] +[] let ``simple select where in query``() = let dc = sql.GetDataContext() let arr = ["ALFKI"; "ANATR"; "AROUT"] @@ -769,7 +769,7 @@ let ``simple select where in query``() = Assert.AreEqual(3, qry.Length) Assert.IsTrue(qry.Contains "ANATR") -[] +[] let ``simple select where in set query``() = let dc = sql.GetDataContext() let itmSet = ["ALFKI"; "ANATR"; "AROUT"] |> Set.ofList @@ -786,7 +786,7 @@ let ``simple select where in set query``() = Assert.IsTrue(qry.Contains "ANATR") -[] +[] let ``simple select where not-in query``() = let dc = sql.GetDataContext() let arr = ["ALFKI"; "ANATR"; "AROUT"] @@ -802,7 +802,7 @@ let ``simple select where not-in query``() = Assert.AreEqual(88, qry.Length) Assert.IsFalse(qry.Contains "ANATR") -[] +[] let ``simple select where in queryable query``() = let dc = sql.GetDataContext() @@ -825,7 +825,7 @@ let ``simple select where in queryable query``() = Assert.AreEqual(6, query2.Length) Assert.IsTrue(query2.Contains "EASTC") -[] +[] let ``simple select where inner-join box-check and not in queryable query``() = let dc = sql.GetDataContext() @@ -850,7 +850,7 @@ let ``simple select where inner-join box-check and not in queryable query``() = Assert.AreEqual(3, query2.Length) CollectionAssert.Contains(query2, "FISSA") -[] +[] let ``simple select where in query custom syntax``() = let dc = sql.GetDataContext() let arr = ["ALFKI"; "ANATR"; "AROUT"] @@ -865,7 +865,7 @@ let ``simple select where in query custom syntax``() = Assert.AreEqual(3, qry.Length) Assert.IsTrue(qry.Contains "ANATR") -[] +[] let ``simple select where like query``() = let dc = sql.GetDataContext() let qry = @@ -878,7 +878,7 @@ let ``simple select where like query``() = CollectionAssert.IsNotEmpty qry CollectionAssert.Contains(qry, "FRANR") -[] +[] let ``simple select where like query2``() = let dc = sql.GetDataContext() @@ -901,7 +901,7 @@ let ``simple select where like query2``() = CollectionAssert.Contains(qry, "ALFKI") -[] +[] let ``simple select where query with operations in where``() = let dc = sql.GetDataContext() let qry = @@ -915,7 +915,7 @@ let ``simple select where query with operations in where``() = Assert.AreEqual(1, qry.Length) Assert.AreEqual("Berlin", qry.[0].City) -[] +[] let ``simple select query with minBy``() = let dc = sql.GetDataContext() let qry = @@ -925,7 +925,7 @@ let ``simple select query with minBy``() = } Assert.AreEqual(0m, qry) -[] +[] let ``simple select query with minBy2``() = let dc = sql.GetDataContext() let qry = @@ -936,7 +936,7 @@ let ``simple select query with minBy2``() = Assert.AreEqual(0., qry) -[] +[] let ``simple select query with minBy DateTime``() = let dc = sql.GetDataContext() let qry = @@ -946,7 +946,7 @@ let ``simple select query with minBy DateTime``() = } Assert.AreEqual(DateTime(1937, 09, 19), qry) -[] +[] let ``simple select query with sumBy``() = let dc = sql.GetDataContext() let qry = @@ -971,7 +971,7 @@ let ``simple select query with sumBy join``() = Assert.Greater(56501m, qry) Assert.Less(56499m, qry) -[] +[] let ``subquery with 1 tables``() = let dc = sql.GetDataContext() @@ -1005,7 +1005,7 @@ let ``subquery with 2 tables``() = Assert.IsNotEmpty(qry2) -[] +[] let ``simple where before join test``() = let dc = sql.GetDataContext() @@ -1022,7 +1022,7 @@ let ``simple where before join test``() = Assert.LessOrEqual((fst qry.[0]), (fst qry.[1000])) CollectionAssert.Contains(qry, (10257L, 10257L)) -[] +[] let ``simple where before join test2``() = let dc = sql.GetDataContext() @@ -1057,7 +1057,7 @@ let ``simple navigation sum async``() = Assert.GreaterOrEqual(res, 0m) } :> Task -[] +[] let ``simple where before join test3``() = let dc = sql.GetDataContext() @@ -1076,7 +1076,7 @@ let ``simple where before join test3``() = Assert.AreEqual(qry.Length, 24) Assert.LessOrEqual(qry.[0].ShipCity, qry.[20].ShipCity) -[] +[] let ``simple select query with sumBy times two``() = let dc = sql.GetDataContext() let qry = @@ -1088,7 +1088,7 @@ let ``simple select query with sumBy times two``() = Assert.Less(3393418m, qry) -[] +[] let ``simple select query with averageBy``() = let dc = sql.GetDataContext() let qry = @@ -1099,7 +1099,7 @@ let ``simple select query with averageBy``() = Assert.Greater(27m, qry) Assert.Less(26m, qry) -[] +[] let ``simple select query with averageBy length``() = let dc = sql.GetDataContext() let qry = @@ -1111,7 +1111,7 @@ let ``simple select query with averageBy length``() = Assert.Less(13m, qry) -[] +[] let ``simplest select query with groupBy``() = let dc = sql.GetDataContext() let qry = @@ -1122,7 +1122,7 @@ let ``simplest select query with groupBy``() = Assert.IsNotEmpty(qry) -[] +[] let ``simplest select query with groupBy 2 columns``() = let dc = sql.GetDataContext() let qry = @@ -1133,7 +1133,7 @@ let ``simplest select query with groupBy 2 columns``() = Assert.IsNotEmpty(qry) -[] +[] let ``simplest select query with groupBy key aggregate``() = let dc = sql.GetDataContext() let qry = @@ -1146,7 +1146,7 @@ let ``simplest select query with groupBy key aggregate``() = Assert.IsNotEmpty(qry) -[] +[] let ``simplest select query with groupBy aggregate``() = let dc = sql.GetDataContext() let qry = @@ -1158,7 +1158,7 @@ let ``simplest select query with groupBy aggregate``() = Assert.IsNotEmpty(qry) -[] +[] let ``simplest select query with groupBy aggregate via select``() = let dc = sql.GetDataContext() let qry = @@ -1170,7 +1170,7 @@ let ``simplest select query with groupBy aggregate via select``() = Assert.IsNotEmpty(qry) -[] +[] let ``simplest select query with groupBy aggregate temp total``() = let dc = sql.GetDataContext() let qry = @@ -1211,7 +1211,7 @@ let ``simplest select query with groupBy constant with operation``() = Assert.AreEqual(qry, 24) -[] +[] let ``simple select query with join groupBy constant``() = let dc = sql.GetDataContext() let qry = @@ -1228,7 +1228,7 @@ let ``simple select query with join groupBy constant``() = Assert.IsNotEmpty(qry) -[] +[] let ``simple select query with groupBy``() = let dc = sql.GetDataContext() let qry = @@ -1241,7 +1241,7 @@ let ``simple select query with groupBy``() = Assert.IsNotEmpty(res) Assert.AreEqual(6, res.["London"]) -[] +[] let ``simple select query with groupBy and then sort``() = let dc = sql.GetDataContext() let qry = @@ -1259,7 +1259,7 @@ let ``simple select query with groupBy and then sort``() = Assert.Less(50, lonAvg) Assert.Greater(100, lonAvg) -[] +[] let ``simple select query with groupBy multiple columns``() = let dc = sql.GetDataContext() let qry = @@ -1271,7 +1271,7 @@ let ``simple select query with groupBy multiple columns``() = let res = qry |> dict Assert.IsNotEmpty(res) -[] +[] let ``simple select query with groupBy sum``() = let dc = sql.GetDataContext() let qry = @@ -1297,7 +1297,7 @@ let ``simple select query with groupBy sum``() = Assert.Less(689m, plusones) Assert.Greater(plustwos, plusones) -[] +[] let ``simple select query with groupBy having count``() = let dc = sql.GetDataContext() let qry = @@ -1312,7 +1312,7 @@ let ``simple select query with groupBy having count``() = Assert.IsNotEmpty(res) Assert.AreEqual(3, res.["Buenos Aires"]) -[] +[] let ``simple select query with groupBy where and having``() = let dc = sql.GetDataContext() let qry = @@ -1330,7 +1330,7 @@ let ``simple select query with groupBy where and having``() = Assert.AreEqual(9L, res.["Seattle"]) -[] +[] let ``simple select query with groupBy having key``() = let dc = sql.GetDataContext() let qry = @@ -1344,7 +1344,7 @@ let ``simple select query with groupBy having key``() = Assert.IsNotEmpty(res) Assert.AreEqual(6, res.["London"]) -[] +[] let ``simple select query with groupBy having nested``() = let dc = sql.GetDataContext() let subQry = @@ -1364,7 +1364,7 @@ let ``simple select query with groupBy having nested``() = Assert.IsNotEmpty(qry) Assert.Contains("AROUT", qry) -[] +[] let ``simple select query with groupBy date``() = let dc = sql.GetDataContext() let qry = @@ -1376,7 +1376,7 @@ let ``simple select query with groupBy date``() = let res = qry |> dict Assert.IsNotEmpty(res) -[] +[] let ``simple select query with groupBy2``() = let dc = sql.GetDataContext() let qry = @@ -1389,7 +1389,7 @@ let ``simple select query with groupBy2``() = Assert.IsNotNull(qry) Assert.AreEqual(7, qry.Sum(fun k -> k.Value)) -[] +[] let ``simple select query with groupBy complex operations``() = let dc = sql.GetDataContext SelectOperations.DatabaseSide let old = DateTime(1990,01,01) @@ -1406,7 +1406,7 @@ let ``simple select query with groupBy complex operations``() = Assert.AreEqual(458.91m, qry.["Belgium", "Bruxelles"]) Assert.AreEqual(2151.67m, qry.["UK", "London"]) -[] +[] let ``simple select query with groupBy join complex operations``() = let dc = sql.GetDataContext() let old = DateTime(1990,01,01) @@ -1422,7 +1422,7 @@ let ``simple select query with groupBy join complex operations``() = } |> Seq.toArray Assert.IsNotNull(qry) -[] +[] let ``simple groupValBy item``() = let dc = sql.GetDataContext() let qry = @@ -1432,7 +1432,7 @@ let ``simple groupValBy item``() = } |> Seq.toList Assert.IsNotEmpty(qry) -[] +[] let ``simple select query with groupValBy``() = let dc = sql.GetDataContext() let qry = @@ -1443,7 +1443,7 @@ let ``simple select query with groupValBy``() = } |> dict Assert.IsNotEmpty(qry) -[] +[] let ``complex select query with groupValBy``() = let dc = sql.GetDataContext() let qry = @@ -1495,7 +1495,7 @@ let ``simple select query with case on client``() = CollectionAssert.Contains(qry, "London") CollectionAssert.Contains(qry, "Outside UK") -[] +[] let ``simple select and sort query``() = let dc = sql.GetDataContext() let qry = @@ -1508,7 +1508,7 @@ let ``simple select and sort query``() = CollectionAssert.IsNotEmpty qry CollectionAssert.AreEquivalent([|"Aachen"; "Albuquerque"; "Anchorage"|], qry.[0..2]) -[] +[] let ``simple select and sort query2``() = let dc = sql.GetDataContext() let sortbyCity=true @@ -1536,7 +1536,7 @@ type SortbyCity = | SortbyCity.B -> "b" | SortbyCity.Asdf -> "asdf" -[] +[] let ``simple select and sort query3``() = let dc = sql.GetDataContext() let sortbyCity=SortbyCity.Asdf @@ -1556,7 +1556,7 @@ let ``simple select and sort query3``() = CollectionAssert.AreEquivalent([|"Aachen"; "Albuquerque"; "Anchorage"|], qry.[0..2]) -[] +[] let ``simple select and sort desc query``() = let dc = sql.GetDataContext() let qry = @@ -1569,7 +1569,7 @@ let ``simple select and sort desc query``() = CollectionAssert.IsNotEmpty qry CollectionAssert.AreEquivalent([|"Ã…rhus"; "Warszawa"; "Walla Walla"|], qry.[0..2]) -[] +[] let ``simple select and sort query with then by query``() = let dc = sql.GetDataContext() let qry = @@ -1583,7 +1583,7 @@ let ``simple select and sort query with then by query``() = CollectionAssert.IsNotEmpty qry CollectionAssert.AreEquivalent([|"Buenos Aires"; "Buenos Aires"; "Buenos Aires"; "Graz"|], qry.[0..3]) -[] +[] let ``simple select and sort query with then by desc query``() = let dc = sql.GetDataContext() let qry = @@ -1597,7 +1597,7 @@ let ``simple select and sort query with then by desc query``() = CollectionAssert.IsNotEmpty qry CollectionAssert.AreEquivalent([|"Buenos Aires"; "Buenos Aires"; "Buenos Aires"; "Salzburg"|], qry.[0..3]) -[] +[] let ``simple sort query with lambda cast to IComparable``() = let dc = sql.GetDataContext() let qry = @@ -1611,7 +1611,7 @@ let ``simple sort query with lambda cast to IComparable``() = CollectionAssert.IsNotEmpty qry CollectionAssert.AreEquivalent([|"Berlin"|], qry) -[] +[] let ``simple date opearations with implicit convert``() = let dc = sql.GetDataContext() let today = DateTime.Today @@ -1623,7 +1623,7 @@ let ``simple date opearations with implicit convert``() = } |> Seq.toList CollectionAssert.IsNotEmpty qry -[] +[] let ``simple sort query with then by desc query with lambda cast to IComparable``() = let dc = sql.GetDataContext() let qry = @@ -1638,7 +1638,7 @@ let ``simple sort query with then by desc query with lambda cast to IComparable` CollectionAssert.IsNotEmpty qry CollectionAssert.AreEquivalent([|"Berlin"|], qry) -[] +[] let ``simple select query with join``() = let dc = sql.GetDataContext() let qry = @@ -1658,7 +1658,7 @@ let ``simple select query with join``() = |], qry.[0..3]) -[] +[] let ``simple select query with join and then groupBy``() = let dc = sql.GetDataContext() let qry = @@ -1673,7 +1673,7 @@ let ``simple select query with join and then groupBy``() = Assert.IsNotEmpty(res) Assert.AreEqual("WX3 6FW", res.["London","London"]) -[] +[] let ``simple select query with join and then groupBy 2``() = let dc = sql.GetDataContext() let qry = @@ -1689,7 +1689,7 @@ let ``simple select query with join and then groupBy 2``() = Assert.IsNotEmpty(res) Assert.AreEqual(32, res.Length) -[] +[] let ``simple select query with joins and then groupBy``() = let dc = sql.GetDataContext() let qry = @@ -1733,7 +1733,7 @@ let ``simple select query with groupBy and then join``() = let res = qry |> Seq.toList Assert.IsNotEmpty(res) -[] +[] let ``simple select query with join multi columns``() = let dc = sql.GetDataContext() let qry = @@ -1772,7 +1772,7 @@ let ``simple select query with join using relationships``() = "VICTE", DateTime(1996,7,8) |], qry.[0..3]) -[] +[] let ``simple select query with group join``() = let dc = sql.GetDataContext() let qry = @@ -1821,7 +1821,7 @@ let ``simple select query with multiple joins on relationships``() = "HANAR", 65L, 15s |], qry.[0..7]) -[] +[] let ``simple select query with left outer join``() = let dc = sql.GetDataContext() let qry = @@ -1843,7 +1843,7 @@ let ``simple select query with left outer join``() = Assert.AreEqual(1, fissa.Length) Assert.AreEqual(Unchecked.defaultof, snd fissa.[0]) -[] +[] let ``simple sumBy``() = let dc = sql.GetDataContext() let qry = @@ -1877,7 +1877,7 @@ let ``simple async sum with operations``() = Assert.That(qry, Is.EqualTo(3454230.7769M).Within(0.1M)) } :> Task -[] +[] let ``simple async sum with join and operations``() = task { let dc = sql.GetDataContext() @@ -1917,7 +1917,7 @@ let ``simple async sum with operations 2``() = Assert.That(qry, Is.EqualTo(31886.0M).Within(1.0M)) } :> Task -[] +[] // Note: // Use float if you try to do any math, don't use decimal. // Decimal is by intention of SQL-drivers handled as text to be non-lossy, @@ -1933,7 +1933,7 @@ let ``simple math op``() = CollectionAssert.IsNotEmpty qry -[] +[] let ``simple averageBy``() = let dc = sql.GetDataContext() let qry = @@ -1953,7 +1953,7 @@ let ``simple averageByNullable``() = } Assert.That(qry, Is.EqualTo(26.2185m).Within(0.001M)) -[] +[] let ``simple select with distinct``() = let dc = sql.GetDataContext() let qry = @@ -1967,7 +1967,7 @@ let ``simple select with distinct``() = Assert.AreEqual(69, qry.Length) CollectionAssert.AreEquivalent([|"Aachen"; "Albuquerque"; "Anchorage"|], qry.[0..2]) -[] +[] let ``simple select with skip``() = let dc = sql.GetDataContext() let qry = @@ -1982,7 +1982,7 @@ let ``simple select with skip``() = Assert.AreEqual(86, qry.Length) CollectionAssert.AreEquivalent([|"Bergamo"; "Berlin"; "Bern"|], qry.[0..2]) -[] +[] let ``simple select with take``() = let dc = sql.GetDataContext() let qry = @@ -2057,7 +2057,7 @@ let ``simple select into a generic type with pipe`` () = CollectionAssert.IsNotEmpty qry -[] +[] let ``simple select with bool outside query``() = let dc = sql.GetDataContext() let rnd = Random() @@ -2079,7 +2079,7 @@ let ``simple select with bool outside query``() = CollectionAssert.IsNotEmpty qry -[] +[] let ``simple select with bool outside query2``() = let dc = sql.GetDataContext() let rnd = Random() @@ -2100,7 +2100,7 @@ let ``simple select with bool outside query2``() = CollectionAssert.IsNotEmpty qry -[] +[] let ``simple select cross-join test``() = //crossjoin: SELECT ... FROM Customers, Employees let dc = sql.GetDataContext() @@ -2115,7 +2115,7 @@ let ``simple select cross-join test``() = CollectionAssert.Contains(qry, ("Maria Anders", "Buchanan")) -[] +[] let ``simple select cross-join test 3 tables``() = let dc = sql.GetDataContext() let qry = @@ -2163,7 +2163,7 @@ let ``simple select nested query``() = Assert.IsNotNull(qry) CollectionAssert.Contains(qry, "Fuller") -[] +[] let ``simple select nested emp query``() = let dc = sql.GetDataContext() let qry = @@ -2179,7 +2179,7 @@ let ``simple select nested emp query``() = Assert.IsNotNull(qry) CollectionAssert.Contains(qry, "Anne") -[] +[] let ``simple select entityValue form another query``() = let dc = sql.GetDataContext() let ent1 = @@ -2198,7 +2198,7 @@ let ``simple select entityValue form another query``() = Assert.IsNotNull(ent2) Assert.AreEqual(ent1.CustomerId, ent2.CustomerId) -[] +[] let ``simple select nested query sort``() = let dc = sql.GetDataContext() let qry = @@ -2213,7 +2213,7 @@ let ``simple select nested query sort``() = } |> Seq.toList Assert.IsNotNull(qry) -[] +[] let ``simple select join twice to same table``() = let dc = sql.GetDataContext() let qry = @@ -2229,7 +2229,7 @@ let ``simple select join twice to same table``() = Assert.AreEqual("Butte",c1) Assert.AreEqual("Kirkland",c2) -[] +[] let ``simple select with multiple table joins with 4 tables``() = let dc = sql.GetDataContext() let results = @@ -2260,7 +2260,7 @@ let ``simple select query async``() = CollectionAssert.IsNotEmpty task.Result } :> Task -[] +[] let ``simple select query async2``() = task { let dc = sql.GetDataContext() @@ -2279,7 +2279,7 @@ let ``simple select query async2``() = CollectionAssert.Contains(r, ("55 Grizzly Peak Rd.", "Butte", "Liu Wong")) } :> Task -[] +[] let ``simple select query async3``() = task { let dc = sql.GetDataContext() @@ -2299,7 +2299,7 @@ let ``simple select query async3``() = () } :> Task -[] +[] let ``simple select query async4``() = task { let dc = sql.GetDataContext() @@ -2320,7 +2320,7 @@ let ``simple select query async4``() = } :> Task -[] +[] let ``simple select query async5``() = task { let dc = sql.GetDataContext() @@ -2348,7 +2348,7 @@ type CustomType = { Country : String; } -[] +[] let ``simple select query async6``() = task { let dc = sql.GetDataContext() @@ -2371,7 +2371,7 @@ let ``simple select query async6``() = () } :> Task -[] +[] let ``simple select query lengthAsync``() = task { let dc = sql.GetDataContext() @@ -2384,7 +2384,7 @@ let ``simple select query lengthAsync``() = Assert.IsTrue(count >= 0) } -[] // Generates COUNT(DISTINCT CustomerId) +[] // Generates COUNT(DISTINCT CustomerId) let ``simple select with distinct count async``() = task { let! _ = async { @@ -2420,7 +2420,7 @@ let ``simple select query with different return types options``() = CollectionAssert.IsNotEmpty qry -[] +[] let ``simple select with contains query with where boolean option type``() = let dc = sqlOption.GetDataContext() let qry = @@ -2432,7 +2432,7 @@ let ``simple select with contains query with where boolean option type``() = } Assert.IsTrue(qry) -[] +[] let ``simple select with contains query with where not boolean option type``() = let dc = sqlOption.GetDataContext() let qry = @@ -2444,7 +2444,7 @@ let ``simple select with contains query with where not boolean option type``() = } Assert.IsTrue(qry) -[] +[] let ``leftOuterJoin with option types yields None for unmatched columns``() = // Issue #540: with UseOptionTypes, the left-joined columns must be None when there is no match. let dc = sqlOption.GetDataContext() @@ -2464,7 +2464,7 @@ let ``leftOuterJoin with option types yields None for unmatched columns``() = Assert.AreEqual(1, fissa.Length) Assert.AreEqual(None, snd fissa.[0]) -[] +[] let ``leftOuterJoin anti-join finds customers without orders``() = // The classic anti-join: LEFT JOIN + IS NULL on the joined side. The where on the // left-joined column must translate to `IS NULL` and keep only the unmatched rows. @@ -2479,7 +2479,7 @@ let ``leftOuterJoin anti-join finds customers without orders``() = } |> Seq.toArray CollectionAssert.AreEquivalent([|"FISSA"; "PARIS"; "WOLZA"|], res) -[] +[] let ``simple select with where boolean option types``() = let dc = sqlOption.GetDataContext() let getOptionFilter (city : string option) = @@ -2497,7 +2497,7 @@ let ``simple select with where boolean option types``() = type MyOpt = {MyItem: string option} -[] +[] let ``simple select with custom option types in where``() = let dc = sqlOption.GetDataContext() let someItem = {MyItem = Some "London"} @@ -2511,7 +2511,7 @@ let ``simple select with custom option types in where``() = } Assert.AreEqual("AROUT",qry) -[] +[] let ``simple async sum with option operations``() = task { let dc = sqlOption.GetDataContext() @@ -2524,7 +2524,7 @@ let ``simple async sum with option operations``() = Assert.That(qry, Is.EqualTo(603221955M).Within(10M)) } :> Task -[] +[] let ``simple select query with left join``() = let dc = sqlOption.GetDataContext() let qry = @@ -2537,7 +2537,7 @@ let ``simple select query with left join``() = CollectionAssert.IsNotEmpty qry Assert.AreEqual(91, qry.Length) -[] +[] let ``simple math operations query``() = let dc = sql.GetDataContext() let qry = @@ -2549,7 +2549,7 @@ let ``simple math operations query``() = Assert.AreEqual(44L, qry.Head) -[] +[] let ``simple math operations query2``() = let dc = sql.GetDataContext() let qry2 = @@ -2560,7 +2560,7 @@ let ``simple math operations query2``() = } |> Seq.toList CollectionAssert.IsEmpty qry2 -[] +[] let ``simple canonical operation substing query``() = let dc = sql.GetDataContext() let qry = @@ -2575,7 +2575,7 @@ let ``simple canonical operation substing query``() = Assert.AreEqual(1, qry.Length) Assert.IsTrue(qry.Contains "ANATR") -[] +[] let ``simple canonical operation inverted operations query``() = let dc = sql.GetDataContext() let qry = @@ -2589,7 +2589,7 @@ let ``simple canonical operation inverted operations query``() = Assert.AreEqual(13, qry.Length) CollectionAssert.Contains(qry, 10514L) -[] +[] let ``simple canonical operations query``() = let dc = sql.GetDataContext() @@ -2618,7 +2618,7 @@ let ``simple canonical operations query``() = Assert.AreEqual(12, qry.Length) Assert.IsTrue(qry.[0] |> fun (id,c,b) -> c="London" && b.Month>2) -[] +[] let ``simple canonical operations case-when-elses``() = let dc = sql.GetDataContext() @@ -2646,7 +2646,7 @@ let ``simple canonical operations case-when-elses``() = CollectionAssert.IsNotEmpty qry2 CollectionAssert.Contains(qry2, "London") -[] +[] let ``simple operations in select query``() = let dc = sql.GetDataContext() //let dc = sql.GetDataContext(SelectOperations.DatabaseSide) // same results, maybe different order @@ -2671,7 +2671,7 @@ let ``simple operations in select query``() = CollectionAssert.IsNotEmpty qry Assert.AreEqual(6, qry.Length) -[] +[] let ``simple canonical operations in nested select query``() = let dc = sql.GetDataContext() @@ -2694,7 +2694,7 @@ let ``simple canonical operations in nested select query``() = CollectionAssert.IsNotEmpty qry2 Assert.AreEqual(6, qry2.Length) -[] +[] let ``simple canonical join query``() = let dc = sql.GetDataContext() @@ -2721,7 +2721,7 @@ let ``simple canonical join query``() = CollectionAssert.IsNotEmpty qry2 CollectionAssert.AreEqual(qry1, qry2) -[] +[] let ``simple query yield test``() = let dc = sql.GetDataContext() let query1 = @@ -2735,7 +2735,7 @@ let ``simple query yield test``() = CollectionAssert.IsNotEmpty res CollectionAssert.Contains(query1, "London1") -[] +[] let ``simple union query test``() = let dc = sql.GetDataContext() let query1 = @@ -2785,7 +2785,7 @@ let ``simple union all query test``() = Assert.IsNotEmpty(res2) CollectionAssert.Contains(res2, "Sevilla") -[] +[] let ``verify groupBy results``() = let dc = sql.GetDataContext() let enumtest = @@ -2810,7 +2810,7 @@ let ``verify groupBy results``() = CollectionAssert.AreEqual(inlogics,groupqry) -[] +[] let ``simple delete where query``() = task { let dc = sql.GetDataContext() @@ -2821,7 +2821,7 @@ let ``simple delete where query``() = () } :> Task -[] +[] let ``simple left join``() = let dc = sqlOption.GetDataContext() let qry = @@ -2853,7 +2853,7 @@ let ``simple query sproc result``() = Assert.IsNotNull(pragmaSchemaAsync.ResultSet) } :> Task -[] +[] let ``simple select with subquery exists subquery``() = let dc = sql.GetDataContext() let qry = @@ -2869,7 +2869,7 @@ let ``simple select with subquery exists subquery``() = Assert.AreEqual(91, qry.Count()) CollectionAssert.Contains(qry, "QUEEN") -[] +[] let ``simple select with join subqry``() = let dc = sql.GetDataContext() let subqry = @@ -2887,7 +2887,7 @@ let ``simple select with join subqry``() = Assert.AreEqual(91, qry.Count()) CollectionAssert.Contains(qry |> List.map fst, "QUEEN") -[] +[] let ``simple select with join to filtered subqry``() = let dc = sql.GetDataContext() let expected = @@ -2910,7 +2910,7 @@ let ``simple select with join to filtered subqry``() = } |> Seq.toList |> List.sort Assert.AreEqual(expected, qry) -[] +[] let ``simple select with subquery exists parameter from main query``() = let dc = sql.GetDataContext() let qry = @@ -2927,7 +2927,7 @@ let ``simple select with subquery exists parameter from main query``() = Assert.AreEqual(718, qry.Count()) CollectionAssert.Contains(qry, 10305L) -[] +[] let ``simple select with subquery not exists parameter from main query``() = let dc = sql.GetDataContext() let qry = @@ -2997,7 +2997,7 @@ let ``simple select navigation properties``() = let oid = alfkiOrders |> Seq.head Assert.AreEqual(10643L,oid) -[] +[] let ``where join order shouldn't matter``() = let dc = sql.GetDataContext() @@ -3023,7 +3023,7 @@ let ``where join order shouldn't matter``() = CollectionAssert.Contains(qry1, (10372L, 10372L)) CollectionAssert.Contains(qry2, (10372L, 10372L)) -[] +[] let ``simple select with subquery of subqueries``() = let dc = sql.GetDataContext() let subquery (subQueryIds:IQueryable) = @@ -3067,7 +3067,7 @@ type Employee = { LastName : string HireDate : DateTime } -[] +[] let ``simple mapTo test``() = let dc = sql.GetDataContext() let qry = @@ -3097,7 +3097,7 @@ let ``simple select query with different return types valueoptions``() = CollectionAssert.IsNotEmpty qry -[] +[] let ``leftOuterJoin with value-option types yields ValueNone for unmatched columns``() = // With VALUE_OPTION, a left-joined column is a ValueOption: ValueNone when there is no match, // ValueSome otherwise. (The entity itself is never option-wrapped, so selecting it whole is null.) @@ -3118,7 +3118,7 @@ let ``leftOuterJoin with value-option types yields ValueNone for unmatched colum Assert.AreEqual(1, fissa.Length) Assert.IsTrue(ValueOption.isNone (snd fissa.[0])) -[] +[] let ``simple select with ValueOption type query``() = let dcv = sqlValueOption.GetDataContext() let qry = @@ -3130,7 +3130,7 @@ let ``simple select with ValueOption type query``() = qry |> Assert.IsNotEmpty -[] +[] let ``simple select with group-by ValueOption type query``() = let dcv = sqlValueOption.GetDataContext() let qry = @@ -3145,7 +3145,7 @@ let ``simple select with group-by ValueOption type query``() = res |> Assert.IsNotEmpty -[] +[] let ``simple select with group-by count distinct type query``() = let dcv = sqlValueOption.GetDataContext() let qry = @@ -3160,7 +3160,7 @@ let ``simple select with group-by count distinct type query``() = res |> Assert.IsNotEmpty -[] +[] let ``valueoption copyOfStruct test``() = let dcv = sqlValueOption.GetDataContext() let qry = @@ -3211,7 +3211,7 @@ let test_querylogic_cont (ctx:sqlValueOption.dataContext) = } itms |> Seq.toList -[] +[] let ``mock for unit-testing: datacontext``() = let sampleDataMap = @@ -3286,7 +3286,7 @@ let ``simple select query with MapTo with options``() = Assert.AreEqual(Some "VINET", mapped2.CustomerId) Assert.True(mapped2.ShipRegion.IsNone) -[] +[] let ``exists-subquery correlated over outer join``() = let dc = sql.GetDataContext() // ground truth client-side @@ -3307,7 +3307,7 @@ let ``exists-subquery correlated over outer join``() = } |> Seq.toList |> List.sort Assert.AreEqual(expected, actual) -[] +[] let ``exists-subquery with inner join correlated over outer join``() = let dc = sql.GetDataContext() let custIds = query { for c in dc.Main.Customers do select c.CustomerId } |> Seq.toList |> Set.ofList @@ -3333,7 +3333,7 @@ let ``exists-subquery with inner join correlated over outer join``() = } |> Seq.toList |> List.sort Assert.AreEqual(expected, actual) -[] +[] let ``exists-subquery with inner join correlated to simple outer``() = let dc = sql.GetDataContext() let orders = query { for o in dc.Main.Orders do select o.OrderId } |> Seq.toList @@ -3353,3 +3353,106 @@ let ``exists-subquery with inner join correlated to simple outer``() = select ord.OrderId } |> Seq.toList |> List.sort Assert.AreEqual(expected, actual) + +[] +let ``groupBy with value-option columns in the key keeps their types``() = + let dc = sqlValueOption.GetDataContext() + let actual = + query { + for order in dc.Main.Orders do + join cust in dc.Main.Customers on (order.CustomerId.Value = cust.CustomerId) + where (cust.Country.Value = "Germany" || cust.Country.Value = "Venezuela") + groupBy (cust.CustomerId, cust.Region, cust.Fax, order.ShippedDate) into g + select (g.Key, g.Min(fun (o, _) -> o.OrderId)) + } |> Seq.toArray |> Array.sort + let expected = + query { + for order in dc.Main.Orders do + join cust in dc.Main.Customers on (order.CustomerId.Value = cust.CustomerId) + select (cust.Country, cust.CustomerId, cust.Region, cust.Fax, order.ShippedDate, order.OrderId) + } |> Seq.toArray + |> Array.filter (fun (country, _, _, _, _, _) -> country = ValueSome "Germany" || country = ValueSome "Venezuela") + |> Array.groupBy (fun (_, id, region, fax, shipped, _) -> (id, region, fax, shipped)) + |> Array.map (fun (key, rows) -> key, rows |> Array.map (fun (_, _, _, _, _, oid) -> oid) |> Array.min) + |> Array.sort + Assert.IsTrue(expected |> Array.exists (fun ((_, region, _, _), _) -> region.IsNone)) + Assert.IsTrue(expected |> Array.exists (fun ((_, region, _, _), _) -> region.IsSome)) + Assert.IsTrue(expected |> Array.exists (fun ((_, _, _, shipped), _) -> shipped.IsNone)) + Assert.AreEqual(expected, actual) + +[] +let ``groupBy with option columns in the key keeps their types``() = + let dc = sqlOption.GetDataContext() + let actual = + query { + for order in dc.Main.Orders do + join cust in dc.Main.Customers on (order.CustomerId.Value = cust.CustomerId) + where (cust.Country.Value = "Germany" || cust.Country.Value = "Venezuela") + groupBy (cust.CustomerId, cust.Region, order.ShippedDate) into g + select (g.Key, g.Count()) + } |> Seq.toArray |> Array.sort + let expected = + query { + for order in dc.Main.Orders do + join cust in dc.Main.Customers on (order.CustomerId.Value = cust.CustomerId) + select (cust.Country, cust.CustomerId, cust.Region, order.ShippedDate) + } |> Seq.toArray + |> Array.filter (fun (country, _, _, _) -> country = Some "Germany" || country = Some "Venezuela") + |> Array.countBy (fun (_, id, region, shipped) -> (id, region, shipped)) + |> Array.sort + Assert.IsTrue(expected |> Array.exists (fun ((_, _, shipped), _) -> shipped.IsNone)) + Assert.AreEqual(expected, actual) + +[] +let ``groupBy with a computed IsSome in the key``() = + let dc = sqlValueOption.GetDataContext() + let actual = + query { + for order in dc.Main.Orders do + join cust in dc.Main.Customers on (order.CustomerId.Value = cust.CustomerId) + where (cust.Country.Value = "Germany" || cust.Country.Value = "Venezuela") + groupBy (cust.CustomerId, order.ShipCountry, order.ShippedDate.IsSome) into g + select (g.Key, g.Count()) + } |> Seq.toArray |> Array.sort + let expected = + query { + for order in dc.Main.Orders do + join cust in dc.Main.Customers on (order.CustomerId.Value = cust.CustomerId) + select (cust.Country, cust.CustomerId, order.ShipCountry, order.ShippedDate) + } |> Seq.toArray + |> Array.filter (fun (country, _, _, _) -> country = ValueSome "Germany" || country = ValueSome "Venezuela") + |> Array.countBy (fun (_, id, shipCountry, shipped) -> (id, shipCountry, shipped.IsSome)) + |> Array.sort + Assert.IsTrue(expected |> Array.exists (fun ((_, _, shipped), _) -> not shipped)) + Assert.AreEqual(expected, actual) + +[] +let ``groupBy with two computed keys and no join gives each key its own result name``() = + let dc = sqlValueOption.GetDataContext() + let commands = System.Collections.Concurrent.ConcurrentQueue() + let capture = Handler(fun _ e -> commands.Enqueue e.Command) + Common.QueryEvents.SqlQueryEvent.AddHandler capture + try + let actual = + query { + for order in dc.Main.Orders do + groupBy (order.ShippedDate.IsSome, order.ShipRegion.IsNone) into g + select (g.Key, g.Count()) + } |> Seq.toArray |> Array.sort + let expected = + query { + for order in dc.Main.Orders do + select (order.ShippedDate, order.ShipRegion) + } |> Seq.toArray + |> Array.countBy (fun (shipped, region) -> (shipped.IsSome, region.IsNone)) + |> Array.sort + // Both keys vary, so a lost key would show + Assert.IsTrue(expected |> Array.exists (fun ((shipped, _), _) -> not shipped)) + Assert.IsTrue(expected |> Array.exists (fun ((_, noRegion), _) -> noRegion)) + Assert.AreEqual(expected, actual) + // Unaliased, PostgreSQL and SQL Server return both CASE columns under one name + let grouped = commands |> Seq.find (fun command -> command.Contains "GROUP BY") + StringAssert.Contains(" as key0", grouped) + StringAssert.Contains(" as key1", grouped) + finally + Common.QueryEvents.SqlQueryEvent.RemoveHandler capture diff --git a/tests/SqlProvider.Tests/SprocParameterTests.fs b/tests/SqlProvider.Tests/SprocParameterTests.fs index 226786e9..19a21d9d 100644 --- a/tests/SqlProvider.Tests/SprocParameterTests.fs +++ b/tests/SqlProvider.Tests/SprocParameterTests.fs @@ -14,7 +14,7 @@ let private cursorType = TypeMapping.Create(typeof.ToString(), DbType.Objec let private param name ordinal typeMapping direction = QueryParameter.Create(name, ordinal, typeMapping, direction) -[] +[] let ``MSSQL sproc call ignores result set columns from an offline schema``() = let resultSet = param "ResultSet" 0 cursorType ParameterDirection.Output // Offline the cached parameter list has the result set appended to the real parameters @@ -32,7 +32,7 @@ let ``MSSQL sproc call ignores result set columns from an offline schema``() = Assert.AreEqual([| box 1; box 2 |], values) Assert.AreEqual(1, outps.Length) -[] +[] let ``MSSQL function call passes the return value parameter``() = let returnValue = param "ReturnValue" 0 intType ParameterDirection.ReturnValue let inputParameters = @@ -45,7 +45,7 @@ let ``MSSQL function call passes the return value parameter``() = let names = allParams |> Array.map (fun (_,_,p) -> (p :?> IDataParameter).ParameterName) Assert.AreEqual([| "@RETURN_VALUE"; "@a" |], names) -[] +[] let ``distinctNamed drops repeated named parameters only``() = let parameters = [ param "" 0 intType ParameterDirection.ReturnValue diff --git a/tests/SqlProvider.Tests/SqlCheck.fs b/tests/SqlProvider.Tests/SqlCheck.fs new file mode 100644 index 00000000..05df8d07 --- /dev/null +++ b/tests/SqlProvider.Tests/SqlCheck.fs @@ -0,0 +1,100 @@ +/// Checks the SQL each test sends. A test must send at least one SQL command, so a query that by accident runs in +/// memory instead of in the database fails; [] marks a test that sends none on purpose. [] lists what +/// the SQL must contain: each argument is one expectation, met when any command the test sent contains one of its +/// |-separated alternatives, ignoring case and spacing, e.g. []. +module SqlCheck + +open System +open System.Collections.Concurrent +open System.Text.RegularExpressions +open FSharp.Data.Sql +open NUnit.Framework +open NUnit.Framework.Interfaces + +/// What the test's SQL must contain; see the module comment. +[] +type ExpectSqlAttribute([] expectations: string array) = + inherit Attribute() + member _.Expectations = expectations + +/// The test sends no SQL on purpose (a mock context, a failure before execution, or no query at all). +[] +type NoSqlAttribute() = + inherit Attribute() + +/// The commands sent by each running test, by test id. +let private sent = ConcurrentDictionary>() + +let private testId () = + try + TestContext.CurrentContext.Test.ID + with _ -> + null + +/// A command belongs to NUnit's current test where the flow that sent it knows the test. A continuation on the thread +/// pool (an Async awaited in a task) may not; it belongs to the running test when only one runs. +let private subscribed = + lazy + (Common.QueryEvents.SqlQueryEvent + |> Event.add (fun e -> + match testId () with + | id when not (isNull id) && sent.ContainsKey id -> sent[id].Enqueue e.Command + | _ -> + match List.ofSeq sent.Values with + | [ only ] -> only.Enqueue e.Command + | _ -> ())) + +/// Upper case, single spaces, no spaces around parentheses or commas. +let normalise (sql: string) = + let spaced = Regex.Replace(sql.ToUpperInvariant(), @"\s+", " ") + Regex.Replace(spaced, @"\s*([(),])\s*", "$1") + +/// The expectations no command meets. +let unmet (expectations: string seq) (commands: string list) = + let normalised = commands |> List.map normalise + + expectations + |> Seq.filter (fun expectation -> + let alternatives = expectation.Split('|') |> Array.map normalise + not (normalised |> List.exists (fun command -> alternatives |> Array.exists command.Contains))) + |> List.ofSeq + +[] +type SqlCheckAttribute() = + inherit Attribute() + + interface ITestAction with + member _.Targets = ActionTargets.Test + + member _.BeforeTest(test: ITest) = + subscribed.Force() + sent[test.Id] <- ConcurrentQueue() + + member _.AfterTest(test: ITest) = + match sent.TryRemove test.Id with + | true, commands when TestContext.CurrentContext.Result.Outcome.Status = TestStatus.Passed -> + let commands = List.ofSeq commands + let noSql, expected = + match test.Method with + | null -> false, [||] + | m -> + not (Array.isEmpty (m.GetCustomAttributes(false))), + m.GetCustomAttributes(false) + + let shown () = + if commands.IsEmpty then "(none)" else String.Join("\n", commands) + + if not noSql then + if commands.IsEmpty then + Assert.Fail "The test sent no SQL: its query ran in memory. Mark it [] if that is meant." + + for expect in expected do + match unmet expect.Expectations commands with + | [] -> () + | missing -> + let wanted = String.Join(", ", missing) + Assert.Fail($"No SQL sent contains {wanted}. Sent:\n{shown ()}") + | _ -> () + +[] +do () diff --git a/tests/SqlProvider.Tests/SqlProvider.Tests.fsproj b/tests/SqlProvider.Tests/SqlProvider.Tests.fsproj index 5b382419..e4029588 100644 --- a/tests/SqlProvider.Tests/SqlProvider.Tests.fsproj +++ b/tests/SqlProvider.Tests/SqlProvider.Tests.fsproj @@ -20,6 +20,7 @@ true + @@ -37,6 +38,7 @@ + Always diff --git a/tests/SqlProvider.Tests/more/AdvancedQueryTests.fs b/tests/SqlProvider.Tests/more/AdvancedQueryTests.fs index 13933754..b4afcc96 100644 --- a/tests/SqlProvider.Tests/more/AdvancedQueryTests.fs +++ b/tests/SqlProvider.Tests/more/AdvancedQueryTests.fs @@ -41,7 +41,7 @@ type sqlOpt = SqlDataProvider] +[] let ``complex join with navigation properties`` () = let ctx = sql.GetDataContext() let query = @@ -56,7 +56,7 @@ let ``complex join with navigation properties`` () = let results = query |> Seq.toList Assert.IsNotEmpty(results) -[] +[] let ``subquery with exists pattern`` () = let ctx = sql.GetDataContext() @@ -75,7 +75,7 @@ let ``subquery with exists pattern`` () = let results = expensiveOrderCustomers |> Seq.toList Assert.IsNotEmpty(results) -[] +[] let ``complex aggregation with grouping`` () = let ctx = sql.GetDataContext() @@ -93,7 +93,7 @@ let ``complex aggregation with grouping`` () = let results = orderStats |> Seq.toList Assert.IsNotEmpty(results) -[] +[] let ``option type handling with ValueSome pattern`` () = let ctx = sqlOpt.GetDataContext() @@ -107,7 +107,7 @@ let ``option type handling with ValueSome pattern`` () = let results = customersWithRegion |> Seq.toList Assert.IsNotEmpty(results) -[] +[] let ``pagination with complex sorting`` () = let ctx = sql.GetDataContext() let pageSize = 10 @@ -128,7 +128,7 @@ let ``pagination with complex sorting`` () = Assert.LessOrEqual(results.Length, pageSize) -[] +[] let ``dynamic filtering with multiple optional parameters`` () = let ctx = sql.GetDataContext() @@ -156,7 +156,7 @@ let ``dynamic filtering with multiple optional parameters`` () = Assert.IsNotEmpty(results1) Assert.IsNotEmpty(results2) -[] +[] let ``complex case when expression`` () = let ctx = sql.GetDataContext() diff --git a/tests/SqlProvider.Tests/more/AdvancedQueryTestsWithOpts.fs b/tests/SqlProvider.Tests/more/AdvancedQueryTestsWithOpts.fs index 25671c94..811b8c69 100644 --- a/tests/SqlProvider.Tests/more/AdvancedQueryTestsWithOpts.fs +++ b/tests/SqlProvider.Tests/more/AdvancedQueryTestsWithOpts.fs @@ -34,7 +34,7 @@ let resolutionPath = __SOURCE_DIRECTORY__ + "/../libs" type sql = SqlDataProvider FSharp.Data.Sql.Common.QueryEvents.SqlQueryEvent |> Event.add (printfn "Executing SQL: %O") -[] +[] let ``three table join with forced join operator``() = let dc = sql.GetDataContext() let query = @@ -51,7 +51,7 @@ let ``three table join with forced join operator``() = Assert.IsTrue(results.Length > 0) Assert.IsTrue(results.Length <= 5) -[] +[] let ``multiple table join with complex filtering``() = let dc = sql.GetDataContext() let query = @@ -74,7 +74,7 @@ let ``multiple table join with complex filtering``() = // Subquery and Contains Test Examples -[] +[] let ``contains with subquery``() = let dc = sql.GetDataContext() @@ -96,7 +96,7 @@ let ``contains with subquery``() = let results = query |> Array.ofSeq Assert.IsTrue(results.Length > 0) -[] +[] let ``multiple subqueries with complex conditions``() = let dc = sql.GetDataContext() @@ -131,7 +131,7 @@ let ``multiple subqueries with complex conditions``() = let results = query |> Array.ofSeq Assert.IsTrue(results.Length > 0) -[] +[] let ``nested subquery with exists pattern``() = let dc = sql.GetDataContext() @@ -153,7 +153,7 @@ let ``nested subquery with exists pattern``() = // Advanced Sorting and Pagination Tests -[] +[] let ``multiple sort criteria with thenBy``() = let dc = sql.GetDataContext() let query = @@ -175,7 +175,7 @@ let ``multiple sort criteria with thenBy``() = let cities = items |> Array.map (fun (_, city, _) -> city) |> Array.distinct |> Array.sort Assert.IsTrue(cities.Length > 0) -[] +[] let ``pagination with skip and take``() = let dc = sql.GetDataContext() let pageSize = 5 @@ -193,7 +193,7 @@ let ``pagination with skip and take``() = let results = query |> Array.ofSeq Assert.IsTrue(results.Length <= pageSize) -[] +[] let ``conditional sorting based on runtime parameter``() = let dc = sql.GetDataContext() @@ -223,7 +223,7 @@ let ``conditional sorting based on runtime parameter``() = // Async Execution Tests -[] +[] let ``async array execution``() = task { let dc = sql.GetDataContext() @@ -237,7 +237,7 @@ let ``async array execution``() = Assert.AreEqual(10, results.Length) } -[] +[] let ``async list execution``() = task { let dc = sql.GetDataContext() @@ -251,7 +251,7 @@ let ``async list execution``() = Assert.AreEqual(5, results.Length) } -[] +[] let ``async single result with tryHeadAsync``() = task { let dc = sql.GetDataContext() @@ -266,7 +266,7 @@ let ``async single result with tryHeadAsync``() = Assert.AreEqual("ALFKI", result.Value.CustomerId) } -[] +[] let ``async single result with headAsync``() = task { let dc = sql.GetDataContext() @@ -281,7 +281,7 @@ let ``async single result with headAsync``() = Assert.IsTrue(result.Length > 0) } -[] +[] let ``parallel async query execution``() = task { let dc = sql.GetDataContext() @@ -309,7 +309,7 @@ let ``parallel async query execution``() = // Option Type Handling Tests -[] +[] let ``filtering with option type IsSome``() = let dc = sql.GetDataContext() let query = @@ -322,7 +322,7 @@ let ``filtering with option type IsSome``() = let results = query |> Array.ofSeq Assert.IsTrue(results.Length > 0) -[] +[] let ``filtering with option type IsNone``() = let dc = sql.GetDataContext() let query = @@ -335,7 +335,7 @@ let ``filtering with option type IsNone``() = let results = query |> Array.ofSeq Assert.IsTrue(results.Length >= 0) // May be 0 if all orders are shipped -[] +[] let ``complex option type filtering``() = let dc = sql.GetDataContext() let query = @@ -354,7 +354,7 @@ let ``complex option type filtering``() = // Performance and Integration Tests -[] +[] let ``large result set with chunked processing``() = task { let dc = sql.GetDataContext() @@ -383,7 +383,7 @@ let ``large result set with chunked processing``() = Assert.IsTrue(totalProcessed > 0) } -[] +[] let ``complex business logic query``() = let dc = sql.GetDataContext() diff --git a/tests/SqlProvider.Tests/more/ComplexJoinTests.fs b/tests/SqlProvider.Tests/more/ComplexJoinTests.fs index f297b271..c0fc63cf 100644 --- a/tests/SqlProvider.Tests/more/ComplexJoinTests.fs +++ b/tests/SqlProvider.Tests/more/ComplexJoinTests.fs @@ -36,7 +36,7 @@ FSharp.Data.Sql.Common.QueryEvents.SqlQueryEvent |> Event.add (printfn "Executin let dc = sql.GetDataContext() -[] +[] let ``three table join with nullable foreign key should work``() = let result = query { @@ -55,7 +55,7 @@ let ``three table join with nullable foreign key should work``() = Assert.IsTrue(not (String.IsNullOrEmpty employee)) printfn "Order %d: %s handled by %s" orderId company employee) -[] +[] let ``four table join should work correctly``() = let result = query { @@ -83,7 +83,7 @@ let ``four table join should work correctly``() = printfn "Order %d: %s ordered %d x %s at $%.2f" order.OrderId order.CompanyName order.Quantity order.ProductName order.UnitPrice) -[] +[] let ``left join should include records with no matches``() = let result = query { @@ -110,7 +110,7 @@ let ``left join should include records with no matches``() = Assert.IsTrue(not (String.IsNullOrEmpty company)) printfn "%s: %s" company orderInfo) -[] +[] let ``conditional join with complex where clause should work``() = let startDate = DateTime(1996, 1, 1) let endDate = DateTime(1996, 12, 31) @@ -135,7 +135,7 @@ let ``conditional join with complex where clause should work``() = Assert.IsTrue(orderDate >= startDate && orderDate <= endDate) printfn "London customer %s: Order %d (%d items) on %A" company orderId quantity orderDate) -[] +[] let ``multiple joins with aggregation should work``() = let result = query { @@ -152,7 +152,7 @@ let ``multiple joins with aggregation should work``() = Assert.IsTrue(orderDetailCount > 0) printfn "London customer %s has %d order details" company orderDetailCount) -[] +[] let ``join with calculated fields should work``() = let result = query { @@ -182,7 +182,7 @@ let ``join with calculated fields should work``() = printfn "Order %d: %s, Qty: %d, Price: $%.2f, Total: $%.2f, Discounted: $%.2f" line.OrderId line.ProductName line.Quantity line.UnitPrice line.LineTotal line.DiscountedTotal) -[] +[] let ``complex join with multiple conditions should work``() = let result = query { @@ -204,7 +204,7 @@ let ``complex join with multiple conditions should work``() = Assert.IsTrue(not (String.IsNullOrEmpty product)) printfn "UK customer %s ordered %s at $%.2f on %A" company product price orderDate) -[] +[] let ``join with union-like pattern should work``() = // Get customer info with order summary let customerOrders = @@ -234,7 +234,7 @@ let ``join with union-like pattern should work``() = Assert.IsTrue(customer.OrderCount >= 0) printfn "Customer %s (%s) has %d orders" customer.CompanyName customer.CustomerId customer.OrderCount) -[] +[] let ``nested join conditions should work``() = // Find orders where the employee's city matches the customer's city let result = @@ -254,7 +254,7 @@ let ``nested join conditions should work``() = Assert.IsTrue(not (String.IsNullOrEmpty city)) printfn "Order %d: %s in %s handled by local employee %s" orderId company city employee) -[] +[] let ``join performance with filtering should be efficient``() = // Test that filtering before joining is more efficient let stopwatch = System.Diagnostics.Stopwatch.StartNew() diff --git a/tests/SqlProvider.Tests/more/NavigationPropertiesTests.fs b/tests/SqlProvider.Tests/more/NavigationPropertiesTests.fs index 2abb19e9..fc23d01c 100644 --- a/tests/SqlProvider.Tests/more/NavigationPropertiesTests.fs +++ b/tests/SqlProvider.Tests/more/NavigationPropertiesTests.fs @@ -61,7 +61,7 @@ let ``multiple level navigation`` () = Assert.IsNotEmpty(customerOrderDetails) -[] +[] let ``navigation with filtering`` () = let ctx = sql.GetDataContext() @@ -75,7 +75,7 @@ let ``navigation with filtering`` () = Assert.IsNotEmpty(recentCustomerOrders) -[] +[] let ``navigation with aggregation`` () = let ctx = sql.GetDataContext() @@ -106,7 +106,7 @@ let ``reverse navigation properties`` () = Assert.IsNotEmpty(orderCustomers) -[] +[] let ``navigation with sorting`` () = let ctx = sql.GetDataContext() @@ -121,7 +121,7 @@ let ``navigation with sorting`` () = Assert.IsNotEmpty(sortedCustomerOrders) -[] +[] let ``navigation with date filtering`` () = let ctx = sql.GetDataContext() @@ -135,7 +135,7 @@ let ``navigation with date filtering`` () = Assert.IsNotEmpty(recentCustomerActivity) -[] +[] let ``navigation with distinct results`` () = let ctx = sql.GetDataContext() @@ -151,7 +151,7 @@ let ``navigation with distinct results`` () = Assert.IsNotEmpty(customersWithLargeOrders) -[] +[] let ``navigation with conditional logic`` () = let ctx = sql.GetDataContext() @@ -172,7 +172,7 @@ let ``navigation with conditional logic`` () = Assert.IsNotEmpty(customerCategories) -[] +[] let ``navigation with pagination`` () = let ctx = sql.GetDataContext() let pageSize = 10 diff --git a/tests/SqlProvider.Tests/more/OptionTypesTests.fs b/tests/SqlProvider.Tests/more/OptionTypesTests.fs index 3b0301c4..697d8807 100644 --- a/tests/SqlProvider.Tests/more/OptionTypesTests.fs +++ b/tests/SqlProvider.Tests/more/OptionTypesTests.fs @@ -35,7 +35,7 @@ type sql = SqlDataProvider Event.add (printfn "Executing SQL: %O") -[] +[] let ``value option basic usage`` () = let ctx = sql.GetDataContext() @@ -68,7 +68,7 @@ let ``value option pattern matching`` () = Assert.IsNotEmpty(categorizeCustomers) -[] +[] let ``value option filtering patterns`` () = let ctx = sql.GetDataContext() @@ -140,7 +140,7 @@ let ``value option mapping and transformation`` () = Assert.IsNotEmpty(upperCaseRegions) -[] +[] let ``value option with nullable datetime`` () = let ctx = sql.GetDataContext() @@ -161,7 +161,7 @@ let ``value option with nullable datetime`` () = Assert.IsNotEmpty(ordersWithShipDate) -[] +[] let ``value option aggregations`` () = let ctx = sql.GetDataContext() @@ -185,7 +185,7 @@ let ``value option aggregations`` () = Assert.Greater(regionsCount, 0) Assert.IsNotEmpty(distinctRegions) -[] +[] let ``value option in joins`` () = let ctx = sql.GetDataContext() @@ -249,7 +249,7 @@ let ``value option insertion and updates`` () = do! ctx.SubmitUpdatesAsync() } -[] +[] let ``value option complex expressions`` () = let ctx = sql.GetDataContext() @@ -313,7 +313,7 @@ let ``value option bind operations`` () = Assert.IsNotEmpty(processCustomerData) -[] +[] let ``value option with nullable foreign keys`` () = let ctx = sql.GetDataContext() diff --git a/tests/SqlProvider.Tests/more/PerformancePatternTests.fs b/tests/SqlProvider.Tests/more/PerformancePatternTests.fs index 7ca55afa..01d93448 100644 --- a/tests/SqlProvider.Tests/more/PerformancePatternTests.fs +++ b/tests/SqlProvider.Tests/more/PerformancePatternTests.fs @@ -46,7 +46,7 @@ let measureQueryPerformance<'T> (queryName: string) (query: unit -> 'T) = printfn "Query '%s' completed in %d ms" queryName stopwatch.ElapsedMilliseconds result, stopwatch.ElapsedMilliseconds -[] +[] let ``chunking large ID arrays should work efficiently``() = // Get a list of order IDs to test with let allOrderIds = @@ -77,7 +77,7 @@ let ``chunking large ID arrays should work efficiently``() = Assert.IsTrue(Array.forall2 (=) originalIds retrievedIds) printfn "Successfully processed %d orders in %d chunks" allOrderIds.Length chunks.Length -[] +[] let ``pagination should work correctly``() = let pageSize = 5 let page1 = @@ -112,7 +112,7 @@ let ``pagination should work correctly``() = printfn "Page 1: %A" page1 printfn "Page 2: %A" page2 -[] +[] let ``cursor-based pagination should be more efficient for large offsets``() = // Simulate cursor pagination let pageSize = 3 @@ -152,7 +152,7 @@ let ``cursor-based pagination should be more efficient for large offsets``() = printfn "First page: %A" firstPageItems printfn "Second page: %A" secondPageItems -[] +[] let ``early filtering should be more efficient than late filtering``() = let testDate = DateTime(1997, 1, 1) @@ -185,7 +185,7 @@ let ``early filtering should be more efficient than late filtering``() = printfn "Early filtering found %d results" earlyResults.Length -[] +[] let ``selective column loading should be more efficient``() = // Selective loading - only needed columns let (selectiveResults, selectiveTime) = measureQueryPerformance "SelectiveLoading" (fun () -> @@ -215,7 +215,7 @@ let ``selective column loading should be more efficient``() = printfn "Selective loading: %d ms, Full loading: %d ms" selectiveTime fullTime -[] +[] let ``streaming large datasets should control memory usage``() = let processedCount = ref 0 let maxItemsToProcess = 50 // Limit for test @@ -244,7 +244,7 @@ let ``streaming large datasets should control memory usage``() = printfn "Streamed %d items in %d ms" !processedCount streamTime -[] +[] let ``batch processing should handle large datasets efficiently``() = let batchSize = 10 let maxBatches = 3 // Limit for test @@ -280,7 +280,7 @@ let ``batch processing should handle large datasets efficiently``() = printfn "Processed %d items in %d batches (%d ms)" !totalProcessed !batchesProcessed batchTime -[] +[] let ``index-friendly queries should perform well``() = // Query using indexed column (OrderId is typically indexed) let (indexedResults, indexedTime) = measureQueryPerformance "IndexedQuery" (fun () -> @@ -304,7 +304,7 @@ let ``index-friendly queries should perform well``() = printfn "Indexed query returned %d results in %d ms" indexedResults.Length indexedTime -[] +[] let ``query performance should be consistent across multiple runs``() = let runs = 3 let times = ResizeArray() @@ -337,7 +337,7 @@ let ``query performance should be consistent across multiple runs``() = printfn "Query times: Min=%d ms, Max=%d ms, Avg=%.1f ms" minTime maxTime avgTime -[] +[] let ``complex query optimization should show measurable improvement``() = // Complex query with multiple joins and filters let complexQuery() = diff --git a/tests/SqlProvider.Tests/more/PerformanceTests.fs b/tests/SqlProvider.Tests/more/PerformanceTests.fs index 4bb7bb07..bc0485d3 100644 --- a/tests/SqlProvider.Tests/more/PerformanceTests.fs +++ b/tests/SqlProvider.Tests/more/PerformanceTests.fs @@ -37,7 +37,7 @@ type sql = SqlDataProvider Event.add (printfn "Executing SQL: %O") -[] +[] let ``connection pooling pattern`` () = let pool = ConcurrentStack() let maxPoolSize = 5 @@ -64,7 +64,7 @@ let ``connection pooling pattern`` () = Assert.Greater(pool.Count, 0) } -[] +[] let ``batch processing pattern`` () = let batchSize = 100 @@ -102,7 +102,7 @@ let ``batch processing pattern`` () = Assert.AreEqual(totalCustomers, processedCount) } -[] +[] let ``memoization pattern test`` () = let cache = ConcurrentDictionary>() let mutable callCount = 0 @@ -173,7 +173,7 @@ let ``chunked processing for large datasets`` () = Assert.AreEqual(allCustomers.Length, totalProcessed) } -[] +[] let ``parallel query execution`` () = let getCustomerOrderCount customerId = task { @@ -206,7 +206,7 @@ let ``parallel query execution`` () = Assert.IsTrue(results |> Array.forall (fun (_, count) -> count >= 0)) } -[] +[] let ``connection timeout handling`` () = task { try @@ -223,7 +223,8 @@ let ``connection timeout handling`` () = select (g.Key, g.Sum(fun (_,_,x) -> x.UnitPrice * decimal x.Quantity)) } |> Seq.executeQueryAsync |> Async.AwaitTask - Assert.IsNotNull(result) + // Enumerating runs the query: for this shape executeQueryAsync returns it unexecuted + Assert.IsNotEmpty(result) with | ex when ex.Message.Contains "timeout" -> Assert.Pass("Timeout handled correctly") @@ -275,7 +276,7 @@ let ``memory efficient large result processing`` () = Assert.Greater(totalValue, 0.0m) } -[] +[] let ``query plan caching test`` () = let ctx = sql.GetDataContext() // in execution order: the list is built by prepending, so it is reversed at the end diff --git a/tests/SqlProvider.Tests/more/SubqueryCompositionTests.fs b/tests/SqlProvider.Tests/more/SubqueryCompositionTests.fs index 257174a1..2ee133cd 100644 --- a/tests/SqlProvider.Tests/more/SubqueryCompositionTests.fs +++ b/tests/SqlProvider.Tests/more/SubqueryCompositionTests.fs @@ -37,7 +37,7 @@ FSharp.Data.Sql.Common.QueryEvents.SqlQueryEvent |> Event.add (printfn "Executin let dc = sql.GetDataContext() -[] +[] let ``subquery as filter should work correctly``() = // Find customers who have placed large orders let largeOrderCustomers = @@ -63,7 +63,7 @@ let ``subquery as filter should work correctly``() = Assert.IsTrue(not (String.IsNullOrEmpty company)) printfn "Large order customer: %s (%s)" company id) -[] +[] let ``multiple subqueries combined should work``() = // Find customers who have placed recent orders let recentOrderCustomers = @@ -97,7 +97,7 @@ let ``multiple subqueries combined should work``() = result |> List.iter (fun (pid, company) -> printfn "Recent + expensive customer: %s (%s)" company pid) -[] +[] let ``exists pattern using contains should work``() = // Find customers who have ordered seafood products (CategoryId = 8) let seafoodCustomerIds = @@ -120,7 +120,7 @@ let ``exists pattern using contains should work``() = result |> List.iter (fun (pid, company) -> printfn "Seafood customer: %s (%s)" company pid) -[] +[] let ``not exists pattern should work``() = // Find customers who have never placed an order let customerIdsWithOrders = @@ -143,7 +143,7 @@ let ``not exists pattern should work``() = result |> List.iter (fun (pid, company) -> printfn "No orders: %s (%s)" company pid) -[] +[] let ``correlated subquery pattern should work``() = // Find customers with above-average order counts for their country let result = @@ -183,7 +183,7 @@ let ``correlated subquery pattern should work``() = type CustomerFilter = IQueryable -> IQueryable -[] +[] let ``query composition with filter functions should work``() = // Define reusable query filters @@ -222,7 +222,7 @@ type SearchCriteria = { MinOrderValue: float option } -[] +[] let ``dynamic query building should work``() = let buildCustomerQuery (criteria: SearchCriteria) = @@ -282,7 +282,7 @@ let ``dynamic query building should work``() = Assert.AreEqual("London", customer.City.Value) printfn "London customer with orders: %s" customer.CompanyName) -[] +[] let ``complex subquery with multiple levels should work``() = // Find products that were ordered by customers from specific regions let europeanCustomerIds = @@ -320,7 +320,7 @@ let ``complex subquery with multiple levels should work``() = result |> List.iter (fun (name, pid) -> printfn "Popular European product: %s (ID: %d)" name pid) -[] +[] let ``subquery result caching should work``() = // Simulate caching subquery results for reuse let expensiveProductIds = @@ -360,3 +360,76 @@ let ``subquery result caching should work``() = result |> List.iter (fun (company, pid) -> printfn "Customer ordering expensive products: %s (%s)" company pid) + +[] +let ``parameters of a subquery nested in another subquery are bound``() = + let minQty = 50s + let allCases = false + let onlyCase = "ERNSH" + let openOnly = true + let bigOrders = + query { + for detail in dc.Main.OrderDetails do + where (detail.Quantity > minQty) + select detail.OrderId + } + let chosen = + query { + for order in dc.Main.Orders do + where ((allCases || order.CustomerId.Value = onlyCase) && (not openOnly || not (bigOrders.Contains order.OrderId))) + select order.OrderId + } + let runQuery () = + query { + for order in dc.Main.Orders do + where (chosen.Contains order.OrderId) + select order.OrderId + } |> Seq.toArray |> Array.sort + + let bigIds = query { for d in dc.Main.OrderDetails do select (d.OrderId, d.Quantity) } |> Seq.filter (fun (_, q) -> q > minQty) |> Seq.map fst |> Set.ofSeq + let expected = + query { for o in dc.Main.Orders do select (o.OrderId, o.CustomerId) } + |> Seq.filter (fun (id, c) -> c = ValueSome onlyCase && not (bigIds.Contains id)) + |> Seq.map fst |> Seq.toArray |> Array.sort + + Assert.IsNotEmpty expected + Assert.AreEqual(expected, runQuery ()) + Assert.AreEqual(expected, runQuery ()) + +[] +let ``parameters of three nested subquery levels are bound``() = + let minQty = 50s + let country = "Germany" + let bigOrders = + query { + for detail in dc.Main.OrderDetails do + where (detail.Quantity > minQty) + select detail.OrderId + } + let germanBigOrders = + query { + for order in dc.Main.Orders do + where (order.ShipCountry.Value = country && bigOrders.Contains order.OrderId) + select order.OrderId + } + let customersOfThose = + query { + for order in dc.Main.Orders do + where (order.OrderId > 0L && germanBigOrders.Contains order.OrderId) + select order.CustomerId.Value + } + let result = + query { + for customer in dc.Main.Customers do + where (customer.CustomerId <> "" && customersOfThose.Contains customer.CustomerId) + select customer.CustomerId + } |> Seq.toArray |> Array.sort + + let bigIds = query { for d in dc.Main.OrderDetails do select (d.OrderId, d.Quantity) } |> Seq.filter (fun (_, q) -> q > minQty) |> Seq.map fst |> Set.ofSeq + let expected = + query { for o in dc.Main.Orders do select (o.OrderId, o.ShipCountry, o.CustomerId) } + |> Seq.filter (fun (id, c, _) -> c = ValueSome country && bigIds.Contains id) + |> Seq.map (fun (_, _, cust) -> cust.Value) |> Seq.distinct |> Seq.toArray |> Array.sort + + Assert.IsNotEmpty expected + Assert.AreEqual(expected, result) diff --git a/tests/SqlProvider.Tests/more/SupplementaryTests.fs b/tests/SqlProvider.Tests/more/SupplementaryTests.fs index c4a97127..651cfa2d 100644 --- a/tests/SqlProvider.Tests/more/SupplementaryTests.fs +++ b/tests/SqlProvider.Tests/more/SupplementaryTests.fs @@ -36,7 +36,7 @@ type sql = SqlDataProvider Event.add (printfn "Executing SQL: %O") -[] +[] let ``option type IsSome filtering``() = let dc = sql.GetDataContext() let query = @@ -49,7 +49,7 @@ let ``option type IsSome filtering``() = let results = query |> Array.ofSeq Assert.IsTrue(results.Length > 0) -[] +[] let ``option type IsNone filtering``() = let dc = sql.GetDataContext() let query = @@ -62,7 +62,7 @@ let ``option type IsNone filtering``() = let results = query |> Array.ofSeq Assert.IsTrue(results.Length >= 0) // May be 0 if all orders are shipped -[] +[] let ``complex option type conditions``() = let dc = sql.GetDataContext() let query = @@ -79,7 +79,7 @@ let ``complex option type conditions``() = let results = query |> Array.ofSeq Assert.IsTrue(results.Length >= 0) -[] +[] let ``option value extraction in queries``() = let dc = sql.GetDataContext() let query = @@ -96,7 +96,7 @@ let ``option value extraction in queries``() = Assert.IsTrue(shipDate <> DateTime.MinValue) ) -[] +[] let ``option type aggregations``() = let dc = sql.GetDataContext() let query = @@ -123,7 +123,7 @@ let ``option type aggregations``() = // Dynamic Query Composition Tests //module DynamicQueryTests = -[] +[] let ``conditional where clauses``() = let dc = sql.GetDataContext() @@ -167,7 +167,7 @@ let ``conditional where clauses``() = Assert.AreEqual(0, noneSelected.Length) Assert.IsTrue(allOrders.Length >= shippedOnly.Length + unshippedOnly.Length) -[] +[] let ``dynamic sorting based on parameter``() = let dc = sql.GetDataContext() @@ -198,7 +198,7 @@ let ``dynamic sorting based on parameter``() = let countriesSorted = byCountry |> Array.map fst |> Array.pairwise |> Array.forall (fun (a, b) -> a <= b) Assert.IsTrue(countriesSorted) -[] +[] let ``conditional filtering with multiple parameters``() = let dc = sql.GetDataContext() @@ -251,7 +251,7 @@ let ``conditional filtering with multiple parameters``() = // Advanced Aggregation Tests //module AggregationTests = -[] +[] let ``groupBy with multiple aggregations``() = let dc = sql.GetDataContext() let query = @@ -279,7 +279,7 @@ let ``groupBy with multiple aggregations``() = Assert.IsTrue(minPrice <= maxPrice) ) -[] +[] let ``complex groupBy with having clause equivalent``() = let dc = sql.GetDataContext() let query = @@ -301,7 +301,7 @@ let ``complex groupBy with having clause equivalent``() = // Performance and Edge Case Tests //module PerformanceTests = -[] +[] let ``large dataset pagination performance``() = task { let dc = sql.GetDataContext() @@ -329,7 +329,7 @@ let ``large dataset pagination performance``() = Assert.IsTrue(totalProcessed > 0) } -[] +[] let ``null value handling in aggregations``() = let dc = sql.GetDataContext() @@ -352,7 +352,7 @@ let ``null value handling in aggregations``() = let shippedGroup = results |> Array.tryFind (fun (isShipped, _, _) -> isShipped) Assert.IsTrue(shippedGroup.IsSome) -[] +[] let ``empty result set handling``() = let dc = sql.GetDataContext() @@ -367,7 +367,7 @@ let ``empty result set handling``() = let results = query |> Array.ofSeq Assert.AreEqual(0, results.Length) -[] +[] let ``concurrent query execution``() = task { let dc = sql.GetDataContext() diff --git a/tests/SqlProvider.Tests/more/ValueOptionTests.fs b/tests/SqlProvider.Tests/more/ValueOptionTests.fs index 605da69f..41bea916 100644 --- a/tests/SqlProvider.Tests/more/ValueOptionTests.fs +++ b/tests/SqlProvider.Tests/more/ValueOptionTests.fs @@ -37,7 +37,7 @@ FSharp.Data.Sql.Common.QueryEvents.SqlQueryEvent |> Event.add (printfn "Executin let dc = sql.GetDataContext() -[] +[] let ``valueOption column should filter correctly with IsSome``() = let result = query { @@ -49,7 +49,7 @@ let ``valueOption column should filter correctly with IsSome``() = Assert.IsTrue(result.Length > 0) printfn "Found %d customers with region" result.Length -[] +[] let ``valueOption column should filter by value correctly``() = let result = query { @@ -63,7 +63,7 @@ let ``valueOption column should filter by value correctly``() = Assert.AreEqual("WA", region) printfn "Customer %s in region %s" pid region) -[] +[] let ``valueOption should work with pattern matching in select``() = let result = query { @@ -80,7 +80,7 @@ let ``valueOption should work with pattern matching in select``() = Assert.IsTrue(not (String.IsNullOrEmpty region)) printfn "Customer %s: %s" pid region) -[] +[] let ``valueOption should work with defaultValue``() = let result = query { @@ -94,7 +94,7 @@ let ``valueOption should work with defaultValue``() = Assert.IsTrue(not (String.IsNullOrEmpty region)) printfn "Customer %s: %s" pid region) -[] +[] let ``valueOption nullable foreign key join should work``() = let result = query { @@ -111,7 +111,7 @@ let ``valueOption nullable foreign key join should work``() = Assert.IsTrue(not (String.IsNullOrEmpty firstName)) printfn "Order %d handled by %s %s" orderId firstName lastName) -[] +[] let ``valueOption date comparison should work``() = let testDate = DateTime(1996, 7, 1) let result = @@ -127,7 +127,7 @@ let ``valueOption date comparison should work``() = Assert.IsTrue(orderDate >= testDate) printfn "Order %d on %A" orderId orderDate) -[] +[] let ``valueOption complex where clause should work``() = let result = query { @@ -144,7 +144,7 @@ let ``valueOption complex where clause should work``() = Assert.IsTrue(shipped > required) printfn "Late order %d: required %A, shipped %A" orderId required shipped) -[] +[] let ``valueOption count with filter should work``() = let countWithRegion = query { @@ -165,7 +165,7 @@ let ``valueOption count with filter should work``() = Assert.IsTrue(countWithRegion <= totalCount) printfn "Customers with region: %d out of %d total" countWithRegion totalCount -[] +[] let ``valueOption computed values should work``() = let result = query { @@ -193,7 +193,7 @@ let ``valueOption computed values should work``() = | ValueNone -> printfn "Order %d shipping time unknown, late: %b" order.OrderId order.IsLate) -[] +[] let ``valueOption aggregation should handle nulls correctly``() = // Count of non-null regions let regionCount =