Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

leftOuterJoin and `.DefaultIfEmpty()` queries in F#

Tags:

f#

I have a relatively simple F# query expression with a join:

let mdrQuery = 
    query {
        for header in db.CustomerDetails do
        leftOuterJoin row in db.MDR_0916
            on (header.PID = row.PID) into result
        select (result, header)
        } 

This returns every header and result but for a header that has not match in row, result is just an empty sequence and, when the query results are passed to a custom Type, I get an error that a constructor associated with a field in row is not defined. This makes sense as for any header that has no match in row, a null sequence is returned. An example:

mdrQuery |> Seq.head;;
val it :
  seq<dbSchema.ServiceTypes.MDR_0916> * dbSchema.ServiceTypes.CustomerDetails
= (seq [null], CustomerDetails {ACCOUNTMANAGER = null;
                            ACCOUNTSTATUS = "XC";
                            ADDRESSLINE1 = null;
                            ADDRESSLINE2 = null;
                            ADDRESSLINE3 = null;
                            ADDRESSLINE4 = "123 PIG ROAD"...

I suspect that there's a way around this because of the leftOuterJoin documentation here. But, when I try using that example as a template for my query:

let mdrQuery = 
    query {
        for header in db.CustomerDetails do
        leftOuterJoin row in db.MDR_0916
            on (header.PID = row.PID) into result
        for row in result.DefaultIfEmpty() do
        select (result, header)
        }

the .DefaultIfEmpty() piece errors out with

error FS0039: The field, constructor or member 'DefaultIfEmpty' is not defined

Is there a way that I can make this join happen and select every row, filling the unmatched rows in result with None (or some other null SQL null value) so that the entirety of the query can be passed to my record type?

Ideally, the output for an unmatched row would be something like (truncated results created by hand below)

mdrQuery |> Seq.head;;
val it :
  seq<dbSchema.ServiceTypes.MDR_0916> * dbSchema.ServiceTypes.CustomerDetails
= (MDR_0916 {AIMExp = null;
         AP = null;
         APComp = null;
         APEng = null;
         APFine = null;
         APForl = null;...}, 
CustomerDetails {ACCOUNTMANAGER = null;
                            ACCOUNTSTATUS = "XC";
                            ADDRESSLINE1 = null;
                            ADDRESSLINE2 = null;
                            ADDRESSLINE3 = null;
                            ADDRESSLINE4 = "123 PIG ROAD"...

Edit: This question/answer is similar to mine but including ToOption result simply outputs a Some (seq [null]).

like image 326
Steven Avatar asked Nov 08 '22 07:11

Steven


1 Answers

The documentation is wrong; in C# there is no direct equivalent of a leftOuterJoin operator, so DefaultIfEmpty is used with a normal join, but in F# you don't need this (the query builder performs this translation for you - see QueryBuilder.LeftOuterJoin in the source if you're curious).

If you want the results of a traditional left join, then just add the additional for loop without DefaultIfEmpty (but note - you want to select the newly bound row value, not the result sequence):

let mdrQuery = 
    query {
        for header in db.CustomerDetails do
        leftOuterJoin row in db.MDR_0916
            on (header.PID = row.PID) into result
        for row in result do
        select (row, header)
    }

Note that this will give you null values for MDR_0916 entries that were missing, not special MDR_0916 values with null field values, though, so you might want to apply a post-processing step if you need the latter.

like image 62
kvb Avatar answered Jan 04 '23 03:01

kvb