I have a test database containing projects, users, and project memberships, and I have F# records that correspond to each of these tables. I created a FromSql method on each record that looks something like this:
type Project with
static member FromSql (sqlData : sql.dataContext.``testdb.projectsEntity``) = {
Id = sqlData.Id
Name = sqlData.Name
Description = sqlData.Description
// ...
}
Then I wrote a query that looks like:
let projectsQueryAsync (connString : string) () =
let ctx = sql.GetDataContext connString
async {
let sqlProjects = query {
for project in ctx.Testdb.Projects do
select (Project.FromSql project)
}
return! List.executeQueryAsync sqlProjects
}
This failed with the exception "System.FormatException: Input string was not in a correct format." (I've included the full stack trace below).
When I renamed the name project in my query to proj, the query worked. That is, I had a query that looks like:
let sqlProjects = query {
for proj in ctx.Testdb.Projects do
select (Project.FromSql proj)
}
And that was the only change: the rest of projectsQueryAsync was identical.
I finally traced the bug to the resolveTuplePropertyName function in SQLProvider/Utils.fs: it was being called with the name of my projection parameter (i.e., projects), for some reason I don't yet understand. (I lost the thread of the code in that big transform function in SqlRuntime.QueryExpression.fs). There's a code path in resolveTuplePropertyName that assumes that the name it receives will be in the form ItemNNN, so it does this:
let itemid =
if name.Length > 4 then
(int <| name.Remove(0, 4))
else Int32.MaxValue
But when the name it receives is project, it tries to parse the string ect as an int, which of course fails.
The best solution, I think, is to use Int32.TryParse in resolveTuplePropertyName:
let itemid =
if name.Length > 4 then
match Int32.TryParse (name.Remove(0, 4)) with
| (true, n) -> n
| (false, _) -> Int32.MaxValue
else Int32.MaxValue
Since resolveTuplePropertyName appears to be written with the assumption that sometimes, it will be called with something that isn't a tuple property name like Item1 (otherwise why would the if name.Length > 4 check be there?), then it should follow that assumption all the way through, using TryParse to ensure that it never throws an exception if it was given something that wasn't in the format ItemNNN.
Known workarounds
Keeping the name used in the for loop down to four letters or less bypasses the buggy code path, so I just need to do for proj in ctx.Testdb.Projects to work around the bug.
Related information
Used database: MySQL Ver 14.14 Distrib 5.7.27
Operating system: Linux Mint 19 (based on Ubuntu 18.04 Bionic)
SQLProvider version: 1.1.66 from NuGet
.NET Runtime, CoreCLR or Mono Version: .Net Core SDK 2.2.401
Stack trace
System.FormatException: Input string was not in a correct format.
at System.Number.StringToNumber(ReadOnlySpan`1 str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
at System.Number.ParseInt32(ReadOnlySpan`1 s, NumberStyles style, NumberFormatInfo info)
at Microsoft.FSharp.Core.LanguagePrimitives.ParseInt32(String s) in E:\A\_work\130\s\src\fsharp\FSharp.Core\prim-types.fs:line 2280
at FSharp.Data.Sql.Common.Utilities.resolveTuplePropertyName(String name, List`1 tupleIndex)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.|SourceTupleGet|_|$cont@50(List`1 tupleIndex, FSharpMap`2 aliasEntityDict, FSharpOption`1 ultimateChild, Expression e, Unit unitVar)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.|ProjectionItem|_|@215(List`1 tupleIndex, ParameterExpression databaseParam, FSharpMap`2 aliasEntityDict, FSharpOption`1 ultimateChild, Dictionary`2 projectionMap, ExpressionType _arg30, Expression _arg31)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.transform@261(List`1 tupleIndex, ParameterExpression databaseParam, FSharpMap`2 aliasEntityDict, FSharpOption`1 ultimateChild, Dictionary`2 replaceParams, Boolean useCanonicalsOnSelect, Dictionary`2 projectionMap, List`1 groupProjectionMap, FSharpOption`1 en, Expression e)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.transformed@330.Invoke(Expression a)
at Microsoft.FSharp.Collections.Internal.IEnumerator.map@75.DoMoveNext(b& curr) in E:\A\_work\130\s\src\fsharp\FSharp.Core\seq.fs:line 78
at Microsoft.FSharp.Collections.Internal.IEnumerator.MapEnumerator`1.System-Collections-IEnumerator-MoveNext() in E:\A\_work\130\s\src\fsharp\FSharp.Core\seq.fs:line 64
at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items)
at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source)
at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
at System.Dynamic.Utils.CollectionExtensions.ToReadOnly[T](IEnumerable`1 enumerable)
at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, IEnumerable`1 arguments)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.transform@261(List`1 tupleIndex, ParameterExpression databaseParam, FSharpMap`2 aliasEntityDict, FSharpOption`1 ultimateChild, Dictionary`2 replaceParams, Boolean useCanonicalsOnSelect, Dictionary`2 projectionMap, List`1 groupProjectionMap, FSharpOption`1 en, Expression e)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.transform@261(List`1 tupleIndex, ParameterExpression databaseParam, FSharpMap`2 aliasEntityDict, FSharpOption`1 ultimateChild, Dictionary`2 replaceParams, Boolean useCanonicalsOnSelect, Dictionary`2 projectionMap, List`1 groupProjectionMap, FSharpOption`1 en, Expression e)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.transform(Expression projection, List`1 tupleIndex, ParameterExpression databaseParam, FSharpMap`2 aliasEntityDict, FSharpOption`1 ultimateChild, Dictionary`2 replaceParams, Boolean useCanonicalsOnSelect)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.visitExpression@476-1.Invoke(Expression prevProj, ParameterExpression dbParam)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.composeProjections@534[a](Boolean useCanonicalsOnSelect, List`1 entityIndex, SqlQuery sqlQuery, List`1 groupgin, Dictionary`2 replaceParams, ParameterExpression initDbParam, FSharpList`1 projs, LambdaExpression prevLambda, Dictionary`2 foundparams)
at FSharp.Data.Sql.QueryExpression.QueryExpressionTransformer.convertExpression(SqlExp exp, List`1 entityIndex, IDbConnection con, ISqlProvider provider, Boolean isDeleteScript, Boolean useCanonicalsOnSelect)
at FSharp.Data.Sql.Runtime.QueryImplementation.executeQueryAsync@135-1.Invoke(DbConnection _arg1)
at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvoke[T,TResult](AsyncActivation`1 ctxt, TResult result1, FSharpFunc`2 part2) in E:\A\_work\130\s\src\fsharp\FSharp.Core\async.fs:line 398
at Model.projectsQueryAsync@113-4.Invoke(AsyncActivation`1 ctxt) in /home/rmunn/code/bugrepro/Model.fs:line 113
at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in E:\A\_work\130\s\src\fsharp\FSharp.Core\async.fs:line 109
Description
I have a test database containing projects, users, and project memberships, and I have F# records that correspond to each of these tables. I created a
FromSql
method on each record that looks something like this:Then I wrote a query that looks like:
This failed with the exception "System.FormatException: Input string was not in a correct format." (I've included the full stack trace below).
When I renamed the name
project
in my query toproj
, the query worked. That is, I had a query that looks like:And that was the only change: the rest of
projectsQueryAsync
was identical.I finally traced the bug to the
resolveTuplePropertyName
function inSQLProvider/Utils.fs
: it was being called with the name of my projection parameter (i.e.,projects
), for some reason I don't yet understand. (I lost the thread of the code in that bigtransform
function in SqlRuntime.QueryExpression.fs). There's a code path inresolveTuplePropertyName
that assumes that the name it receives will be in the formItemNNN
, so it does this:But when the name it receives is
project
, it tries to parse the stringect
as an int, which of course fails.The best solution, I think, is to use
Int32.TryParse
inresolveTuplePropertyName
:Since
resolveTuplePropertyName
appears to be written with the assumption that sometimes, it will be called with something that isn't a tuple property name like Item1 (otherwise why would theif name.Length > 4
check be there?), then it should follow that assumption all the way through, using TryParse to ensure that it never throws an exception if it was given something that wasn't in the format ItemNNN.Known workarounds
Keeping the name used in the
for
loop down to four letters or less bypasses the buggy code path, so I just need to dofor proj in ctx.Testdb.Projects
to work around the bug.Related information
Stack trace