Skip to content

Commit

Permalink
Merge pull request #367 from piaste/more-tracestring-improvem
Browse files Browse the repository at this point in the history
More tracestring improvements. Thanks @piaste.
  • Loading branch information
smoothdeveloper authored Sep 11, 2020
2 parents d9791fb + 2e81e7b commit a92db69
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 35 deletions.
54 changes: 43 additions & 11 deletions src/SqlClient/ISqlCommand.fs
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,26 @@ type ``ISqlCommand Implementation``(cfg: DesignTimeConfig, connection: Connectio
member this.ToTraceString parameters =
``ISqlCommand Implementation``.SetParameters(cmd, parameters)
let parameterDefinition (p : SqlParameter) =
// decimal uses precision and scale instead of size
if List.contains p.SqlDbType [SqlDbType.Money; SqlDbType.SmallMoney; SqlDbType.Decimal] then
// maximum size is 38
sprintf "%s %A(%u,%u)" p.ParameterName p.SqlDbType p.Precision p.Scale

// tinyint and Xml have size 1 and -1 respectively, but MSSQL will throw if they are specified
if p.Size <> 0 &&
elif p.Size <> 0 &&
p.SqlDbType <> SqlDbType.Xml &&
p.SqlDbType <> SqlDbType.TinyInt then

sprintf "%s %A(%d)" p.ParameterName p.SqlDbType p.Size
else
sprintf "%s %A" p.ParameterName p.SqlDbType

// helper map to resolve each parameter's target type
let getSqlDbType =
let lookup = Map.ofSeq <| Seq.zip (parameters |> Seq.map (fun (name, value) -> name))
(cmd.Parameters |> Seq.cast<SqlParameter> |> Seq.map (fun p -> p.SqlDbType))
fun name -> Map.find name lookup

seq {

yield sprintf "exec sp_executesql N'%s'" (cmd.CommandText.Replace("'", "''"))
Expand All @@ -157,16 +169,36 @@ type ``ISqlCommand Implementation``(cfg: DesignTimeConfig, connection: Connectio
if parameters.Length > 0
then
yield parameters
|> Seq.map(fun (name,value) ->
let printedValue =
match value with
// print dates in roundtrip ISO8601 format "O"
| :? System.DateTime as d -> d.ToString("O")
// print timespans in constant format "c
| :? System.TimeSpan as t -> t.ToString("c")
| v -> sprintf "%O" v
// escapes the resulting value
sprintf "%s='%s'" name (printedValue.Replace("'", "''"))
|> Seq.map(fun (name,value) ->
// NULL isn't escaped
match value with
| null | :? DBNull -> sprintf "%s=NULL" name
| nonNullValue ->
let printedValue =
match nonNullValue with
// print dates with high precision (SQL datetimeoffset, datetime2) in roundtrip ISO8601 format "O"
| :? System.DateTimeOffset as d -> d.ToString("O")
| :? System.DateTime as d when getSqlDbType name = SqlDbType.DateTime2 -> d.ToString("O")
// print dates with low precision (SQL datetime) in legacy format
| :? System.DateTime as d when getSqlDbType name <> SqlDbType.DateTime2 -> d.ToString("yyyy-MM-ddTHH:mm:ss.fff")
// print timespans in constant format "c
| :? System.TimeSpan as t -> t.ToString("c")
// print numeric values in culture-invariant format
| :? decimal as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? double as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? single as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? bigint as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? uint64 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? int64 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? uint32 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? int as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? uint16 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? int16 as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? byte as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| :? sbyte as n -> n.ToString(System.Globalization.CultureInfo.InvariantCulture)
| v -> sprintf "%O" v
// escapes the resulting value, with Unicode notation
sprintf "%s=N'%s'" name (printedValue.Replace("'", "''"))
)
|> String.concat ","
} |> String.concat "," //Using string.concat to handle annoying case with no parameters
Expand Down
67 changes: 56 additions & 11 deletions tests/SqlClient.Tests/CreateCommand.fs
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,62 @@ let columnsShouldNotBeNull2() =
let _,_,_,_,precision = cmd.Execute().Value
Assert.Equal(None, precision)

[<Fact>]
let toTraceString() =
let now = System.DateTime.Now
let universalNow = now.ToString("O")
let num = 42
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
let cmd = DB.CreateCommand<"SELECT CAST(@Date AS DATE), CAST(@Number AS INT)", ResultType.Tuples>()
Assert.Equal<string>(
expected,
actual = cmd.ToTraceString( now, num)
)
module TraceTests =

let [<Literal>] queryStart = "SELECT CAST(@Value AS "
let [<Literal>] queryEnd = ")"

let [<Literal>] DATETIME = "DateTime"
let [<Literal>] queryDATETIME = queryStart + DATETIME + queryEnd

let [<Literal>] DATETIMEOFFSET = "DateTimeOffset"
let [<Literal>] queryDATETIMEOFFSET = queryStart + DATETIMEOFFSET + queryEnd

let [<Literal>] TIMESTAMP = "Time"
let [<Literal>] queryTIMESTAMP = queryStart + TIMESTAMP + queryEnd

let [<Literal>] INT = "Int"
let [<Literal>] queryINT = queryStart + INT + queryEnd

let [<Literal>] DECIMAL63 = "Decimal(6,3)"
let [<Literal>] queryDECIMAL63 = queryStart + DECIMAL63 + queryEnd

let inline testTraceString traceQuery (cmd : ^cmd) dbType (value : ^value) printedValue =
let expected = sprintf "exec sp_executesql N'%s',N'@Value %s',@Value=N'%s'" traceQuery dbType printedValue
Assert.Equal<string>(expected, actual = (^cmd : (member ToTraceString : ^value -> string) (cmd, value)))

[<Fact>]
let traceDate() =
let now = System.DateTime.Now
testTraceString queryDATETIME (DB.CreateCommand<queryDATETIME>()) DATETIME
now (now.ToString("yyyy-MM-ddTHH:mm:ss.fff"))

[<Fact>]
let traceDateTimeOffset() =
let now = System.DateTimeOffset.Now
testTraceString queryDATETIMEOFFSET (DB.CreateCommand<queryDATETIMEOFFSET>()) DATETIMEOFFSET
now (now.ToString("O"))

[<Fact>]
let traceTimestamp() =
let timeOfDay = System.DateTime.Now.TimeOfDay
testTraceString queryTIMESTAMP (DB.CreateCommand<queryTIMESTAMP>()) TIMESTAMP
timeOfDay (timeOfDay.ToString("c"))

[<Fact>]
let traceInt() =
testTraceString queryINT (DB.CreateCommand<queryINT>()) INT
42 "42"

[<Fact>]
let traceDecimal() =
testTraceString queryDECIMAL63 (DB.CreateCommand<queryDECIMAL63>()) DECIMAL63
123.456m (123.456m.ToString(System.Globalization.CultureInfo.InvariantCulture))

[<Fact>]
let traceNull() =
let expected = sprintf "exec sp_executesql N'SELECT CAST(@Value AS NVARCHAR(20))',N'@Value NVarChar(20)',@Value=NULL"
Assert.Equal<string>(expected, actual = DB.CreateCommand<"SELECT CAST(@Value AS NVARCHAR(20))">().ToTraceString(Unchecked.defaultof<string>))

[<Fact>]
let resultSetMapping() =
Expand Down
73 changes: 60 additions & 13 deletions tests/SqlClient.Tests/TypeProviderTest.fs
Original file line number Diff line number Diff line change
Expand Up @@ -89,63 +89,110 @@ let singleRowOption() =
[<Fact>]
let ToTraceString() =
let now = DateTime.Now
let universalPrintedNow = now.ToString("O")
let universalPrintedNow = now.ToString("yyyy-MM-ddTHH:mm:ss.fff")
let num = 42
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
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
let cmd = new SqlCommandProvider<"SELECT CAST(@Date AS DATE), CAST(@Number AS INT)", ConnectionStrings.AdventureWorksNamed, ResultType.Tuples>()
Assert.Equal<string>(
expected,
actual = cmd.ToTraceString( now, num)
)

let runString query =
let runScalarQuery query =
use conn = new SqlConnection(ConnectionStrings.AdventureWorks)
conn.Open()
use cmd = new System.Data.SqlClient.SqlCommand()
cmd.Connection <- conn
cmd.CommandText <- query
cmd.ExecuteNonQuery()
cmd.ExecuteScalar()

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

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

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

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

[<Fact>]
let ``ToTraceString for xml with single quotes``() =
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS XML)", ConnectionStrings.AdventureWorksNamed>()
runString <| cmd.ToTraceString("<foo>b'ar</foo>")
runScalarQuery <| cmd.ToTraceString("<foo>b'ar</foo>")

[<Fact>]
let ``Roundtrip ToTraceString for unicode``() =
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS NVARCHAR(20))", ConnectionStrings.AdventureWorksNamed>()
let rocket = "🚀"
let result = runScalarQuery <| cmd.ToTraceString(rocket)
Assert.Equal(expected = rocket, actual = unbox<string> result)

[<Fact>]
let ``Roundtrip ToTraceString for decimals with maximum precision``() =
// Note: maximum precision for MSSQL decimals is 38, but maximum for MSSQL <-> .NET conversion is 29
// https://weblogs.sqlteam.com/mladenp/2010/08/31/when-does-sql-server-decimal-not-convert-to-net-decimal/
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS DECIMAL(29, 19))", ConnectionStrings.AdventureWorksNamed>()
let decimal_29_19 = 1234567890.1234567890123456789m
let result = runScalarQuery <| cmd.ToTraceString(decimal_29_19)
Assert.Equal(expected = decimal_29_19, actual = unbox<decimal> result)

