fsprojects / SQLProvider

A general F# SQL database erasing type provider, supporting LINQ queries, schema exploration, individuals, CRUD operations and much more besides.
https://fsprojects.github.io/SQLProvider
Other
570 stars 144 forks source link

Query incorrectly parsed when calling a local function and parameter name is more than 4 letters #631

Closed rmunn closed 4 years ago

rmunn commented 4 years ago

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:

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

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