Skip to content

Commit 15cf3ce

Browse files
authored
Merge pull request #1611 from fsprojects/copilot/sub-pr-1609
[WIP] Add DateOnly and TimeOnly inference support
2 parents 15de10c + 2063b64 commit 15cf3ce

10 files changed

Lines changed: 209 additions & 51 deletions

File tree

src/FSharp.Data.DesignTime/CommonProviderImplementation/Helpers.fs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,17 @@ module internal ProviderHelpers =
150150
member x.Inverse(denominator) : Type =
151151
ProvidedMeasureBuilder.Inverse(denominator) }
152152

153+
#if NET6_0_OR_GREATER
154+
/// Returns true when the target runtime assembly is a .NET 6+ build and therefore
155+
/// supports System.DateOnly / System.TimeOnly in generated types.
156+
let runtimeSupportsNet6Types (runtimeAssemblyPath: string) =
157+
// The assembly path contains the TFM, e.g. "…/net8.0/FSharp.Data.dll".
158+
// Anything matching "/net<N>." where N ≥ 6 is a net6+ target.
159+
let path = runtimeAssemblyPath.Replace('\\', '/').ToLowerInvariant()
160+
let m = System.Text.RegularExpressions.Regex.Match(path, @"/net(\d+)\.")
161+
m.Success && (int m.Groups.[1].Value) >= 6
162+
#endif
163+
153164
let asyncMap (resultType: Type) (valueAsync: Expr<Async<'T>>) (body: Expr<'T> -> Expr) =
154165
let (?) = QuotationBuilder.(?)
155166
let convFunc = ReflectionHelpers.makeDelegate (Expr.Cast >> body) typeof<'T>

src/FSharp.Data.DesignTime/Csv/CsvProvider.fs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,23 @@ type public CsvProvider(cfg: TypeProviderConfig) as this =
103103
let inferredFields =
104104
use _holder = IO.logTime "Inference" sample
105105

106-
sampleCsv.InferColumnTypes(
107-
inferRows,
108-
TextRuntime.GetMissingValues missingValuesStr,
109-
inferenceMode,
110-
TextRuntime.GetCulture cultureStr,
111-
schema,
112-
assumeMissingValues,
113-
preferOptionals,
114-
unitsOfMeasureProvider
115-
)
106+
let fields =
107+
sampleCsv.InferColumnTypes(
108+
inferRows,
109+
TextRuntime.GetMissingValues missingValuesStr,
110+
inferenceMode,
111+
TextRuntime.GetCulture cultureStr,
112+
schema,
113+
assumeMissingValues,
114+
preferOptionals,
115+
unitsOfMeasureProvider
116+
)
117+
#if NET6_0_OR_GREATER
118+
if ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then fields
119+
else fields |> List.map StructuralInference.downgradeNet6PrimitiveProperty
120+
#else
121+
fields
122+
#endif
116123

117124
use _holder = IO.logTime "TypeGeneration" sample
118125

src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ module internal HtmlGenerator =
3737
let private createTableType
3838
getTableTypeName
3939
(inferenceParameters, missingValuesStr, cultureStr)
40+
supportsNet6Types
4041
(table: HtmlTable)
4142
=
4243

43-
let columns =
44+
let rawColumns =
4445
match table.InferedProperties with
4546
| Some inferedProperties -> inferedProperties
4647
| None ->
@@ -52,6 +53,14 @@ module internal HtmlGenerator =
5253
else
5354
table.Rows)
5455

