One of the benefits of the type system on F# is avoid a null exception... or that was something I believe... because I'm getting a null problem:
[<CLIMutable>]
type Customer = {
[<AutoIncrement>] id:option<int64>
code:string
name:string
}
I'm running a SQL code:
let SqlFTS<'T>(table:string, searchTable:string, query:string) =
use db = openDb()
let sql = sprintf "SELECT * FROM %s WHERE id in (SELECT docid FROM %s WHERE data MATCH %A)" table searchTable query
printfn "%A" sql
db.SqlList<'T>(sql) |> Seq.toArray
testCase "Customers" <|
fun _ ->
let rows = GenData.genCustomers(50)
Customers.insert(rows)
isEqual "Failed to insert" 50L (DB.SqlCount<Customers.Customer>())
//Until here, it works
let c = Customers.byId(1L)
printfn "%A" c
//Customers.byId return me a record with all the properties as NULLS!
//Then c.name is null, and the code above fail.
let rows = Customers.searchCustomers(c.name)
This was very unexpected. Why I can get a record with all values to null?
Let's start with line 1:
[<CLIMutable>]
The docs for CLIMutable state
Adding this attribute to a record type causes it to be compiled to a Common Language Infrastructure (CLI) representation with a default constructor with property getters and setters.
That default constructor means the fields will be initialized to default values. The default value for string is null. That's valid to the CLR and it's valid to C# and VB.NET. You may not be able to call the default ctor from F#, but pretty much anyone else can.
Welcome to interop; it can be a pain.
Related
My research on this seems to conclude a bug in dapper, but I'm hoppign I'm missing something simple.
I have a union type
type PrimaryKey<'x> =
| Id of int
| EmptyPrimaryKey
And I want to be able to map that to int columns in SQL.
My custom type handler is below:
type PrimaryKeyHandler<'X>() =
inherit SqlMapper.TypeHandler<PrimaryKey<'X>>()
override _.SetValue(param, value) =
printfn "Running set value"
let valueOrNull =
match value with
| PrimaryKey.Id id ->
box id
| EmptyPrimaryKey ->
null
param.Value <- valueOrNull
override _.Parse value =
if isNull value || value = box DBNull.Value
then EmptyPrimaryKey
else Id (value :?> int)
_.Parse is working correctly when I'm using Select queries, so all good there.
However when I'm trying to Insert a value, .SetValue does not seem to be being called at all and the program is dying with the type cannot be used as a parameter value
And when dumping out the values that's going into the query, it is indeed a PrimaryKey rather than an integer. And no printfn statements or breakpoints are being hit.
This doesn't seem to be a super uncommon problem but I haven't found a clear solution yet.
Getting into the bowels now. Hopefully this is the last time I have to deal with Reflection for a while and I can return to the high level where I belong.
I have a type "PrimaryKey" defined as such.
type PrimaryKey<'x> =
| Id of int
| EmptyPrimaryKey
Then a bunch of record types associated with database tables, for example:
type User = {
user_id: PrimaryKey<User>
username: string
email: string
address_id: PrimaryKey<Address>
} with
static member DatabaseTable = "users"
I have written a custom type handler for Dapper to handle the Primary Key type.
type PrimaryKeyHandler<'X>() =
inherit SqlMapper.TypeHandler<PrimaryKey<'X>>()
(* I don't think this works but that's a future problem *)
override _.SetValue(param, value) =
let valueOrNull =
match value with
| PrimaryKey.Id id -> box id
| EmptyPrimaryKey -> null
param.Value <- valueOrNull
override _.Parse value =
if isNull value || value = box DBNull.Value
then EmptyPrimaryKey
else Id (value :?> int)
Now the problem with that is I have to call::
SqlMapper.AddTypeHandler (PrimaryKeyHandler<User>())
SqlMapper.AddTypeHandler (PrimaryKeyHandler<OtherRecordType>())
On every record that has a primary key. I'm basing my solution off of this: Dapper generic typehandler for F# Union types
But I don't understand it enough to adapt it to my needs, I think I need extra handling for the generic type.
What I've started with is this:
let RegisterTypeHandlers () =
let assembly = Assembly.GetCallingAssembly()
let handler = typedefof<PrimaryKeyHandler<_>>
assembly.GetTypes()
|> Seq.filter(fun t ->
FSharpType.IsRecord(t) && t.GetProperty("DatabaseTable") <> null
)
Which successfully returns a list of record types which have database table associations.
However trying to iterate over that list and call AddTypeHandler on all of those types fails:
let RegisterTypeHandlers () =
let assembly = Assembly.GetCallingAssembly()
let handler = typedefof<PrimaryKeyHandler<_>>
assembly.GetTypes()
|> Seq.filter(fun t ->
FSharpType.IsRecord(t) && t.GetProperty("DatabaseTable") <> null
)
|> Seq.iter(fun t ->
printfn $"Type: {t.Name}"
let ctor = handler
.MakeGenericType(t)
.GetConstructor(Array.empty)
.Invoke(Array.empty)
(typeof<SqlMapper>.GetMethods()
|> Seq.filter(fun methodInfo ->
if methodInfo.Name = "AddTypeHandler" && methodInfo.IsGenericMethodDefinition then
let gp = methodInfo.GetParameters()
not <| isNull gp && gp.Length = 1 && gp.[0].ParameterType.Name.Contains("TypeHandler")
else false)
|> Seq.head)
.MakeGenericMethod(t)
.Invoke(null, [| ctor |]) |> ignore
)
The error being
Unhandled exception. System.ArgumentException: Object of type 'MyModule.Common+PrimaryKeyHandler`1[Program+User]' cannot be converted to type 'Dapper.SqlMapper+TypeHandler`1[Program+User]'.
I've been looking at some of the GenericType functions in reflection but not really sure where to go from here.
First of all, you are getting an error saying that PrimaryKeyHandler<User> cannot be converted to type TypeHandler<User>. This is correct, because your type PrimaryKeyHandler<User> inherits from TypeHandler<PrimaryKey<User>>.
I think this happens because you get the AddTypeHandler method via reflection and then use MakeGenericMethod(t) to make it generic - but if t is User, then you get the wrong generic instantiation - you need to wrap t with PrimaryKey<..> around it first.
I have not tested this, but I think the following should work:
let addTyMi =
typeof<SqlMapper>.GetMethods()
|> Seq.find(fun methodInfo ->
if methodInfo.Name = "AddTypeHandler" &&
methodInfo.IsGenericMethodDefinition then
let gp = methodInfo.GetParameters()
not <| isNull gp && gp.Length = 1 &&
gp.[0].ParameterType.Name.Contains("TypeHandler")
else false)
let pkt = typedefof<PrimaryKey<_>>.MakeGenericType(t)
addTyMi.MakeGenericMethod(pkt).Invoke(null, [| ctor |]) |> ignore
It seems to me that there is also a non-generic overload of AddTypeHandler taking System.Type (by browsing GitHub source - I have not tried this). Maybe you could do just:
let pkt = typedefof<PrimaryKey<_>>.MakeGenericType(t)
SqlMapper.AddTypeHandler(pkt, ctor)
...avoiding some of the reflection. Also, ctor is a bad name, because the variable refers to the instance!
I'm making, for demo purposes, an Insert function that I would like to return a variable of the type that is passed in.
I'm user Dapper.fsharp for the initial query, then I'd like to run a raw SQL query to get the last inserted value.
So I have something like this for demo purposes
let Insert<'a> asyncQuery =
asyncQuery
|> connection.InsertAsync<'a>
|> RunSynchronously
|> ignore
(*This is a Dapper.fsharp query. Running this returns the number of rows inserted (int) *)
let table = asyncQuery.Table (*This is a string*)
let result =
connection.Query<'a>($"""Select * From {table} Where id = (select last_insert_id())""") (*Should return an IENumerable of type 'a*)
|> EnumerableToArray
result |> first
And then that's called like
let newSession =
insert {
table "sessions"
value newSession
} |> Insert
where newSession is of type session
module Session
type session = {id: int; session_id: string; clerk_json: string; clerk_id: int; expires: int}
(*This is also the structure of the SQL table exactly*)
The error I get is
"A parameterless default constructor or one matching signature (System.Int32 id, System.String session_id, System.Int32 clerk_id, System.String clerk_json, System.Int32 expires) is required for Session+session materialization"
Which indicates to me that it's not getting the right type signature from the database, but the column names and type match, and nothing in the table is null.
Maybe I'm overlooking something simple or misunderstanding how the library should be used?
The type has to be [<CLIMutable>]...
Trying to understand how to parameterize query with SqlCommandProvider. So far I've got this
[<Literal>]
let connectionString = #"Data Source=.\SQL2019;Initial Catalog=MyDb;Trusted_Connection=True;"
type InsertCommand =
SqlCommandProvider<"INSERT INTO Category (Name, Value, Website) VALUES (#name, #value, #website)", connectionString, SingleRow = true>
let cmdInsert = new InsertCommand()
let insertCategories (data: seq<string * string>) =
data
|> Seq.iter
(
fun x ->
cmdInsert.Execute(name = fst x, value = Int32.Parse(snd x), website = "www.example.com") |> ignore
)
But on the line new InsertCommand() I'm getting this error
The member or object constructor 'SqlCommandProvider,CommandText="...",ConnectionStringOrName="...",SingleRow="True"'
does not take 0 argument(s). An overload was found taking 2 arguments
Can someone please explain? If not, can someone please give an example on how to do parameterized insert query?
The error message is telling you that the you are missing an argument for the constructor of the provided type, i.e. your InsertCommand type. I do not have a SQL database to try this, but the type provided by SqlCommandProvider should have an overload that takes a connection string and (optionally) a timeout.
The following should do the trick:
let cmdInsert = new InsertCommand(connectionString)
I have a record type which occurs quite often in a nested complex data structure. Because the record type has an automatically generated ToString the ToString of my bigger structure becomes way to confusing and I do not care for the string representation of my record.
So I want to have an empty string as representation for my record. Overriding ToString seems to not do anything, using StructuredFormatDisplay does not work with empty strings since it requires an input of the form "Text {Field} Text". Right now I have
[<StructuredFormatDisplay("{}")>]
type MyRecord
{ 5 fields... }
override __.ToString () = ""
But this results in The method MyRecord.ToString could not be found.
So what is the correct way to not have a string representation for a record type?
The comments all provide correct information about how to achieve your goal. Pulling it all together, here's what I would do in a real-world scenario where I wanted a record type to always have the empty string as its string representation:
open System
[<StructuredFormatDisplay("{StringDisplay}")>]
type MyRecord =
{
A: int
B: string
C: decimal
D: DateTime
E: Guid
}
member __.StringDisplay = String.Empty
override this.ToString () = this.StringDisplay
This way, regardless of what technique is used to print the record, or if its ToString method is used by an external caller, the representation will always be the same:
let record = {A = 3; B = "Test"; C = 5.6M; D = DateTime.Now; E = Guid.NewGuid()}
printfn "Structured Format Display: %A" record
printfn "Implicit ToString Call: %O" record
printfn "Explicit ToString Call: %s" <| record.ToString()
This prints:
Structured Format Display:
Implicit ToString Call:
Explicit ToString Call:
One thing to keep in mind is that this will even override the way the record is displayed by F# interactive. Meaning, the record evaluation itself now shows up as:
val record : MyRecord =