Skip to content

Commit

Permalink
Better suggestions and code fixes for non-nullable parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaid-Ajaj committed Aug 27, 2020
1 parent e1680bf commit df8afcd
Show file tree
Hide file tree
Showing 7 changed files with 452 additions and 45 deletions.
331 changes: 298 additions & 33 deletions src/NpgsqlFSharpAnalyzer.Core/SqlAnalysis.fs

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/NpgsqlFSharpVs/NpgsqlFSharpVs.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@
<PackageReference Include="Dotnet.ProjInfo.Workspace.FCS">
<Version>0.43.0</Version>
</PackageReference>
<PackageReference Include="FParsec">
<Version>1.1.1</Version>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.SDK" Version="16.0.206" ExcludeAssets="runtime">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down Expand Up @@ -133,6 +136,10 @@
<Project>{5964bb56-97b8-4fae-9933-8113db11438d}</Project>
<Name>NpgsqlFSharpAnalyzer.Core</Name>
</ProjectReference>
<ProjectReference Include="..\NpgsqlFSharpParser\NpgsqlFSharpParser.fsproj">
<Project>{bc524f8e-6282-4e31-9a0e-29fce38832e7}</Project>
<Name>NpgsqlFSharpParser</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\VSSDK\Microsoft.VsSDK.targets" Condition="'$(VSToolsPath)' != ''" />
Expand Down
108 changes: 96 additions & 12 deletions tests/NpgsqlFSharpAnalyzer.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ let analyzers = [
let inline find file = IO.Path.Combine(__SOURCE_DIRECTORY__ , file)
let project = IO.Path.Combine(__SOURCE_DIRECTORY__, "../examples/hashing/examples.fsproj")

let raiseWhenFailed = function
| Result.Ok _ -> ()
| Result.Error error -> raise error

let inline context file =
AnalyzerBootstrap.context file
|> Option.map SqlAnalyzer.sqlAnalyzerContext
Expand Down Expand Up @@ -166,7 +170,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, roles text[] not null)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

let databaseMetadata = InformationSchema.getDbSchemaLookups db.ConnectionString

Expand All @@ -193,7 +197,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text not null, active bit not null)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

let databaseMetadata = InformationSchema.getDbSchemaLookups db.ConnectionString

Expand All @@ -209,7 +213,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text not null, active bit not null)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

let databaseMetadata = InformationSchema.getDbSchemaLookups db.ConnectionString
let query = "SELECT COUNT(*) FROM users"
Expand All @@ -236,7 +240,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query createFuncQuery
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

let databaseMetadata = InformationSchema.getDbSchemaLookups db.ConnectionString
let query = "SELECT Increment(@Input)"
Expand All @@ -257,7 +261,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text not null, active bit not null)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

match context (find "../examples/hashing/semanticAnalysis-missingColumn.fs") with
| None -> failwith "Could not crack project"
Expand All @@ -282,7 +286,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text not null, active bit not null)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

match context (find "../examples/hashing/semanticAnalysis-missingParameter.fs") with
| None -> failwith "Could not crack project"
Expand All @@ -307,7 +311,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text not null, active bit not null)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

match context (find "../examples/hashing/semanticAnalysis-typeMismatch.fs") with
| None -> failwith "Could not crack project"
Expand All @@ -331,7 +335,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, roles text[] not null)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

match context (find "../examples/hashing/readingTextArray.fs") with
| None -> failwith "Could not crack project"
Expand All @@ -355,7 +359,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, codes uuid[] not null)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

match context (find "../examples/hashing/readingUuidArray.fs") with
| None -> failwith "Could not crack project"
Expand All @@ -379,7 +383,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id bigserial primary key, username text)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

match context (find "../examples/hashing/usingIntArrayParameter.fs") with
| None -> failwith "Could not crack project"
Expand Down Expand Up @@ -451,7 +455,7 @@ let tests =
Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id serial primary key, username text not null, active bit not null)"
|> Sql.executeNonQuery
|> ignore
|> raiseWhenFailed