56+
let columns =
57+
#if NET6_0_OR_GREATER
58+
if supportsNet6Types then rawColumns
59+
else rawColumns |> List.map StructuralInference.downgradeNet6PrimitiveProperty
60+
#else
61+
rawColumns
62+
#endif
63+
5564
let fields =
5665
columns
5766
|> List.mapi (fun index field ->
@@ -128,9 +137,17 @@ module internal HtmlGenerator =
128137

129138
create, tableType
130139

131-
let private createListType getListTypeName (inferenceParameters, missingValuesStr, cultureStr) (list: HtmlList) =
140+
let private createListType getListTypeName (inferenceParameters, missingValuesStr, cultureStr) supportsNet6Types (list: HtmlList) =
132141

133-
let columns = HtmlInference.inferListType inferenceParameters list.Values
142+
let rawColumns = HtmlInference.inferListType inferenceParameters list.Values
143+
144+
let columns =
145+
#if NET6_0_OR_GREATER
146+
if supportsNet6Types then rawColumns
147+
else StructuralInference.downgradeNet6Types rawColumns
148+
#else
149+
rawColumns
150+
#endif
134151

135152
let listItemType, conv =
136153
match columns with
@@ -185,14 +202,23 @@ module internal HtmlGenerator =
185202
let private createDefinitionListType
186203
getDefinitionListTypeName
187204
(inferenceParameters, missingValuesStr, cultureStr)
205+
supportsNet6Types
188206
(definitionList: HtmlDefinitionList)
189207
=
190208

191209
let getListTypeName = typeNameGenerator ()
192210

193211
let createListType index (list: HtmlList) =
194212

195-
let columns = HtmlInference.inferListType inferenceParameters list.Values
213+
let rawColumns = HtmlInference.inferListType inferenceParameters list.Values
214+
215+
let columns =
216+
#if NET6_0_OR_GREATER
217+
if supportsNet6Types then rawColumns
218+
else StructuralInference.downgradeNet6Types rawColumns
219+
#else
220+
rawColumns
221+
#endif
196222

197223
let listItemType, conv =
198224
match columns with
@@ -264,7 +290,7 @@ module internal HtmlGenerator =
264290

265291
definitionListType
266292

267-
let generateTypes asm ns typeName parameters htmlObjects =
293+
let generateTypes asm ns typeName parameters supportsNet6Types htmlObjects =
268294

269295
let htmlType =
270296
ProvidedTypeDefinition(
@@ -303,7 +329,7 @@ module internal HtmlGenerator =
303329
match htmlObj with
304330
| Table table ->
305331
let containerType = getOrCreateContainer "Tables"
306-
let create, tableType = createTableType getTypeName parameters table
332+
let create, tableType = createTableType getTypeName parameters supportsNet6Types table
307333
htmlType.AddMember tableType
308334

309335
containerType.AddMember
@@ -315,7 +341,7 @@ module internal HtmlGenerator =
315341

316342
| List list ->
317343
let containerType = getOrCreateContainer "Lists"
318-
let create, tableType = createListType getTypeName parameters list
344+
let create, tableType = createListType getTypeName parameters supportsNet6Types list
319345
htmlType.AddMember tableType
320346

321347
containerType.AddMember
@@ -326,7 +352,7 @@ module internal HtmlGenerator =
326352
)
327353
| DefinitionList definitionList ->
328354
let containerType = getOrCreateContainer "DefinitionLists"
329-
let tableType = createDefinitionListType getTypeName parameters definitionList
355+
let tableType = createDefinitionListType getTypeName parameters supportsNet6Types definitionList
330356
htmlType.AddMember tableType
331357

332358
containerType.AddMember

src/FSharp.Data.DesignTime/Html/HtmlProvider.fs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,15 @@ type public HtmlProvider(cfg: TypeProviderConfig) as this =
6262
PreferOptionals = preferOptionals
6363
InferenceMode = inferenceMode }
6464

65+
#if NET6_0_OR_GREATER
66+
let supportsNet6Types = ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly
67+
#else
68+
let supportsNet6Types = false
69+
#endif
70+
6571
doc
6672
|> HtmlRuntime.getHtmlObjects (Some inferenceParameters) includeLayoutTables
67-
|> HtmlGenerator.generateTypes asm ns typeName (inferenceParameters, missingValuesStr, cultureStr)
73+
|> HtmlGenerator.generateTypes asm ns typeName (inferenceParameters, missingValuesStr, cultureStr) supportsNet6Types
6874

6975
use _holder = IO.logTime "TypeGeneration" sample
7076

src/FSharp.Data.DesignTime/Json/JsonProvider.fs

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -76,28 +76,35 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
7676
let inferedType =
7777
use _holder = IO.logTime "Inference" (if schema <> "" then schema else sample)
7878

