Skip to content

Commit

Permalink
Determining parameter nullability for INSERT queries
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaid-Ajaj committed Aug 26, 2020
1 parent 1aaea18 commit 322a634
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 8 deletions.
6 changes: 4 additions & 2 deletions src/NpgsqlFSharpAnalyzer.Core/InformationSchema.fs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ module InformationSchema =
Precision: byte
Scale : byte
Optional: bool
DataType: DataType }
DataType: DataType
IsNullable : bool }
with

member this.Size = this.MaxLength
Expand Down Expand Up @@ -249,7 +250,8 @@ module InformationSchema =
Precision = p.Precision
Scale = p.Scale
Optional = allParametersOptional
DataType = DataType.Create(p.PostgresType) } ]
DataType = DataType.Create(p.PostgresType)
IsNullable = true } ]

let enums =
outputColumns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@
<PackageReference Include="FSharp.Compiler.Service" Version="36.0.3" />
<PackageReference Include="Npgsql" Version="4.1.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NpgsqlFSharpParser\NpgsqlFSharpParser.fsproj" />
</ItemGroup>
</Project>
71 changes: 65 additions & 6 deletions src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,63 @@ namespace Npgsql.FSharp.Analyzers.Core
open System
open FSharp.Compiler.Range
open F23.StringSimilarity
open NpgsqlFSharpParser
open InformationSchema

module SqlAnalysis =

let determineParameterNullability (parameters: Parameter list) (schema: DbSchemaLookups) query =
match Parser.parse query with
| Result.Error error -> parameters
| Result.Ok (Expr.SelectQuery query) ->
match query.Limit with
| None -> parameters
| Some (Expr.Parameter(name)) ->
let parameter = name.TrimStart('@')
parameters
|> List.map (fun param ->
if param.Name = parameter
then { param with IsNullable = false }
else param
)
| Some otherExpr ->
parameters

| Result.Ok (Expr.InsertQuery query) ->

let findParameter (expr, column) =
match expr with
| Expr.Parameter parameterName ->
Some (parameterName.TrimStart '@', column)
| _ ->
None

let insertParameters =
query.Columns
|> List.zip query.Values
|> List.choose findParameter

let columnNonNullable columnName =
schema.Columns
|> Seq.exists (fun column -> column.Value.Name = columnName && not column.Value.Nullable)

parameters
|> List.map (fun requiredParameter ->
insertParameters
|> List.tryFind (fun (parameter, column) -> parameter = requiredParameter.Name && columnNonNullable column)
|> function
| None -> requiredParameter
| Some _ -> { requiredParameter with IsNullable = false }
)

| Result.Ok expr ->
parameters

let extractParametersAndOutputColumns(connectionString, commandText, dbSchemaLookups) =
try
let parameters, output, enums = InformationSchema.extractParametersAndOutputColumns(connectionString, commandText, false, dbSchemaLookups)
Result.Ok (parameters, output)
let enrichedParameters = determineParameterNullability parameters dbSchemaLookups commandText
Result.Ok (enrichedParameters, output)
with
| ex ->
Result.Error ex.Message
Expand Down Expand Up @@ -166,8 +217,12 @@ module SqlAnalysis =
if providedParam.paramFunc <> "Sql.int16" && providedParam.paramFunc <> "Sql.int16OrNone" && providedParam.paramFunc <> "Sql.dbnull"
then yield typeMismatch [ "Sql.int16"; "Sql.int16OrNone"; "Sql.dbnull" ]
| ("int64" | "bigint" |"bigserial") ->
if providedParam.paramFunc <> "Sql.int64" && providedParam.paramFunc <> "Sql.int64OrNone" && providedParam.paramFunc <> "Sql.dbnull"
then yield typeMismatch [ "Sql.int64"; "Sql.int64OrNone"; "Sql.dbnull" ]
if requiredParam.IsNullable then
if providedParam.paramFunc <> "Sql.int64" && providedParam.paramFunc <> "Sql.int" && providedParam.paramFunc <> "Sql.int64OrNone" && providedParam.paramFunc <> "Sql.dbnull"
then yield typeMismatch [ "Sql.int64"; "Sql.int"; "Sql.int64OrNone"; "Sql.dbnull" ]
else
if providedParam.paramFunc <> "Sql.int64" && providedParam.paramFunc <> "Sql.int"
then yield typeMismatch [ "Sql.int64"; "Sql.int" ]
| ("numeric" | "decimal" | "money") ->
if providedParam.paramFunc <> "Sql.decimal" && providedParam.paramFunc <> "Sql.decimalOrNone" && providedParam.paramFunc <> "Sql.dbnull"
then yield typeMismatch [ "Sql.decimal"; "Sql.decimalOrNone"; "Sql.dbnull" ]
Expand Down Expand Up @@ -195,8 +250,12 @@ module SqlAnalysis =
then yield typeMismatch [ "Sql.jsonb"; "Sql.jsonbOrNone"; "Sql.dbnull" ]

