Skip to content

Commit a92db69

Browse files
Merge pull request #367 from piaste/more-tracestring-improvem
More tracestring improvements. Thanks @piaste.
2 parents d9791fb + 2e81e7b commit a92db69

File tree

3 files changed

+159
-35
lines changed

3 files changed

+159
-35
lines changed

src/SqlClient/ISqlCommand.fs

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,26 @@ type ``ISqlCommand Implementation``(cfg: DesignTimeConfig, connection: Connectio
134134
member this.ToTraceString parameters =
135135
``ISqlCommand Implementation``.SetParameters(cmd, parameters)
136136
let parameterDefinition (p : SqlParameter) =
137+
// decimal uses precision and scale instead of size
138+
if List.contains p.SqlDbType [SqlDbType.Money; SqlDbType.SmallMoney; SqlDbType.Decimal] then
139+
// maximum size is 38
140+
sprintf "%s %A(%u,%u)" p.ParameterName p.SqlDbType p.Precision p.Scale
141+
137142
// tinyint and Xml have size 1 and -1 respectively, but MSSQL will throw if they are specified
138-
if p.Size <> 0 &&
143+
elif p.Size <> 0 &&
139144
p.SqlDbType <> SqlDbType.Xml &&
140145
p.SqlDbType <> SqlDbType.TinyInt then
141146

142147
sprintf "%s %A(%d)" p.ParameterName p.SqlDbType p.Size
143148
else
144149
sprintf "%s %A" p.ParameterName p.SqlDbType
150+
151+
// helper map to resolve each parameter's target type
152+
let getSqlDbType =
153+
let lookup = Map.ofSeq <| Seq.zip (parameters |> Seq.map (fun (name, value) -> name))
154+
(cmd.Parameters |> Seq.cast<SqlParameter> |> Seq.map (fun p -> p.SqlDbType))
155+
fun name -> Map.find name lookup
156+
145157
seq {
146158

147159
yield sprintf "exec sp_executesql N'%s'" (cmd.CommandText.Replace("'", "''"))
@@ -157,16 +169,36 @@ type ``ISqlCommand Implementation``(cfg: DesignTimeConfig, connection: Connectio
157169
if parameters.Length > 0
158170
then
159171
yield parameters
160-
|> Seq.map(fun (name,value) ->
161-
let printedValue =
162-
match value with
163-
// print dates in roundtrip ISO8601 format "O"
164-
| :? System.DateTime as d -> d.ToString("O")
165-
// print timespans in constant format "c
166-
| :? System.TimeSpan as t -> t.ToString("c")
167-
| v -> sprintf "%O" v
168-
// escapes the resulting value
169-
sprintf "%s='%s'" name (printedValue.Replace("'", "''"))
172+
|> Seq.map(fun (name,value) ->
173+
// NULL isn't escaped
174+
match value with
175+
| null | :? DBNull -> sprintf "%s=NULL" name
176+
| nonNullValue ->
177+
let printedValue =
178+
match nonNullValue with
179+
// print dates with high precision (SQL datetimeoffset, datetime2) in roundtrip ISO8601 format "O"
180+
| :? System.DateTimeOffset as d -> d.ToString("O")
181+
| :? System.DateTime as d when getSqlDbType name = SqlDbType.DateTime2 -> d.ToString("O")
182+
// print dates with low precision (SQL datetime) in legacy format
183+
| :? System.DateTime as d when getSqlDbType name <> SqlDbType.DateTime2 -> d.ToString("yyyy-MM-ddTHH:mm:ss.fff")
184+
// print timespans in constant format "c
185+
| :? System.TimeSpan as t -> t.ToString("c")
186+
// print numeric values in culture-invariant format
187+
| :? decimal as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
188+
| :? double as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
189+
| :? single as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
190+
| :? bigint as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
191+
| :? uint64 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
192+
| :? int64 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
193+
| :? uint32 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
194+
| :? int as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
195+
| :? uint16 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
196+
| :? int16 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
197+
| :? byte as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
198+
| :? sbyte as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
199+
| v -> sprintf "%O" v
200+
// escapes the resulting value, with Unicode notation
201+
sprintf "%s=N'%s'" name (printedValue.Replace("'", "''"))
170202
)
171203
|> String.concat ","
172204
} |> String.concat "," //Using string.concat to handle annoying case with no parameters

tests/SqlClient.Tests/CreateCommand.fs

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,62 @@ let columnsShouldNotBeNull2() =
8282
let _,_,_,_,precision = cmd.Execute().Value
8383
Assert.Equal(None, precision)
8484

85-
[<Fact>]
86-
let toTraceString() =
87-
let now = System.DateTime.Now
88-
let universalNow = now.ToString("O")
89-
let num = 42
90-
let expected = sprintf "exec sp_executesql N'SELECT CAST(@Date AS DATE), CAST(@Number AS INT)',N'@Date Date,@Number Int',@Date='%s',@Number='%d'" universalNow num
91-
let cmd = DB.CreateCommand<"SELECT CAST(@Date AS DATE), CAST(@Number AS INT)", ResultType.Tuples>()
92-
Assert.Equal<string>(
93-
expected,
94-
actual = cmd.ToTraceString( now, num)
95-
)
85+
module TraceTests =
86+
87+
let [<Literal>] queryStart = "SELECT CAST(@Value AS "
88+
let [<Literal>] queryEnd = ")"
89+
90+
let [<Literal>] DATETIME = "DateTime"
91+
let [<Literal>] queryDATETIME = queryStart + DATETIME + queryEnd
92+
93+
let [<Literal>] DATETIMEOFFSET = "DateTimeOffset"
94+
let [<Literal>] queryDATETIMEOFFSET = queryStart + DATETIMEOFFSET + queryEnd
95+
96+
let [<Literal>] TIMESTAMP = "Time"
97+
let [<Literal>] queryTIMESTAMP = queryStart + TIMESTAMP + queryEnd
98+
99+
let [<Literal>] INT = "Int"
100+
let [<Literal>] queryINT = queryStart + INT + queryEnd
101+
102+
let [<Literal>] DECIMAL63 = "Decimal(6,3)"
103+
let [<Literal>] queryDECIMAL63 = queryStart + DECIMAL63 + queryEnd
104+
105+
let inline testTraceString traceQuery (cmd : ^cmd) dbType (value : ^value) printedValue =
106+
let expected = sprintf "exec sp_executesql N'%s',N'@Value %s',@Value=N'%s'" traceQuery dbType printedValue
107+
Assert.Equal<string>(expected, actual = (^cmd : (member ToTraceString : ^value -> string) (cmd, value)))
108+
109+
[<Fact>]
110+
let traceDate() =
111+
let now = System.DateTime.Now
112+
testTraceString queryDATETIME (DB.CreateCommand<queryDATETIME>()) DATETIME
113+
now (now.ToString("yyyy-MM-ddTHH:mm:ss.fff"))
114+
115+
[<Fact>]
116+
let traceDateTimeOffset() =
117+
let now = System.DateTimeOffset.Now
118+
testTraceString queryDATETIMEOFFSET (DB.CreateCommand<queryDATETIMEOFFSET>()) DATETIMEOFFSET
119+
now (now.ToString("O"))
120+
121+
[<Fact>]
122+
let traceTimestamp() =
123+
let timeOfDay = System.DateTime.Now.TimeOfDay
124+
testTraceString queryTIMESTAMP (DB.CreateCommand<queryTIMESTAMP>()) TIMESTAMP
125+
timeOfDay (timeOfDay.ToString("c"))
126+
127+
[<Fact>]
128+
let traceInt() =
129+
testTraceString queryINT (DB.CreateCommand<queryINT>()) INT
130+
42 "42"
131+
132+
[<Fact>]
133+
let traceDecimal() =
134+
testTraceString queryDECIMAL63 (DB.CreateCommand<queryDECIMAL63>()) DECIMAL63
135+
123.456m (123.456m.ToString(System.Globalization.CultureInfo.InvariantCulture))
136+
137+
[<Fact>]
138+
let traceNull() =
139+
let expected = sprintf "exec sp_executesql N'SELECT CAST(@Value AS NVARCHAR(20))',N'@Value NVarChar(20)',@Value=NULL"
140+
Assert.Equal<string>(expected, actual = DB.CreateCommand<"SELECT CAST(@Value AS NVARCHAR(20))">().ToTraceString(Unchecked.defaultof<string>))
96141

97142
[<Fact>]
98143
let resultSetMapping() =

tests/SqlClient.Tests/TypeProviderTest.fs

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,63 +89,110 @@ let singleRowOption() =
8989
[<Fact>]
9090
let ToTraceString() =
9191
let now = DateTime.Now
92-
let universalPrintedNow = now.ToString("O")
92+
let universalPrintedNow = now.ToString("yyyy-MM-ddTHH:mm:ss.fff")
9393
let num = 42
94-
let expected = sprintf "exec sp_executesql N'SELECT CAST(@Date AS DATE), CAST(@Number AS INT)',N'@Date Date,@Number Int',@Date='%s',@Number='%d'" universalPrintedNow num
94+
let expected = sprintf "exec sp_executesql N'SELECT CAST(@Date AS DATE), CAST(@Number AS INT)',N'@Date Date,@Number Int',@Date=N'%s',@Number=N'%d'" universalPrintedNow num
9595
let cmd = new SqlCommandProvider<"SELECT CAST(@Date AS DATE), CAST(@Number AS INT)", ConnectionStrings.AdventureWorksNamed, ResultType.Tuples>()
9696
Assert.Equal<string>(
9797
expected,
9898
actual = cmd.ToTraceString( now, num)
9999
)
100100

101-
let runString query =
101+
let runScalarQuery query =
102102
use conn = new SqlConnection(ConnectionStrings.AdventureWorks)
103103
conn.Open()
104104
use cmd = new System.Data.SqlClient.SqlCommand()
105105
cmd.Connection <- conn
106106
cmd.CommandText <- query
107-
cmd.ExecuteNonQuery()
107+
cmd.ExecuteScalar()
108108

109109
[<Fact>]
110110
let ``ToTraceString for dates``() =
111111
let cmd = new SqlCommandProvider<"SELECT CAST(@Date AS DATE)", ConnectionStrings.AdventureWorksNamed>()
112-
runString <| cmd.ToTraceString(System.DateTime.Now)
112+
runScalarQuery <| cmd.ToTraceString(System.DateTime.Now)
113113

114114
[<Fact>]
115115
let ``ToTraceString for times``() =
116116
let cmd = new SqlCommandProvider<"SELECT CAST(@Time AS Time)", ConnectionStrings.AdventureWorksNamed>()
117-
runString <| cmd.ToTraceString(System.DateTime.Now.TimeOfDay)
117+
runScalarQuery <| cmd.ToTraceString(System.DateTime.Now.TimeOfDay)
118118

119119
[<Fact>]
120120
let ``ToTraceString for tinyint``() =
121121
let cmd = new SqlCommandProvider<"SELECT CAST(@ti AS TINYINT)", ConnectionStrings.AdventureWorksNamed>()
122-
runString <| cmd.ToTraceString(0uy)
122+
runScalarQuery <| cmd.ToTraceString(0uy)
123123

124124
[<Fact>]
125125
let ``ToTraceString for xml``() =
126126
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS XML)", ConnectionStrings.AdventureWorksNamed>()
127-
runString <| cmd.ToTraceString("<foo>bar</foo>")
127+
runScalarQuery <| cmd.ToTraceString("<foo>bar</foo>")
128128

129129
[<Fact>]
130130
let ``ToTraceString for xml with single quotes``() =
131131
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS XML)", ConnectionStrings.AdventureWorksNamed>()
132-
runString <| cmd.ToTraceString("<foo>b'ar</foo>")
132+
runScalarQuery <| cmd.ToTraceString("<foo>b'ar</foo>")
133+
134+
[<Fact>]
135+
let ``Roundtrip ToTraceString for unicode``() =
136+
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS NVARCHAR(20))", ConnectionStrings.AdventureWorksNamed>()
137+
let rocket = "🚀"
138+
let result = runScalarQuery <| cmd.ToTraceString(rocket)
139+
Assert.Equal(expected = rocket, actual = unbox<string> result)
140+
141+
[<Fact>]
142+
let ``Roundtrip ToTraceString for decimals with maximum precision``() =
143+
// Note: maximum precision for MSSQL decimals is 38, but maximum for MSSQL <-> .NET conversion is 29
144+
// https://weblogs.sqlteam.com/mladenp/2010/08/31/when-does-sql-server-decimal-not-convert-to-net-decimal/
145+
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS DECIMAL(29, 19))", ConnectionStrings.AdventureWorksNamed>()
146+
let decimal_29_19 = 1234567890.1234567890123456789m
147+
let result = runScalarQuery <| cmd.ToTraceString(decimal_29_19)
148+
Assert.Equal(expected = decimal_29_19, actual = unbox<decimal> result)
149+
150+
[<Fact>]
151+
let ``Roundtrip ToTraceString for date time ``() =
152+
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS DATETIME)", ConnectionStrings.AdventureWorksNamed>()
153+
// SQL Server DATETIME has precision up to .00333 seconds
154+
let tolerance = 0.00334
155+
let now = System.DateTime.Now
156+
let result = runScalarQuery <| cmd.ToTraceString(now)
157+
Assert.InRange(unbox<DateTime> result, now.AddSeconds(-1. * tolerance), now.AddSeconds(tolerance))
158+
159+
[<Fact>]
160+
let ``Roundtrip ToTraceString for datetime2 ``() =
161+
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS DATETIME2)", ConnectionStrings.AdventureWorksNamed>()
162+
// SQL Server DATETIME2 has the same nanosecond precision as DATETIMEOFFSET so there shouldn't be any discrepance
163+
let now = System.DateTime.Now
164+
let result = runScalarQuery <| cmd.ToTraceString(now)
165+
Assert.Equal(expected = now, actual = unbox<DateTime> result)
166+
167+
[<Fact>]
168+
let ``Roundtrip ToTraceString for date time offsets``() =
169+
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS DATETIMEOFFSET)", ConnectionStrings.AdventureWorksNamed>()
170+
let now = System.DateTimeOffset.Now
171+
let result = runScalarQuery <| cmd.ToTraceString(now)
172+
Assert.Equal(expected = now, actual = unbox<DateTimeOffset> result)
173+
174+
[<Fact>]
175+
let ``Roundtrip ToTraceString for nulls``() =
176+
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS NVARCHAR(20))", ConnectionStrings.AdventureWorksNamed>()
177+
let dbnull = Unchecked.defaultof<string>
178+
let result = runScalarQuery <| cmd.ToTraceString(dbnull)
179+
Assert.Equal(expected = box DBNull.Value, actual = result)
133180