match context (find "../examples/hashing/readAttemptIntegerTypeMismatch.fs") with
| None -> failwith "Could not crack project"
Expand All @@ -469,13 +473,93 @@ let tests =
failwith "Expected only one error message"
}

test "SQL query semantic analysis: type mismatch with comparing against non-nullable column during SELECT" {
use db = createTestDatabase()

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

match context (find "../examples/hashing/selectWithNonNullableColumnComparison.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.int instead" "Message contains suggestion to use Sql.readBool"
| _ ->
failwith "Expected only one error message"
}


test "SQL query semantic analysis: type mismatch with comparing against non-nullable column during SELECT with JOINS" {
use db = createTestDatabase()

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

Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE user_roles (user_role_id serial primary key, role_description text not null, user_id int not null references users(user_id))"
|> Sql.executeNonQuery
|> raiseWhenFailed

match context (find "../examples/hashing/selectWithInnerJoins.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.int instead" "Message contains suggestion to use Sql.readBool"
| _ ->
failwith "Expected only one error message"
}


test "SQL query semantic analysis: type mismatch with comparing against non-nullable column during UPDATE with assignments" {
use db = createTestDatabase()

Sql.connect db.ConnectionString
|> Sql.query "CREATE TABLE users (user_id serial primary key, username text not null, last_login timestamptz)"
|> Sql.executeNonQuery
|> raiseWhenFailed

match context (find "../examples/hashing/updateQueryWithParametersInAssignments.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
| [ firstMessage; secondMessage ] ->
Expect.stringContains firstMessage.Message "Sql.int instead" "Suggest to use non-nullable Sql.int"
Expect.stringContains secondMessage.Message "Sql.text or Sql.string" "Suggest to use non-nullable text"
| _ ->
failwith "Expected only one error message"
}

test "SQL query semantic analysis: redundant parameters" {
use db = createTestDatabase()

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

match context (find "../examples/hashing/semanticAnalysis-redundantParameters.fs") with
| None -> failwith "Could not crack project"
Expand Down
3 changes: 3 additions & 0 deletions tests/examples/hashing/examples.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
</PropertyGroup>

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

open Npgsql.FSharp

let [<Literal>] usersWithRoles = """
SELECT * FROM users
JOIN user_roles ON users.user_id = user_roles.user_id
WHERE users.user_id = @user_id AND role_description = @role
"""

let usersAndTheirRoles connectionString =
connectionString
|> Sql.connect
|> Sql.query usersWithRoles
|> Sql.parameters [ "@user_id", Sql.intOrNone None; "@role", Sql.text "User" ]
|> Sql.execute (fun read -> read.int "user_id")
10 changes: 10 additions & 0 deletions tests/examples/hashing/selectWithNonNullableColumnComparison.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module SelectWithNonNullableColumnComparison

open Npgsql.FSharp

let findUsernames connectionString =
connectionString
|> Sql.connect
|> Sql.query "SELECT * FROM users WHERE user_id = @user_id AND username = @username"
|> Sql.parameters [ "@user_id", Sql.intOrNone None; "@username", Sql.text "User" ]
|> Sql.execute (fun read -> read.int "user_id")
22 changes: 22 additions & 0 deletions tests/examples/hashing/updateQueryWithParametersInAssignments.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module UpdateQueryWithParametersInAssignments

open Npgsql.FSharp
open System

let [<Literal>] updateUsernameQuery = """
UPDATE users
SET username = @username, last_login = @last_login
WHERE user_id = @user_id
RETURNING *
"""

let usersAndTheirRoles connectionString =
connectionString
|> Sql.connect
|> Sql.query updateUsernameQuery
|> Sql.parameters [
"@user_id", Sql.intOrNone (Some 10)
"@username", Sql.textOrNone (Some "John")
"@last_login", Sql.timestamptz DateTime.Now
]
|> Sql.execute (fun read -> read.int "user_id")

0 comments on commit df8afcd

Please sign in to comment.