From de14e3e335aed5e71bbbb79c0717d9d231786d64 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sat, 22 Dec 2018 14:30:56 +0000 Subject: [PATCH 1/3] Simple implementation of left join with non-foreign keys --- src/SQLProvider/Operators.fs | 3 ++- src/SQLProvider/SqlRuntime.Linq.fs | 33 +++++++++++++++------------ tests/SqlProvider.Tests/QueryTests.fs | 20 ++++++++++++++++ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/SQLProvider/Operators.fs b/src/SQLProvider/Operators.fs index 589e6e5d2..1a09005ae 100644 --- a/src/SQLProvider/Operators.fs +++ b/src/SQLProvider/Operators.fs @@ -149,7 +149,8 @@ module Operators = /// Not Like let (<>%) (a:'a) (b:string) = false /// Left join - let (!!) (a:IQueryable<_>) = a + let private leftJoin (a:'a) = a + let (!!) (a:IQueryable<'a>) = query { for x in a do select (leftJoin x) } /// Standard Deviation let StdDev (a:'a) = 1m diff --git a/src/SQLProvider/SqlRuntime.Linq.fs b/src/SQLProvider/SqlRuntime.Linq.fs index c4afe017a..bd0000bb1 100644 --- a/src/SQLProvider/SqlRuntime.Linq.fs +++ b/src/SQLProvider/SqlRuntime.Linq.fs @@ -39,6 +39,12 @@ module internal QueryImplementation = let (|SourceWithQueryData|_|) = function Constant ((:? IWithSqlService as org), _) -> Some org | _ -> None let (|RelDirection|_|) = function Constant ((:? RelationshipDirection as s),_) -> Some s | _ -> None + let (|OptionalOuterJoin|) e = + match e with + | MethodCall(None, (!!), [inner]) -> (true,inner) + | MethodCall(None,MethodWithName("op_BangBang"), [inner]) -> (true,inner) + | _ -> (false,e) + let parseQueryResults (projector:Delegate) (results:SqlEntity[]) = let args = projector.GetType().GenericTypeArguments seq { @@ -435,10 +441,6 @@ module internal QueryImplementation = // multiple SelectMany calls in sequence are represented in the same expression tree which must be parsed recursively (and joins too!) let rec processSelectManys (toAlias:string) (inExp:Expression) (outExp:SqlExp) (projectionParams : ParameterExpression list) (source:IWithSqlService) = - let (|OptionalOuterJoin|) e = - match e with - | MethodCall(None, (!!), [inner]) -> (true,inner) - | _ -> (false,e) match inExp with | MethodCall(None, (MethodWithName "SelectMany"), [ createRelated ; OptionalQuote (Lambda([_], inner)); OptionalQuote (Lambda(projectionParams,_)) ]) -> let outExp = processSelectManys projectionParams.[0].Name createRelated outExp projectionParams source @@ -466,7 +468,6 @@ module internal QueryImplementation = let outExp = processSelectManys projectionParams.[0].Name createRelated outExp projectionParams source let ty, data, sourceEntityName = parseGroupBy meth source sourceAlias destEntity [lambda] exp "" SelectMany(sourceEntityName,destEntity,GroupQuery(data), outExp) - | MethodCall(None, (MethodWithName "Join"), [createRelated ConvertOrTypeAs(MethodCall(Some(Lambda(_,MethodCall(_,MethodWithName "CreateEntities",[String destEntity]))),(MethodWithName "Invoke"),_)) @@ -537,8 +538,8 @@ module internal QueryImplementation = ForeignTable = {Schema="";Name="";Type=""}; OuterJoin = false; RelDirection = RelationshipDirection.Parents } SelectMany(sourceAlias,destAlias,LinkQuery(data),outExp) - | OptionalConvertOrTypeAs(MethodCall(Some(Lambda([_],MethodCall(_,MethodWithName "CreateEntities",[String destEntity]))),(MethodWithName "Invoke"),_)) - | OptionalConvertOrTypeAs(MethodCall(_, MethodWithName "CreateEntities",[String destEntity])) -> + | OptionalOuterJoin(isOuter, OptionalConvertOrTypeAs(MethodCall(Some(Lambda([_],MethodCall(_,MethodWithName "CreateEntities",[String destEntity]))),(MethodWithName "Invoke"),_))) + | OptionalOuterJoin(isOuter, OptionalConvertOrTypeAs(MethodCall(_, MethodWithName "CreateEntities",[String destEntity]))) -> let sourceAlias = if source.TupleIndex.Contains projectionParams.[0].Name then projectionParams.[0].Name else @@ -644,15 +645,16 @@ module internal QueryImplementation = OptionalQuote (Lambda([ParamName sourceAlias],SqlColumnGet(sourceTi,sourceKey,_))) OptionalQuote (Lambda([ParamName destAlias],SqlColumnGet(_,destKey,_))) OptionalQuote projection ]) -> - let destEntity = + let destEntity, isOuter = match dest.SqlExpression with - | BaseTable(_,destEntity) -> destEntity + | BaseTable(_,destEntity) -> destEntity, false + | Projection(MethodCall(None, MethodWithName("Select"), [_ ; OptionalQuote (Lambda(_,MethodCall(None, (MethodWithName "leftJoin"),_)))]),BaseTable(_,destEntity)) -> destEntity, true | _ -> failwithf "Unexpected join destination entity expression (%A)." dest.SqlExpression let sqlExpression = match source.SqlExpression with | BaseTable(alias,entity) when alias = "" -> // special case here as above - this is the first call so replace the top of the tree here with the current base table alias and the select many - let data = { PrimaryKey = [destKey]; PrimaryTable = destEntity; ForeignKey = [sourceKey]; ForeignTable = entity; OuterJoin = false; RelDirection = RelationshipDirection.Parents} + let data = { PrimaryKey = [destKey]; PrimaryTable = destEntity; ForeignKey = [sourceKey]; ForeignTable = entity; OuterJoin = isOuter; RelDirection = RelationshipDirection.Parents} if source.TupleIndex.Any(fun v -> v = sourceAlias) |> not then source.TupleIndex.Add(sourceAlias) if source.TupleIndex.Any(fun v -> v = destAlias) |> not then source.TupleIndex.Add(destAlias) SelectMany(sourceAlias,destAlias, LinkQuery(data),BaseTable(sourceAlias,entity)) @@ -664,7 +666,7 @@ module internal QueryImplementation = // it's ok though because it can always be resolved later after the whole expression tree has been evaluated let data = { PrimaryKey = [destKey]; PrimaryTable = destEntity; ForeignKey = [sourceKey]; ForeignTable = {Schema="";Name="";Type=""}; - OuterJoin = false; RelDirection = RelationshipDirection.Parents } + OuterJoin = isOuter; RelDirection = RelationshipDirection.Parents } SelectMany(sourceAlias,destAlias,LinkQuery(data),source.SqlExpression) let ty = @@ -678,9 +680,10 @@ module internal QueryImplementation = OptionalQuote (Lambda([ParamName sourceAlias],TupleSqlColumnsGet(multisource))) OptionalQuote (Lambda([ParamName destAlias],TupleSqlColumnsGet(multidest))) OptionalQuote projection ]) -> - let destEntity = + let destEntity, isOuter = match dest.SqlExpression with - | BaseTable(_,destEntity) -> destEntity + | BaseTable(_,destEntity) -> destEntity, false + | Projection(MethodCall(None, MethodWithName("Select"), [_ ;OptionalQuote (Lambda(_,MethodCall(None, (MethodWithName "leftJoin"),_)))]),BaseTable(_,destEntity)) -> destEntity, true | _ -> failwithf "Unexpected join destination entity expression (%A)." dest.SqlExpression let destKeys = multidest |> List.map(fun(_,dest,_)->dest) let sourceKeys = multisource |> List.map(fun(_,source,_)->source) @@ -688,7 +691,7 @@ module internal QueryImplementation = match source.SqlExpression with | BaseTable(alias,entity) when alias = "" -> // special case here as above - this is the first call so replace the top of the tree here with the current base table alias and the select many - let data = { PrimaryKey = destKeys; PrimaryTable = destEntity; ForeignKey = sourceKeys; ForeignTable = entity; OuterJoin = false; RelDirection = RelationshipDirection.Parents} + let data = { PrimaryKey = destKeys; PrimaryTable = destEntity; ForeignKey = sourceKeys; ForeignTable = entity; OuterJoin = isOuter; RelDirection = RelationshipDirection.Parents} if source.TupleIndex.Any(fun v -> v = sourceAlias) |> not then source.TupleIndex.Add(sourceAlias) if source.TupleIndex.Any(fun v -> v = destAlias) |> not then source.TupleIndex.Add(destAlias) SelectMany(sourceAlias,destAlias, LinkQuery(data),BaseTable(sourceAlias,entity)) @@ -701,7 +704,7 @@ module internal QueryImplementation = // it's ok though because it can always be resolved later after the whole expression tree has been evaluated let data = { PrimaryKey = destKeys; PrimaryTable = destEntity; ForeignKey = sourceKeys; ForeignTable = {Schema="";Name="";Type=""}; - OuterJoin = false; RelDirection = RelationshipDirection.Parents } + OuterJoin = isOuter; RelDirection = RelationshipDirection.Parents } SelectMany(sourceAlias,destAlias,LinkQuery(data),source.SqlExpression) let ty = diff --git a/tests/SqlProvider.Tests/QueryTests.fs b/tests/SqlProvider.Tests/QueryTests.fs index 1f28b6818..cae56ee2e 100644 --- a/tests/SqlProvider.Tests/QueryTests.fs +++ b/tests/SqlProvider.Tests/QueryTests.fs @@ -1560,6 +1560,26 @@ let ``simple async sum with option operations``() = } |> Seq.sumAsync |> Async.RunSynchronously Assert.That(qry, Is.EqualTo(603221955M).Within(10M)) +[] +let ``simple select query with left join``() = + let dc = sqlOption.GetDataContext() + let qry = + query { + for cust in dc.Main.Customers do + join order in (!!) dc.Main.Orders on (cust.CustomerId = order.OrderId.ToString()) + select (cust.CustomerId, order.OrderDate) + } |> Seq.toArray + + CollectionAssert.IsNotEmpty qry + CollectionAssert.AreEquivalent( + [| + "VINET", None + "TOMSP", None + "HANAR", None + "VICTE", None + |], qry.[0..3]) + + [] let ``simple math operations query``() = From daeb5f9814f3f989e276040f7685ef4596b38f68 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sat, 22 Dec 2018 14:38:52 +0000 Subject: [PATCH 2/3] fix for unit test --- tests/SqlProvider.Tests/QueryTests.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/SqlProvider.Tests/QueryTests.fs b/tests/SqlProvider.Tests/QueryTests.fs index cae56ee2e..34c5fcf2d 100644 --- a/tests/SqlProvider.Tests/QueryTests.fs +++ b/tests/SqlProvider.Tests/QueryTests.fs @@ -1573,10 +1573,10 @@ let ``simple select query with left join``() = CollectionAssert.IsNotEmpty qry CollectionAssert.AreEquivalent( [| - "VINET", None - "TOMSP", None - "HANAR", None - "VICTE", None + "ALFKI", None + "ANATR", None + "ANTON", None + "AROUT", None |], qry.[0..3]) From e0f1515e8134b54d1612100ebc023e1d1922a039 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sat, 22 Dec 2018 16:33:23 +0000 Subject: [PATCH 3/3] unit test fix part 2 --- tests/SqlProvider.Tests/QueryTests.fs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/SqlProvider.Tests/QueryTests.fs b/tests/SqlProvider.Tests/QueryTests.fs index 34c5fcf2d..1e3f25a92 100644 --- a/tests/SqlProvider.Tests/QueryTests.fs +++ b/tests/SqlProvider.Tests/QueryTests.fs @@ -1571,15 +1571,7 @@ let ``simple select query with left join``() = } |> Seq.toArray CollectionAssert.IsNotEmpty qry - CollectionAssert.AreEquivalent( - [| - "ALFKI", None - "ANATR", None - "ANTON", None - "AROUT", None - |], qry.[0..3]) - - + Assert.AreEqual(91, qry.Length) [] let ``simple math operations query``() =