134181
[<Fact>]
135182
let ``ToTraceString for CRUD``() =
136183

137184
Assert.Equal<string>(
138-
expected = "exec sp_executesql N'SELECT CurrencyCode, Name FROM Sales.Currency WHERE CurrencyCode = @code',N'@code NChar(3)',@code='BTC'",
185+
expected = "exec sp_executesql N'SELECT CurrencyCode, Name FROM Sales.Currency WHERE CurrencyCode = @code',N'@code NChar(3)',@code=N'BTC'",
139186
actual = let cmd = new GetBitCoin() in cmd.ToTraceString( bitCoinCode)
140187
)
141188

142189
Assert.Equal<string>(
143-
expected = "exec sp_executesql N'INSERT INTO Sales.Currency VALUES(@Code, @Name, GETDATE())',N'@Code NChar(3),@Name NVarChar(50)',@Code='BTC',@Name='Bitcoin'",
190+
expected = "exec sp_executesql N'INSERT INTO Sales.Currency VALUES(@Code, @Name, GETDATE())',N'@Code NChar(3),@Name NVarChar(50)',@Code=N'BTC',@Name=N'Bitcoin'",
144191
actual = let cmd = new InsertBitCoin() in cmd.ToTraceString( bitCoinCode, bitCoinName)
145192
)
146193

