Skip to content

Commit b7af13c

Browse files
Merge pull request #206 from fsprojects/Issue196
issue 196
2 parents f24fa52 + f4c0583 commit b7af13c

File tree

6 files changed

+101
-95
lines changed

6 files changed

+101
-95
lines changed

docs/content/debugging.fsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,24 @@ While enjoying all benefits of static types at design time one can easily end up
5252
when runtime Sql Server database schema is different from compile time.
5353
Up until now this resulted in confusion runtime exception: `InvalidCastException("Specified cast is not valid.")`.
5454
55-
To improve diagnostics without hurting performance a new configuration section/switch is introduced.
56-
57-
First, define custom sectoin in app.config/web.config
58-
59-
[lang=xml]
60-
<configSections>
61-
<section name="FSharp.Data.SqlClient" type="System.Configuration.NameValueSectionHandler" />
62-
</configSections>
55+
To improve diagnostics without hurting performance a new global singleton configuration object is introduced.
56+
To access `Configuration` type open up `FSharp.Data.SqlClient` namespace.
57+
*)
6358

64-
Second, set on `ResultsetRuntimeVerification` switch
59+
open FSharp.Data.SqlClient
60+
assert(Configuration.Current.ResultsetRuntimeVerification = false)
6561

66-
[lang=xml]
67-
<FSharp.Data.SqlClient>
68-
<add key="ResultsetRuntimeVerification" value="true"/>
69-
</FSharp.Data.SqlClient>
62+
(**
63+
So far it has only one property `ResultsetRuntimeVerification` which set to false by default.
64+
Set it to true to see more descriptive error like:
7065
71-
Now expect to see more descriptive error like:
7266
`InvalidOperationException(Expected column [Total] of type "System.Int32" at position 1 (0-based indexing)
7367
but received column [Now] of type "System.DateTime")`.
68+
*)
69+
70+
Configuration.Current <- { Configuration.Current with ResultsetRuntimeVerification = true }
7471