79-
if schema <> "" then
80-
// Use the JSON Schema for type inference
81-
use _holder = IO.logTime "SchemaInference" schema
82-
83-
let schemaValue = JsonValue.Parse(value)
84-
let jsonSchema = JsonSchema.parseSchema schemaValue
85-
JsonSchema.schemaToInferedType unitsOfMeasureProvider jsonSchema
86-
else
87-
// Use sample-based inference
88-
let samples =
89-
use _holder = IO.logTime "Parsing" sample
90-
91-
if sampleIsList then
92-
JsonDocument.CreateList(new StringReader(value))
93-
|> Array.map (fun doc -> doc.JsonValue)
94-
else
95-
[| JsonValue.Parse(value) |]
96-
97-
samples
98-
|> Array.map (fun sampleJson ->
99-
JsonInference.inferType unitsOfMeasureProvider inferenceMode cultureInfo "" sampleJson)
100-
|> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top
79+
let rawInfered =
80+
if schema <> "" then
81+
// Use the JSON Schema for type inference
82+
use _holder = IO.logTime "SchemaInference" schema
83+
84+
let schemaValue = JsonValue.Parse(value)
85+
let jsonSchema = JsonSchema.parseSchema schemaValue
86+
JsonSchema.schemaToInferedType unitsOfMeasureProvider jsonSchema
87+
else
88+
// Use sample-based inference
89+
let samples =
90+
use _holder = IO.logTime "Parsing" sample
91+
92+
if sampleIsList then
93+
JsonDocument.CreateList(new StringReader(value))
94+
|> Array.map (fun doc -> doc.JsonValue)
95+
else
96+
[| JsonValue.Parse(value) |]
97+
98+
samples
99+
|> Array.map (fun sampleJson ->
100+
JsonInference.inferType unitsOfMeasureProvider inferenceMode cultureInfo "" sampleJson)
101+
|> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top
102+
#if NET6_0_OR_GREATER
103+
if ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then rawInfered
104+
else StructuralInference.downgradeNet6Types rawInfered
105+
#else
106+
rawInfered
107+
#endif
101108

102109
use _holder = IO.logTime "TypeGeneration" (if schema <> "" then schema else sample)
103110

src/FSharp.Data.DesignTime/Xml/XmlProvider.fs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,13 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
7474
let inferedType =
7575
use _holder = IO.logTime "Inference" sample
7676

77-
schemaSet |> XsdParsing.getElements |> List.ofSeq |> XsdInference.inferElements
77+
let t = schemaSet |> XsdParsing.getElements |> List.ofSeq |> XsdInference.inferElements
78+
#if NET6_0_OR_GREATER
79+
if ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then t
80+
else StructuralInference.downgradeNet6Types t
81+
#else
82+
t
83+
#endif
7884

7985
use _holder = IO.logTime "TypeGeneration" sample
8086

@@ -113,14 +119,21 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
113119
let inferedType =
114120
use _holder = IO.logTime "Inference" sample
115121

116-
samples
117-
|> XmlInference.inferType
118-
unitsOfMeasureProvider
119-
inferenceMode
120-
(TextRuntime.GetCulture cultureStr)
121-
false
122-
globalInference
123-
|> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top
122+
let t =
123+
samples
124+
|> XmlInference.inferType
125+
unitsOfMeasureProvider
126+
inferenceMode
127+
(TextRuntime.GetCulture cultureStr)
128+
false
129+
globalInference
130+
|> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top
131+
#if NET6_0_OR_GREATER
132+
if ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then t
133+
else StructuralInference.downgradeNet6Types t
134+
#else
135+
t
136+
#endif
124137

125138
use _holder = IO.logTime "TypeGeneration" sample
126139