147194
Assert.Equal<string>(
148-
expected = "exec sp_executesql N'DELETE FROM Sales.Currency WHERE CurrencyCode = @Code',N'@Code NChar(3)',@Code='BTC'",
195+
expected = "exec sp_executesql N'DELETE FROM Sales.Currency WHERE CurrencyCode = @Code',N'@Code NChar(3)',@Code=N'BTC'",
149196
actual = let cmd = new DeleteBitCoin() in cmd.ToTraceString( bitCoinCode)
150197
)
151198

@@ -160,7 +207,7 @@ let ``ToTraceString double-quotes``() =
160207
let ``ToTraceString double-quotes in parameter``() =
161208
use cmd = new SqlCommandProvider<"SELECT * FROM Sales.Currency WHERE CurrencyCode = @CurrencyCode", ConnectionStrings.AdventureWorksNamed>()
162209
Assert.Equal<string>(
163-
expected = "exec sp_executesql N'SELECT * FROM Sales.Currency WHERE CurrencyCode = @CurrencyCode',N'@CurrencyCode NChar(3)',@CurrencyCode='A''B'",
210+
expected = "exec sp_executesql N'SELECT * FROM Sales.Currency WHERE CurrencyCode = @CurrencyCode',N'@CurrencyCode NChar(3)',@CurrencyCode=N'A''B'",
164211
actual = cmd.ToTraceString("A'B")
165212
)
166213

0 commit comments

Comments
 (0)