72+
(**
7573
Other debugging/instrumentation tools to consider:
7674
-------------------------------------
7775

src/SqlClient.Tests/ConfigurationTest.fs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,6 @@ open FSharp.Data
77

88
let adventureWorks = FSharp.Configuration.AppSettings<"app.config">.ConnectionStrings.AdventureWorks
99

10-
[<Fact>]
11-
let CheckValidFileName() =
12-
let expected = Some "c:\\mysqlfiles\\test.sql"
13-
Assert.Equal(expected, Configuration.GetValidFileName("test.sql", "c:\\mysqlfiles"))
14-
15-
Assert.Equal(expected, Configuration.GetValidFileName("test.sql", "c:\\mysqlfiles"))
16-
Assert.Equal(expected, Configuration.GetValidFileName("../test.sql", "c:\\mysqlfiles\\subfolder"))
17-
Assert.Equal(expected, Configuration.GetValidFileName("c:\\mysqlfiles/test.sql", "d:\\otherdrive"))
18-
Assert.Equal(expected, Configuration.GetValidFileName("../mysqlfiles/test.sql", "c:\\otherfolder"))
19-
Assert.Equal(expected, Configuration.GetValidFileName("a/b/c/../../../test.sql", "c:\\mysqlfiles"))
20-
2110
type Get42RelativePath = SqlCommandProvider<"sampleCommand.sql", ConnectionStrings.AdventureWorksNamed, ResolutionFolder="MySqlFolder">
2211

2312
type Get42 = SqlCommandProvider<"SELECT 42", ConnectionStrings.AdventureWorksNamed, ConfigFile = "appWithInclude.config">
@@ -28,6 +17,17 @@ type LongQuery = SqlCommandProvider<"
2817
", ConnectionStrings.AdventureWorksNamed>
2918

3019
#if DEBUG
20+
[<Fact>]
21+
let CheckValidFileName() =
22+
let expected = Some "c:\\mysqlfiles\\test.sql"
23+
Assert.Equal(expected, SqlCommandProvider.GetValidFileName("test.sql", "c:\\mysqlfiles"))
24+
25+
Assert.Equal(expected, SqlCommandProvider.GetValidFileName("test.sql", "c:\\mysqlfiles"))
26+
Assert.Equal(expected, SqlCommandProvider.GetValidFileName("../test.sql", "c:\\mysqlfiles\\subfolder"))
27+
Assert.Equal(expected, SqlCommandProvider.GetValidFileName("c:\\mysqlfiles/test.sql", "d:\\otherdrive"))
28+
Assert.Equal(expected, SqlCommandProvider.GetValidFileName("../mysqlfiles/test.sql", "c:\\otherfolder"))
29+
Assert.Equal(expected, SqlCommandProvider.GetValidFileName("a/b/c/../../../test.sql", "c:\\mysqlfiles"))
30+
3131
[<Fact>]
3232
let ``Wrong config file name`` () =
3333
Assert.Throws<FileNotFoundException>(

src/SqlClient.Tests/TypeProviderTest.fs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,7 @@ let ResultsetExtendedWithTrailingColumn() =
220220
"
221221
Assert.Equal<_ list>([0..9], [ for x in cmd.Execute() -> x.Value ])
222222

223-
let resultsetRuntimeVerificationEnabled =
224-
lazy
225-
match Configuration.ConfigurationManager.GetSection("FSharp.Data.SqlClient") with
226-
| :? System.Collections.Specialized.NameValueCollection as xs ->
227-
string xs.["ResultsetRuntimeVerification"] = "true"
228-
| _ -> false
223+
open FSharp.Data.SqlClient
229224

230225
[<Fact>]
231226
let ResultsetRuntimeVerificationLessThanExpectedColumns() =
@@ -253,19 +248,25 @@ let ResultsetRuntimeVerificationLessThanExpectedColumns() =
253248
)
254249
SELECT * FROM XS
255250
"
256-
if resultsetRuntimeVerificationEnabled.Value
257-
then
251+
252+
Assert.False(SqlClient.Configuration.Current.ResultsetRuntimeVerification)
253+
254+
try
255+
SqlClient.Configuration.Current <- { ResultsetRuntimeVerification = true }
258256
let err = Assert.Throws<InvalidOperationException>(fun() -> cmd.Execute() |> Seq.toArray |> ignore)
259257
Assert.Equal<string>(
260258
"Expected at least 3 columns in result set but received only 2.",
261259
err.Message
262260
)
263-
else
264-
let err = Assert.Throws<IndexOutOfRangeException>(fun() -> cmd.Execute() |> Seq.toArray |> ignore)
265-
Assert.Equal<string>(
266-
"Index was outside the bounds of the array.",
267-
err.Message
268-
)
261+
finally
262+
SqlClient.Configuration.Current <- { ResultsetRuntimeVerification = false}
263+
264+
let err = Assert.Throws<IndexOutOfRangeException>(fun() -> cmd.Execute() |> Seq.toArray |> ignore)
265+
Assert.Equal<string>(
266+
"Index was outside the bounds of the array.",
267+
err.Message
268+
)
269+
269270

270271
[<Fact>]
271272
let ResultsetRuntimeVerificationDiffColumnTypes() =
@@ -293,19 +294,24 @@ let ResultsetRuntimeVerificationDiffColumnTypes() =
293294
SELECT * FROM XS
294295
"
295296

296-
if resultsetRuntimeVerificationEnabled.Value
297-
then
297+
Assert.False(Configuration.Current.ResultsetRuntimeVerification)
298+
299+
try
300+
Configuration.Current <- { ResultsetRuntimeVerification = true }
301+
298302
let err = Assert.Throws<InvalidOperationException>(fun() -> cmd.Execute() |> Seq.toArray |> ignore)
299303
Assert.Equal<string>(
300304
"""Expected column [Total] of type "System.Int32" at position 1 (0-based indexing) but received column [Now] of type "System.DateTime".""",
301305
err.Message
302306
)
303-
else
304-
let err = Assert.Throws<InvalidCastException>(fun() -> cmd.Execute() |> Seq.toArray |> ignore)
305-
Assert.Equal<string>(
306-
"Specified cast is not valid.",
307-
err.Message
308-
)
307+
finally
308+
Configuration.Current <- { ResultsetRuntimeVerification = false}
309+
310+
let err = Assert.Throws<InvalidCastException>(fun() -> cmd.Execute() |> Seq.toArray |> ignore)
311+
Assert.Equal<string>(
312+
"Specified cast is not valid.",
313+
err.Message
314+
)
309315

310316
module ``The undeclared parameter 'X' is used more than once in the batch being analyzed`` =
311317
[<Fact>]

src/SqlClient/Configuration.fs

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -79,42 +79,20 @@ type internal DesignTimeConnectionString =
7979
else section.ConnectionString
8080
@@>
8181

82-
[<CompilerMessageAttribute("This API supports the FSharp.Data.SqlClient infrastructure and is not intended to be used directly from your code.", 101, IsHidden = true)>]
83-
type Configuration() =
84-
static let invalidPathChars = HashSet(Path.GetInvalidPathChars())
85-
static let invalidFileChars = HashSet(Path.GetInvalidFileNameChars())
86-
87-
static member GetValidFileName (file:string, resolutionFolder:string) =
88-
try
89-
if (file.Contains "\n") || (resolutionFolder.Contains "\n") then None else
90-
let f = Path.Combine(resolutionFolder, file)
91-
if invalidPathChars.Overlaps (Path.GetDirectoryName f) ||
92-
invalidFileChars.Overlaps (Path.GetFileName f) then None
93-
else
94-
// Canonicalizing the path may throw on bad input, the check above does not cover every error.
95-
Some (Path.GetFullPath f)
96-
with _ ->
97-
None
98-
99-
static member ParseTextAtDesignTime(commandTextOrPath : string, resolutionFolder, invalidateCallback) =
100-
match Configuration.GetValidFileName (commandTextOrPath, resolutionFolder) with
101-
| Some path when File.Exists path ->
102-
if Path.GetExtension(path) <> ".sql" then failwith "Only files with .sql extension are supported"
103-
let watcher = new FileSystemWatcher(Filter = Path.GetFileName path, Path = Path.GetDirectoryName path)
104-
watcher.Changed.Add(fun _ -> invalidateCallback())
105-
watcher.Renamed.Add(fun _ -> invalidateCallback())
106-
watcher.Deleted.Add(fun _ -> invalidateCallback())
107-
watcher.EnableRaisingEvents <- true
108-
let task = Task.Factory.StartNew(fun () ->
109-
use stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
110-
use reader = new StreamReader(stream)
111-
reader.ReadToEnd())
112-
if not (task.Wait(TimeSpan.FromSeconds(1.))) then failwithf "Couldn't read command from file %s" path
113-
task.Result, Some watcher
114-
| _ -> commandTextOrPath, None
115-
116-
117-
118-
119-
120-
82+
//this is mess. Clean up later.
83+
type Configuration = {
84+
ResultsetRuntimeVerification: bool
85+
}
86+
87+
namespace FSharp.Data
88+
89+
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
90+
[<AutoOpen>]
91+
module Configuration =
92+
let private guard = obj()
93+
let private current = ref { SqlClient.Configuration.ResultsetRuntimeVerification = false }
94+
95+
type SqlClient.Configuration with
96+
static member Current
97+
with get() = lock guard <| fun() -> !current
98+
and set value = lock guard <| fun() -> current := value

src/SqlClient/ISqlCommand.fs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,6 @@ type ``ISqlCommand Implementation``(cfg: DesignTimeConfig, connection: Connectio
9090

9191
let notImplemented _ : _ = raise <| NotImplementedException()
9292

93-
static let resultsetRuntimeVerification =
94-
lazy
95-
match ConfigurationManager.GetSection("FSharp.Data.SqlClient") with
96-
| :? NameValueCollection as xs ->
97-
match xs.["ResultsetRuntimeVerification"] with | null -> false | s -> s.ToLower() = "true"
98-
| _ -> false
99-
10093
let execute, asyncExecute, executeSingle, asyncExecuteSingle =
10194
match cfg.ResultType with
10295
| ResultType.DataReader ->
@@ -201,7 +194,7 @@ type ``ISqlCommand Implementation``(cfg: DesignTimeConfig, connection: Connectio
201194
//Execute/AsyncExecute versions
202195

203196
static member internal VerifyResultsetColumns(cursor: SqlDataReader, expected) =
204-
if resultsetRuntimeVerification.Value
197+
if Configuration.Current.ResultsetRuntimeVerification
205198
then
206199
if cursor.FieldCount < Array.length expected
207200
then

src/SqlClient/SqlCommandProvider.fs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ do()
2929
type public SqlCommandProvider(config : TypeProviderConfig) as this =
3030
inherit TypeProviderForNamespaces()
3131

32+
static let invalidPathChars = HashSet(Path.GetInvalidPathChars())
33+
static let invalidFileChars = HashSet(Path.GetInvalidFileNameChars())
34+
3235
let mutable watcher = null : IDisposable
3336

3437
let nameSpace = this.GetType().Namespace
@@ -102,7 +105,7 @@ type public SqlCommandProvider(config : TypeProviderConfig) as this =
102105
then resolutionFolder
103106
else Path.Combine (config.ResolutionFolder, resolutionFolder)
104107

105-
Configuration.ParseTextAtDesignTime(sqlStatementOrFile, sqlScriptResolutionFolder, invalidator)
108+
SqlCommandProvider.ParseTextAtDesignTime(sqlStatementOrFile, sqlScriptResolutionFolder, invalidator)
106109

107110
watcher' |> Option.iter (fun x -> watcher <- x)
108111

@@ -187,3 +190,31 @@ type public SqlCommandProvider(config : TypeProviderConfig) as this =
187190

188191
cmdProvidedType
189192

193+
static member internal GetValidFileName (file:string, resolutionFolder:string) =
194+
try
195+
if (file.Contains "\n") || (resolutionFolder.Contains "\n") then None else
196+
let f = Path.Combine(resolutionFolder, file)
197+
if invalidPathChars.Overlaps (Path.GetDirectoryName f) ||
198+
invalidFileChars.Overlaps (Path.GetFileName f) then None
199+
else
200+
// Canonicalizing the path may throw on bad input, the check above does not cover every error.
201+
Some (Path.GetFullPath f)
202+
with _ ->
203+
None
204+
205+
static member ParseTextAtDesignTime(commandTextOrPath : string, resolutionFolder, invalidateCallback) =
206+
match SqlCommandProvider.GetValidFileName (commandTextOrPath, resolutionFolder) with
207+
| Some path when File.Exists path ->
208+
if Path.GetExtension(path) <> ".sql" then failwith "Only files with .sql extension are supported"
209+
let watcher = new FileSystemWatcher(Filter = Path.GetFileName path, Path = Path.GetDirectoryName path)
210+
watcher.Changed.Add(fun _ -> invalidateCallback())
211+
watcher.Renamed.Add(fun _ -> invalidateCallback())
212+
watcher.Deleted.Add(fun _ -> invalidateCallback())
213+
watcher.EnableRaisingEvents <- true
214+
let task = System.Threading.Tasks.Task.Factory.StartNew(fun () ->
215+
use stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
216+
use reader = new StreamReader(stream)
217+
reader.ReadToEnd())
218+
if not (task.Wait(TimeSpan.FromSeconds(1.))) then failwithf "Couldn't read command from file %s" path
219+
task.Result, Some watcher
220+
| _ -> commandTextOrPath, None

0 commit comments

Comments
 (0)