src/FSharp.Data.Runtime.Utilities/StructuralInference.fs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,3 +623,63 @@ let inferPrimitiveType
623623
[<Obsolete("This API will be made internal in a future release. Please file an issue at https://github.com/fsprojects/FSharp.Data/issues/1458 if you need this public.")>]
624624
let getInferedTypeFromString unitsOfMeasureProvider inferenceMode cultureInfo value unit =
625625
inferPrimitiveType unitsOfMeasureProvider inferenceMode cultureInfo value unit
626+
627+
#if NET6_0_OR_GREATER
628+
/// Replaces DateOnly → DateTime and TimeOnly → TimeSpan throughout an InferedType tree.
629+
/// Used in design-time code when the target framework does not support these .NET 6+ types.
630+
let internal downgradeNet6Types (inferedType: InferedType) : InferedType =
631+
let downgradeTag tag =
632+
match tag with
633+
| InferedTypeTag.DateOnly -> InferedTypeTag.DateTime
634+
| InferedTypeTag.TimeOnly -> InferedTypeTag.TimeSpan
635+
| _ -> tag
636+
637+
let downgradeType (typ: Type) =
638+
if typ = typeof<DateOnly> then typeof<DateTime>
639+
elif typ = typeof<TimeOnly> then typeof<TimeSpan>
640+
else typ
641+
642+
// Use reference-equality-based visited set to handle cyclic InferedType graphs
643+
// (e.g. recursive XML schemas). When a cycle is detected we return the original node.
644+
let visited =
645+
System.Collections.Generic.HashSet<InferedType>(
646+
{ new System.Collections.Generic.IEqualityComparer<InferedType> with
647+
member _.Equals(x, y) = obj.ReferenceEquals(x, y)
648+
member _.GetHashCode(x) = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(x) })
649+
650+
let rec convert infType =
651+
if not (visited.Add(infType)) then
652+
infType // cycle detected – return original to avoid infinite recursion
653+
else
654+
let result =
655+
match infType with
656+
| InferedType.Primitive(typ, unit, optional, overrideOnMerge) ->
657+
InferedType.Primitive(downgradeType typ, unit, optional, overrideOnMerge)
658+
| InferedType.Record(name, props, optional) ->
659+
InferedType.Record(name, props |> List.map (fun p -> { p with Type = convert p.Type }), optional)
660+
| InferedType.Collection(order, types) ->
661+
InferedType.Collection(
662+
order |> List.map downgradeTag,
663+
types |> Map.toSeq |> Seq.map (fun (k, (m, t)) -> downgradeTag k, (m, convert t)) |> Map.ofSeq)
664+
| InferedType.Heterogeneous(types, containsOptional) ->
665+
InferedType.Heterogeneous(
666+
types |> Map.toSeq |> Seq.map (fun (k, t) -> downgradeTag k, convert t) |> Map.ofSeq,
667+
containsOptional)
668+
| InferedType.Json(innerType, optional) -> InferedType.Json(convert innerType, optional)
669+
| _ -> infType
670+
result
671+
672+
convert inferedType
673+
674+
/// Replaces DateOnly → DateTime and TimeOnly → TimeSpan in a PrimitiveInferedProperty.
675+
/// Used in design-time code when the target framework does not support these .NET 6+ types.
676+
let internal downgradeNet6PrimitiveProperty (field: StructuralTypes.PrimitiveInferedProperty) =
677+
let v = field.Value
678+
679+
if v.InferedType = typeof<DateOnly> then
680+
{ field with Value = { v with InferedType = typeof<DateTime>; RuntimeType = typeof<DateTime> } }
681+
elif v.InferedType = typeof<TimeOnly> then
682+
{ field with Value = { v with InferedType = typeof<TimeSpan>; RuntimeType = typeof<TimeSpan> } }
683+
else
684+
field
685+
#endif

src/FSharp.Data.Xml.Core/FSharp.Data.Xml.Core.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project Sdk="Microsoft.NET.Sdk">
33
<PropertyGroup>
44
<OutputType>Library</OutputType>
5-
<TargetFramework>netstandard2.0</TargetFramework>
5+
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
66
<OtherFlags>$(OtherFlags) --warnon:1182 --nowarn:10001 --nowarn:44</OtherFlags>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>

src/FSharp.Data.Xml.Core/XsdInference.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,11 @@ module internal XsdInference =
214214
function
215215
| XmlTypeCode.Int -> typeof<int>
216216
| XmlTypeCode.Long -> typeof<int64>
217+
#if NET6_0_OR_GREATER
218+
| XmlTypeCode.Date -> typeof<System.DateOnly>
219+
#else
217220
| XmlTypeCode.Date -> typeof<System.DateTime>
221+
#endif
218222
| XmlTypeCode.DateTime -> typeof<System.DateTimeOffset>
219223
| XmlTypeCode.Boolean -> typeof<bool>
220224
| XmlTypeCode.Decimal -> typeof<decimal>

0 commit comments

Comments
 (0)