| ("text"|"json"|"xml") ->
if providedParam.paramFunc <> "Sql.text" && providedParam.paramFunc <> "Sql.textOrNone" && providedParam.paramFunc <> "Sql.string" && providedParam.paramFunc <> "Sql.textOrNone" && providedParam.paramFunc <> "Sql.dbnull"
then yield typeMismatch [ "Sql.text"; "Sql.string"; "Sql.textOrNone"; "Sql.stringOrNone"; "Sql.dbnull" ]
if requiredParam.IsNullable then
if providedParam.paramFunc <> "Sql.text" && providedParam.paramFunc <> "Sql.textOrNone" && providedParam.paramFunc <> "Sql.string" && providedParam.paramFunc <> "Sql.textOrNone" && providedParam.paramFunc <> "Sql.dbnull"
then yield typeMismatch [ "Sql.text"; "Sql.string"; "Sql.textOrNone"; "Sql.stringOrNone"; "Sql.dbnull" ]
else
if providedParam.paramFunc <> "Sql.text" && providedParam.paramFunc <> "Sql.string"
then yield typeMismatch [ "Sql.text"; "Sql.string" ]
| _ ->
()
else
Expand Down Expand Up @@ -508,7 +567,7 @@ module SqlAnalysis =
match queryAnalysis with
| Result.Error queryError ->
[ createWarning queryError queryRange ]
| Result.Ok (parameters, outputColunms) ->
| Result.Ok (parameters, outputColunms) ->
let readingAttempts = defaultArg (findColumnReadAttempts operation) [ ]
[
yield! analyzeParameters operation parameters
Expand Down
48 changes: 48 additions & 0 deletions tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,54 @@ let tests =
failwith "Expected only one error message"
}

test "SQL query semantic analysis: delect incorrectly used nullable parameter on LIMIT expression" {
use db = createTestDatabase()

Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text not null)"
|> Sql.executeNonQuery
|> ignore

match context (find "../examples/hashing/limitExpressionTest.fs") with
| None -> failwith "Could not crack project"
| Some context ->
match SqlAnalysis.databaseSchema db.ConnectionString with
| Result.Error connectionError ->
failwith connectionError
| Result.Ok schema ->
let operation = List.exactlyOne (SyntacticAnalysis.findSqlOperations context)
let messages = SqlAnalysis.analyzeOperation operation db.ConnectionString schema
match messages with
| [ message ] ->
Expect.stringContains message.Message "Sql.int64 or Sql.int" "Message contains suggestion to use Sql.stringArray"
| _ ->
failwith "Expected only one error message"
}

test "SQL query semantic analysis: delect incorrectly used nullable parameter on non-nullable column insert" {
use db = createTestDatabase()

Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text not null, active boolean not null)"
|> Sql.executeNonQuery
|> ignore

match context (find "../examples/hashing/insertQueryWithNonNullableParameter.fs") with
| None -> failwith "Could not crack project"
| Some context ->
match SqlAnalysis.databaseSchema db.ConnectionString with
| Result.Error connectionError ->
failwith connectionError
| Result.Ok schema ->
let operation = List.exactlyOne (SyntacticAnalysis.findSqlOperations context)
let messages = SqlAnalysis.analyzeOperation operation db.ConnectionString schema
match messages with
| [ message ] ->
Expect.stringContains message.Message "Sql.text or Sql.string" "Message contains suggestion to use Sql.stringArray"
| _ ->
failwith "Expected only one error message"
}

test "SQL query semantic analysis: type mismatch with integer/serial" {
use db = createTestDatabase()

Expand Down
2 changes: 2 additions & 0 deletions tests/examples/hashing/examples.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="insertQueryWithNonNullableParameter.fs" />
<Compile Include="limitExpressionTest.fs" />
<Compile Include="executingQueryInsteadOfNonQuery.fs" />
<Compile Include="usingIntArrayParameter.fs" />
<Compile Include="readingUuidArray.fs" />
Expand Down
9 changes: 9 additions & 0 deletions tests/examples/hashing/insertQueryWithNonNullableParameter.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module InsertQueryWithNonNullableParameter

open Npgsql.FSharp

let findUsers connection =
connection
|> Sql.query "INSERT INTO users (username, active) VALUES (@username, @active) RETURNING *"
|> Sql.parameters [ "@username", Sql.dbnull; "@active", Sql.bool true ]
|> Sql.execute (fun read -> read.text "username")
9 changes: 9 additions & 0 deletions tests/examples/hashing/limitExpressionTest.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module LimitExpressionTest

open Npgsql.FSharp

let findUsers connection =
connection
|> Sql.query "SELECT * FROM users LIMIT @numberOfUsers"
|> Sql.parameters [ "@numberOfUsers", Sql.int64OrNone (Some 42L) ]
|> Sql.execute (fun read -> read.text "username")

0 comments on commit 322a634

Please sign in to comment.