Skip to content

Commit db5419d

Browse files
committed
* MapTo support Option and ValueOption on target types
* TemplateAsRecord to generate list of properies and types
1 parent f51bd75 commit db5419d

7 files changed

Lines changed: 176 additions & 19 deletions

File tree

docs/RELEASE_NOTES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
### 1.5.8 - 04.06.2025
2+
* MapTo support Option and ValueOption on target types
3+
* TemplateAsRecord to generate list of properies and types
4+
5+
### 1.5.7 - 13.05.2025
6+
* Performance improvement: ProvidedTypes update
7+
18
### 1.5.6 - 06.05.2025
29
* Mock-database case sensitivity backup search
310

docs/content/core/mappers.fsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@ type sql = SqlDataProvider<
2020

2121
(**
2222
23-
## Adding a Mapper using dataContext to use generated types from db
23+
## Adding a Mapper using dataContext to use generated types from the DB
2424
25-
Typically F# is about writing business logic and not about OR-mapping. Consider using your database types as is. But sometimes you want to
26-
map objects to different, for example to interact with other languages like C# domain.
27-
28-
This mapper will ensure that you always sync your types with those you receive from your DB.
25+
Typically, F# is about writing business logic and not about OR-mapping. Consider using your database types as is. And select only the columns you need, not full entities. But sometimes you want to
26+
map objects to different ones, for example to interact with other languages like C# domain.
2927
3028
First, add a Domain Model
3129
@@ -41,7 +39,7 @@ type Employee = {
4139
}
4240

4341
(**
44-
Then you can create the mapper using dataContext to use generated types from db
42+
Then you can create the mapper using dataContext to use the generated types from the DB
4543
*)
4644

4745
let mapEmployee (dbRecord:sql.dataContext.``main.EmployeesEntity``) : Employee =
@@ -51,7 +49,25 @@ let mapEmployee (dbRecord:sql.dataContext.``main.EmployeesEntity``) : Employee =
5149
HireDate = dbRecord.HireDate }
5250

5351
(**
54-
SqlProvider also has a `.MapTo<'T>` convenience method:
52+
53+
### TemplateAsRecord
54+
55+
If you want to copy and paste the current SQLProvider objects as separate classes, you can use `TemplateAsRecord` property of your database table:
56+
57+
*)
58+
59+
ctx.Main.Employees.TemplateAsRecord
60+
// intellisense will generate you code that you can copy and paste as template to create your own type:
61+
// ``type MainOrders = { CustomerId : String voption; EmployeeId : Int64 voption; Freight : Decimal voption; OrderDate : DateTime voption; OrderId : Int64; RequiredDate : DateTime voption; ShipAddress : String voption; ShipCity : String voption; ShipCountry : String voption; ShipName : String voption; ShipPostalCode : String voption; ShipRegion : String voption; ShippedDate : DateTime voption }``
62+
63+
(**
64+
65+
This could be useful if you e.g. want to use SQLProvider objects in some reflection based code-generator (because the normal objects are erased).
66+
67+
68+
### MapTo
69+
70+
SqlProvider also has a `.MapTo<'T>` convenience method:
5571
*)
5672

5773