[<Fact>]
let ``Roundtrip ToTraceString for date time ``() =
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS DATETIME)", ConnectionStrings.AdventureWorksNamed>()
// SQL Server DATETIME has precision up to .00333 seconds
let tolerance = 0.00334
let now = System.DateTime.Now
let result = runScalarQuery <| cmd.ToTraceString(now)
Assert.InRange(unbox<DateTime> result, now.AddSeconds(-1. * tolerance), now.AddSeconds(tolerance))

[<Fact>]
let ``Roundtrip ToTraceString for datetime2 ``() =
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS DATETIME2)", ConnectionStrings.AdventureWorksNamed>()
// SQL Server DATETIME2 has the same nanosecond precision as DATETIMEOFFSET so there shouldn't be any discrepance
let now = System.DateTime.Now
let result = runScalarQuery <| cmd.ToTraceString(now)
Assert.Equal(expected = now, actual = unbox<DateTime> result)

[<Fact>]
let ``Roundtrip ToTraceString for date time offsets``() =
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS DATETIMEOFFSET)", ConnectionStrings.AdventureWorksNamed>()
let now = System.DateTimeOffset.Now
let result = runScalarQuery <| cmd.ToTraceString(now)
Assert.Equal(expected = now, actual = unbox<DateTimeOffset> result)

[<Fact>]
let ``Roundtrip ToTraceString for nulls``() =
let cmd = new SqlCommandProvider<"SELECT CAST(@x AS NVARCHAR(20))", ConnectionStrings.AdventureWorksNamed>()
let dbnull = Unchecked.defaultof<string>
let result = runScalarQuery <| cmd.ToTraceString(dbnull)
Assert.Equal(expected = box DBNull.Value, actual = result)

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

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

Assert.Equal<string>(
expected = "exec sp_executesql N'INSERT INTO Sales.Currency VALUES(@Code, @Name, GETDATE())',N'@Code NChar(3),@Name NVarChar(50)',@Code='BTC',@Name='Bitcoin'",
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'",
actual = let cmd = new InsertBitCoin() in cmd.ToTraceString( bitCoinCode, bitCoinName)
)

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

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

Expand Down

0 comments on commit a92db69

Please sign in to comment.