@@ -71,6 +87,9 @@ let qry = query { for row in employees do
7187

7288
(**
7389
The target type can be a record (as in the example) or a class type with properties named as the source columns and with a parameterless setter.
90+
91+
Target will support mapping database nullable fields to Option and ValueOption types automatically.
92+
7493
The target field name can also be different than the column name; in this case, it must be decorated with the MappedColumnAttribute custom attribute:
7594
*)
7695

@@ -88,7 +107,10 @@ let qry2 =
88107

89108

90109
(**
91-
Or alternatively, the ColumnValues from SQLEntity can be used to create a map, with the
110+
111+
### ColumnValues
112+
113+
Or alternatively, the ColumnValues from SQLEntity can be used to create a map, with the
92114
column as a key:
93115
*)
94116

@@ -99,4 +121,3 @@ let rows =
99121

100122
let employees2map = rows |> Seq.map(fun i -> i.ColumnValues |> Map.ofSeq)
101123
let firstNames = employees2map |> Seq.map (fun x -> x.["FirstName"])
102-

src/SQLProvider.Common/SqlRuntime.Common.fs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ type SqlEntity(dc: ISqlDataContext, tableName, columns: ColumnLookup, activeColu
341341
aliasCache.Value.Add(alias,newEntity)
342342
newEntity
343343

344+
/// Maps database entity class to the type provided in generic attribute.
345+
/// You can define more detailed mapping via MappedColumnAttribute or propertyTypeMapping
344346
member x.MapTo<'a>(?propertyTypeMapping : (string * obj) -> obj) =
345347
let typ = typeof<'a>
346348
let propertyTypeMapping = defaultArg propertyTypeMapping snd
@@ -359,15 +361,23 @@ type SqlEntity(dc: ISqlDataContext, tableName, columns: ColumnLookup, activeColu
359361
[|
360362
for prop in fields do
361363
match dataMap.TryGetValue(clean prop) with
362-
| true, data -> yield propertyTypeMapping (prop.Name,data)
364+
| true, null when (prop.PropertyType.Name.StartsWith "FSharpValueOption" || prop.PropertyType.Name.StartsWith "FSharpOption") ->
365+
let cases = FSharpType.GetUnionCases prop.PropertyType
366+
let typedNone = FSharpValue.MakeUnion(cases[0], null)
367+
yield propertyTypeMapping (prop.Name, typedNone)
368+
| true, dataVal -> yield propertyTypeMapping (prop.Name, (Utilities.convertTypes dataVal prop.PropertyType))
363369
| false, _ -> ()
364370
|]
365371
unbox<'a> (ctor(values))
366372
else
367373
let instance = Activator.CreateInstance<'a>()
368374
for prop in typ.GetProperties() do
369375
match dataMap.TryGetValue(clean prop) with
370-
| true, data -> prop.GetSetMethod().Invoke(instance, [|propertyTypeMapping (prop.Name,data)|]) |> ignore
376+
| true, null when (prop.PropertyType.Name.StartsWith "FSharpValueOption" || prop.PropertyType.Name.StartsWith "FSharpOption") ->
377+
let cases = FSharpType.GetUnionCases prop.PropertyType
378+
let typedNone = FSharpValue.MakeUnion(cases[0], null)
379+
prop.GetSetMethod().Invoke(instance, [|propertyTypeMapping (prop.Name, typedNone)|]) |> ignore
380+
| true, dataVal -> prop.GetSetMethod().Invoke(instance, [|propertyTypeMapping (prop.Name, (Utilities.convertTypes dataVal prop.PropertyType))|]) |> ignore
371381
| false, _ -> ()
372382
instance
373383

src/SQLProvider.Common/Utils.fs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ module Utilities =
7878
Type.GetType typename
7979

8080
let rec internal convertTypes (itm:obj) (returnType:Type) =
81+
if not(isNull itm) && Type.(=) (itm.GetType(), returnType) then itm
82+
else
8183
if (returnType.Name.StartsWith("Option") || returnType.Name.StartsWith("FSharpOption")) && returnType.GenericTypeArguments.Length = 1 then
8284
if isNull itm then None |> box
8385
else
@@ -99,7 +101,9 @@ module Utilities =
99101
| :? Char as t -> Option.Some t |> box
100102
| :? DateTimeOffset as t -> Option.Some t |> box
101103
| :? TimeSpan as t -> Option.Some t |> box
102-
| t -> Option.Some t |> box
104+
| t ->
105+
if t.GetType().Name.StartsWith("FSharpOption") then t |> box
106+
else Option.Some t |> box
103107
elif (returnType.Name.StartsWith("ValueOption") || returnType.Name.StartsWith("FSharpValueOption")) && returnType.GenericTypeArguments.Length = 1 then
104108
if isNull itm then ValueNone |> box
105109
else
@@ -121,7 +125,10 @@ module Utilities =
121125
| :? Char as t -> ValueOption.Some t |> box
122126
| :? DateTimeOffset as t -> ValueOption.Some t |> box
123127
| :? TimeSpan as t -> ValueOption.Some t |> box
124-
| t -> ValueOption.Some t |> box
128+
| t ->
129+
if t.GetType().Name.StartsWith("FSharpValueOption") then t|> box
130+
else ValueOption.Some t |> box
131+
125132
elif returnType.Name.StartsWith("Nullable") && returnType.GenericTypeArguments.Length = 1 then
126133
if isNull itm then null |> box
127134
else convertTypes itm (returnType.GenericTypeArguments.[0])

src/SQLProvider.DesignTime/SqlDesignTime.fs

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -930,8 +930,25 @@ module DesignTimeUtils =
930930
e
931931
@@>)
932932

933-
934-
// This genertes a template.
933+
let templateContainer = ProvidedTypeDefinition("TemplateAsRecord", Some typeof<obj>, isErased=true)
934+
let templateCont = ProvidedProperty("TemplateAsRecord", templateContainer, getterCode = empty)
935+
templateCont.AddXmlDocDelayed(fun () -> "As this is erasing TypeProvider, you can use the generated types. However, if you need manual access to corresponding type, e.g. to use it in reflection, this will generate you a template of the runtime type. Copy and paste this to use however you will (e.g. with MapTo).")
936+
templateContainer.AddMemberDelayed(fun () ->
937+
let optType =
938+
match useOptionTypes with
939+
| NullableColumnType.OPTION -> " option"
940+
| NullableColumnType.VALUE_OPTION -> " voption"
941+
| NullableColumnType.NO_OPTION
942+
| _ -> ""
943+
let template=
944+
let items =
945+
columns
946+
|> Seq.toArray
947+
|> Array.map(fun kvp -> (SchemaProjections.nicePascalName kvp.Value.Name) + " : " + (Utilities.getType kvp.Value.TypeMapping.ClrType).Name + (if kvp.Value.IsNullable then optType else ""))
948+
"type " + (SchemaProjections.nicePascalName key) + " = { " + (String.concat "; " items) + " }"
949+
ProvidedProperty(template, typeof<unit>, getterCode = empty) :> MemberInfo
950+
)
951+
serviceType.AddMember templateContainer
935952

936953
seq {
937954
if not (ct.DeclaredProperties |> Seq.exists(fun m -> m.Name = "Individuals")) then
@@ -956,6 +973,7 @@ module DesignTimeUtils =
956973
backwardCompatibilityOnly.Length <> minimalParameters.Length then
957974
create4old.AddXmlDoc("This will be obsolete soon. Migrate away from this!")
958975
yield create4old :> MemberInfo
976+
yield templateCont :> MemberInfo
959977

960978
} |> Seq.toList
961979
)
@@ -1071,6 +1089,76 @@ module DesignTimeUtils =
10711089
| Some con -> if (dbVendor <> DatabaseProviderTypes.MSACCESS) && con.State <> ConnectionState.Closed then con.Close()
10721090
| None -> ()
10731091

1092+
let funcType =
1093+
typedefof<FSharpFunc<_, _>>.MakeGenericType(serviceType, typedefof<System.Threading.Tasks.Task>)
1094+
let wtr = ProvidedMethod("WithTransaction", [ ProvidedParameter("func", funcType)], typedefof<System.Threading.Tasks.Task>, invokeCode = fun args ->
1095+
let a0 = args.[0]
1096+
let func = args.[1]
1097+
let isMono = not(isNull (Type.GetType "Mono.Runtime"))
1098+
<@@
1099+
task {
1100+
use scope =
1101+
match isMono with
1102+
| true -> Unchecked.defaultof<Transactions.TransactionScope> // new Transactions.TransactionScope()
1103+
| false ->
1104+
// Note1: On Mono, 4.6.1 or newer is requred for compiling TransactionScopeAsyncFlowOption.
1105+
// Note2: We should here also set the transaction option isolation level.
1106+
new Transactions.TransactionScope(System.Transactions.TransactionScopeAsyncFlowOption.Enabled)
1107+
// new Transactions.TransactionScope(
1108+
// Transactions.TransactionScopeOption.Required,
1109+
// new Transactions.TransactionOptions(
1110+
// IsolationLevel = Transactions.IsolationLevel.RepeatableRead),
1111+
// System.Transactions.TransactionScopeAsyncFlowOption.Enabled)
1112+
1113+
let! res = ((%%func : obj) :?> FSharpFunc<ISqlDataContext,Task<_>>) ((%%a0 : obj) :?> ISqlDataContext)
1114+
1115+
if not (isMono || isNull scope) then
1116+
try
1117+
scope.Complete()
1118+
scope.Dispose()
1119+
with
1120+
| :? ObjectDisposedException -> ()
1121+
return res
1122+
}
1123+
@@> )
1124+
wtr.AddXmlDoc ("Write operations can be wrapped into a single transaction with this. Try to avoid transactions taking possibly seconds.")
1125+
rootType.AddMember wtr
1126+
1127+
let funcTypeA =
1128+
typedefof<FSharpFunc<_, _>>.MakeGenericType(serviceType, typedefof<Async>)
1129+
let wtra = ProvidedMethod("WithTransactionAsync", [ ProvidedParameter("func", funcTypeA)], typedefof<Async>, invokeCode = fun args ->
1130+
let a0 = args.[0]
1131+
let func = args.[1]
1132+
<@@
1133+
task {
1134+
let isMono = not(isNull (Type.GetType "Mono.Runtime"))
1135+
use scope =
1136+
match isMono with
1137+
| true -> Unchecked.defaultof<Transactions.TransactionScope> // new Transactions.TransactionScope()
1138+
| false ->
1139+
// Note1: On Mono, 4.6.1 or newer is requred for compiling TransactionScopeAsyncFlowOption.
1140+
// Note2: We should here also set the transaction option isolation level.
1141+
new Transactions.TransactionScope(System.Transactions.TransactionScopeAsyncFlowOption.Enabled)
1142+
// new Transactions.TransactionScope(
1143+
// Transactions.TransactionScopeOption.Required,
1144+
// new Transactions.TransactionOptions(
1145+
// IsolationLevel = Transactions.IsolationLevel.RepeatableRead),
1146+
// System.Transactions.TransactionScopeAsyncFlowOption.Enabled)
1147+
1148+
let! res = ((%%func : obj) :?> FSharpFunc<ISqlDataContext,Async<_>>) ((%%a0 : obj) :?> ISqlDataContext)
1149+
1150+
if not (isMono || isNull scope) then
1151+
try
1152+
scope.Complete()
1153+
scope.Dispose()
1154+
with
1155+
| :? ObjectDisposedException -> ()
1156+
return res
1157+
}
1158+
@@> )
1159+
wtra.AddXmlDoc ("Write operations can be wrapped into a single transaction with this. Try to avoid transactions taking possibly seconds.")
1160+
rootType.AddMember wtra
1161+
10741162
()
10751163

10761164
let createConstructors (config:TypeProviderConfig) (rootType:ProvidedTypeDefinition, serviceType, readServiceType, args) =
@@ -1091,7 +1179,7 @@ module DesignTimeUtils =
10911179

10921180
let customResPath =
10931181
"resolutionPath",
1094-
"The location to look for dynamically loaded assemblies containing database vendor specific connections and custom types. Types used in desing-time: If no better clue, prefer .NET Standard 2.0 versions. Semicolon to separate multiple.",
1182+
"The location to look for dynamically loaded assemblies containing database vendor specific connections and custom types. Types used in design-time: If no better clue, prefer .NET Standard 2.0 versions. Semicolon to separate multiple.",
10951183
typeof<string>
10961184

10971185
let customTransOpts =
@@ -1447,7 +1535,7 @@ type public SqlTypeProvider(config: TypeProviderConfig) as this =
14471535
<param name='DatabaseVendor'> The target database vendor</param>
14481536
<param name='IndividualsAmount'>The amount of sample entities to project into the type system for each SQL entity type. Default 50. Note GDPR/PII regulations if using individuals with ContextSchemaPath.</param>
14491537
<param name='UseOptionTypes'>If set, F# option types will be used in place of nullable database columns. If not, you will always receive the default value of the column's type even if it is null in the database.</param>
1450-
<param name='ResolutionPath'>The location to look for dynamically loaded assemblies containing database vendor specific connections and custom types. Types used in desing-time: If no better clue, prefer .NET Standard 2.0 versions. Semicolon to separate multiple.</param>
1538+
<param name='ResolutionPath'>The location to look for dynamically loaded assemblies containing database vendor specific connections and custom types. Types used in design-time: If no better clue, prefer .NET Standard 2.0 versions. Semicolon to separate multiple.</param>
14511539
<param name='Owner'>Oracle: The owner of the schema for this provider to resolve. PostgreSQL: A list of schemas to resolve, separated by spaces, newlines, commas, or semicolons.</param>
14521540
<param name='CaseSensitivityChange'>Should we do ToUpper or ToLower when generating table names?</param>
14531541
<param name='TableNames'>Comma separated table names list to limit a number of tables in big instances. The names can have '%' sign to handle it as in the 'LIKE' query (Oracle and MSSQL Only)</param>

src/SQLProvider.Runtime/Providers.MsSqlServer.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ module MSSqlServer =
8080
findClrType <- clrMappings.TryFind
8181
findDbType <- dbMappings.TryFind
8282

83-
let checkIfMsSqlAssemblyIsDesingOnly (assembly:System.Reflection.Assembly)=
83+
let checkIfMsSqlAssemblyIsDesignOnly (assembly:System.Reflection.Assembly)=
8484
try
8585
let metaData =
8686
assembly.GetCustomAttributes(typeof<System.Reflection.AssemblyMetadataAttribute>, false)

tests/SqlProvider.Tests/QueryTests.fs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2928,5 +2928,29 @@ let ``mock for unit-testing: datacontext``() =
29282928

29292929
Assert.AreEqual(2, res.Length)
29302930
()
2931-
29322931

2932+
2933+
// Generated with dc.Main.OrderDetails.TemplateAsRecord
2934+
type MyMainOrders = { CustomerId : String voption; EmployeeId : Int64 voption; Freight : Decimal voption; OrderDate : DateTime voption; OrderId : Int64; RequiredDate : DateTime voption; ShipAddress : String voption; ShipCity : String voption; ShipCountry : String voption; ShipName : String voption; ShipPostalCode : String voption; ShipRegion : String voption; ShippedDate : DateTime voption }
2935+
2936+
type MyMainOrders2() =
2937+
class
2938+
member val CustomerId = ValueSome "" with get,set
2939+
member val ShipRegion = ValueSome "" with get,set
2940+
end
2941+
2942+
2943+
[<Test >]
2944+
let ``simple select query with MapTo``() =
2945+
let dc = sqlValueOption.GetDataContext()
2946+
2947+
let qry =
2948+
query {
2949+
for ord in dc.Main.Orders do
2950+
select (ord)
2951+
} |> Seq.head
2952+
let mapped1 = qry.MapTo<MyMainOrders>()
2953+
let mapped2 = qry.MapTo<MyMainOrders2>()
2954+
2955+
Assert.AreEqual(ValueSome "VINET", mapped1.CustomerId)
2956+
Assert.AreEqual(ValueSome "VINET", mapped2.CustomerId)

0 commit comments

Comments
 (0)