forked from Zaid-Ajaj/Npgsql.FSharp.Analyzer
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="Types.fs" /> | ||
<Compile Include="Parser.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="FParsec" Version="1.1.1" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
[<RequireQualifiedAccess>] | ||
module NpgsqlFSharpParser.Parser | ||
|
||
open FParsec | ||
|
||
let identifier : Parser<Expr, unit> = | ||
let isIdentifierFirstChar token = isLetter token | ||
let isIdentifierChar token = isLetter token || isDigit token || token = '.' || token = '_' | ||
many1Satisfy2L isIdentifierFirstChar isIdentifierChar "identifier" .>> spaces | ||
|>> Expr.Ident | ||
|
||
let parameter : Parser<Expr, unit> = | ||
let isIdentifierFirstChar token = token = '@' | ||
let isIdentifierChar token = isLetter token || isDigit token || token = '_' | ||
many1Satisfy2L isIdentifierFirstChar isIdentifierChar "identifier" .>> spaces | ||
|>> Expr.Parameter | ||
|
||
let text value : Parser<string, unit> = | ||
(optional spaces) >>. pstringCI value .>> (optional spaces) | ||
|
||
let star : Parser<Expr, unit> = | ||
text "*" |>> fun _ -> Expr.Star | ||
|
||
let opp = new OperatorPrecedenceParser<Expr, unit, unit>() | ||
|
||
let expr = opp.ExpressionParser | ||
|
||
let parens parser = between (text "(") (text ")") parser | ||
|
||
let commaSeparatedExprs = sepBy expr (text ",") | ||
|
||
let selections = | ||
(star |>> List.singleton) | ||
<|> (attempt commaSeparatedExprs) | ||
<|> (attempt (parens commaSeparatedExprs)) | ||
|
||
let functionExpr = | ||
let isIdentifierFirstChar token = isLetter token | ||
let isIdentifierChar token = isLetter token || isDigit token || token = '.' || token = '_' | ||
many1Satisfy2L isIdentifierFirstChar isIdentifierChar "identifier" .>> spaces | ||
>>= fun functionName -> | ||
(parens commaSeparatedExprs) | ||
|>> fun arguments -> Expr.Function(functionName, arguments) | ||
|
||
let optionalDistinct = | ||
optional (attempt (text "DISTINCT ON") <|> attempt (text "DISTINCT")) | ||
|
||
let selectFromWhere = | ||
text "SELECT" >>= fun _ -> | ||
optionalDistinct >>= fun _ -> | ||
selections >>= fun selections -> | ||
text "FROM" >>= fun _ -> | ||
identifier >>= fun tableName -> | ||
text "WHERE" >>. expr |>> fun condition -> | ||
let query = | ||
{ SelectExpr.Default with | ||
Columns = selections | ||
From = Some tableName | ||
Where = Some condition } | ||
|
||
Expr.Query (TopLevelExpr.Select query) | ||
|
||
let selectFrom = | ||
text "SELECT" >>= fun _ -> | ||
optionalDistinct >>= fun _ -> | ||
selections >>= fun selections -> | ||
text "FROM" >>= fun _ -> | ||
identifier |>> fun tableName -> | ||
let query = | ||
{ SelectExpr.Default with | ||
Columns = selections | ||
From = Some tableName } | ||
|
||
Expr.Query (TopLevelExpr.Select query) | ||
|
||
let primitiveSelect = | ||
text "SELECT" >>= fun _ -> | ||
optionalDistinct >>= fun _ -> | ||
selections |>> fun selections -> | ||
let query = | ||
{ SelectExpr.Default with | ||
Columns = selections } | ||
|
||
Expr.Query (TopLevelExpr.Select query) | ||
|
||
opp.AddOperator(InfixOperator("AND", spaces, 7, Associativity.Left, fun left right -> Expr.And(left, right))) | ||
opp.AddOperator(InfixOperator("OR", spaces, 7, Associativity.Left, fun left right -> Expr.Or(left, right))) | ||
opp.AddOperator(InfixOperator("IN", spaces, 8, Associativity.Left, fun left right -> Expr.In(left, right))) | ||
opp.AddOperator(InfixOperator(">", spaces, 9, Associativity.Left, fun left right -> Expr.GreaterThan(left, right))) | ||
opp.AddOperator(InfixOperator("<", spaces, 9, Associativity.Left, fun left right -> Expr.LessThan(left, right))) | ||
opp.AddOperator(InfixOperator("<=", spaces, 9, Associativity.Left, fun left right -> Expr.LessThanOrEqual(left, right))) | ||
opp.AddOperator(InfixOperator(">=", spaces, 9, Associativity.Left, fun left right -> Expr.GreaterThanOrEqual(left, right))) | ||
opp.AddOperator(InfixOperator("=", spaces, 9, Associativity.Left, fun left right -> Expr.Equals(left, right))) | ||
opp.AddOperator(PostfixOperator("IS NULL", spaces, 8, false, fun value -> Expr.Equals(Expr.Null, value))) | ||
opp.AddOperator(PostfixOperator("IS NOT NULL", spaces, 8, false, fun value -> Expr.Not(Expr.Equals(Expr.Null, value)))) | ||
|
||
opp.TermParser <- choice [ | ||
star | ||
(text "(") >>. expr .>> (text ")") | ||
(attempt selectFromWhere <|> attempt selectFrom <|> attempt primitiveSelect) | ||
(attempt functionExpr) | ||
identifier | ||
parameter | ||
] | ||
|
||
let fullParser = (optional spaces) >>. expr .>> (optional spaces <|> (text ";" |>> fun _ -> ())) | ||
|
||
let parse (input: string) : Result<Expr, string> = | ||
match run fullParser input with | ||
| Success(result,_,_) -> Result.Ok result | ||
| Failure(errMsg,_,_) -> Result.Error errMsg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
namespace rec NpgsqlFSharpParser | ||
|
||
[<RequireQualifiedAccess>] | ||
type TopLevelExpr = | ||
| Select of SelectExpr | ||
| Insert of InsertExpr | ||
| Delete of DeleteExpr | ||
| Update of UpdateExpr | ||
|
||
[<RequireQualifiedAccess>] | ||
type Expr = | ||
| Star | ||
| Ident of string | ||
| Parameter of string | ||
| Boolean of bool | ||
| Integer of int | ||
| Float of float | ||
| Null | ||
| Function of name:string * arguments:Expr list | ||
| And of left:Expr * right:Expr | ||
| Or of left:Expr * right:Expr | ||
| In of left:Expr * right:Expr | ||
| Not of expr:Expr | ||
| Equals of left:Expr * right:Expr | ||
| GreaterThan of left:Expr * right:Expr | ||
| LessThan of left:Expr * right:Expr | ||
| GreaterThanOrEqual of left:Expr * right:Expr | ||
| LessThanOrEqual of left:Expr * right:Expr | ||
| Between of value:Expr * leftBound:Expr * rightBound:Expr | ||
| Query of expr:TopLevelExpr | ||
|
||
type Ordering = { | ||
Column : string | ||
ASC : bool | ||
DESC : bool | ||
NullFirst : bool | ||
NullLast : bool | ||
} | ||
|
||
type JoinExpr = | ||
| Join of tableName:string | ||
| LeftOuterJoin of tableName:string | ||
| RightOuterJoin of tableName:string | ||
| FullOuterJoin of tableName:string | ||
|
||
type SelectExpr = { | ||
Columns : Expr list | ||
From : Expr option | ||
Joins : JoinExpr list | ||
Where : Expr option | ||
OrderBy : Ordering list | ||
GroupBy : Expr list | ||
} | ||
with | ||
static member Default = { | ||
Columns = [ ] | ||
From = None | ||
Where = None | ||
OrderBy = [ ] | ||
GroupBy = [ ] | ||
Joins = [ ] | ||
} | ||
|
||
type UpdateExpr = { | ||
Table : string | ||
Assignments : Map<string, Expr list> | ||
ConflictResolution : Map<string, Expr list> | ||
Returning : Expr list | ||
} | ||
|
||
type DeleteExpr = { | ||
Table : string | ||
Where : Expr option | ||
} | ||
|
||
type InsertExpr = { | ||
Table: string | ||
Columns : string list | ||
Values : Expr list | ||
ConflictResolution : Map<string, Expr list> | ||
Returning : Expr list | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
module ParserTests | ||
|
||
open Expecto | ||
open NpgsqlFSharpParser | ||
|
||
let testSelect query expected = | ||
test query { | ||
match Parser.parse query with | ||
| Ok (Expr.Query (TopLevelExpr.Select query)) -> | ||
Expect.equal query expected "The query is parsed correctly" | ||
| Ok somethingElse -> | ||
failwithf "Unexpected select statement %A" somethingElse | ||
| Error errorMsg -> | ||
failwith errorMsg | ||
} | ||
|
||
let ftestSelect query expected = | ||
ftest query { | ||
match Parser.parse query with | ||
| Ok (Expr.Query (TopLevelExpr.Select query)) -> | ||
Expect.equal query expected "The query is parsed correctly" | ||
| Ok somethingElse -> | ||
failwithf "Unexpected select statement %A" somethingElse | ||
| Error errorMsg -> | ||
failwith errorMsg | ||
} | ||
|
||
[<Tests>] | ||
let parserTests = testList "Parser tests" [ | ||
|
||
testSelect "SELECT NOW()" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Function("NOW", [])] | ||
} | ||
|
||
testSelect "SELECT NOW();" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Function("NOW", [])] | ||
} | ||
|
||
testSelect "SELECT username, user_id FROM users" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Ident "username"; Expr.Ident "user_id"] | ||
From = Some (Expr.Ident "users") | ||
} | ||
|
||
testSelect "SELECT DISTINCT username, user_id FROM users" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Ident "username"; Expr.Ident "user_id"] | ||
From = Some (Expr.Ident "users") | ||
} | ||
|
||
testSelect "SELECT DISTINCT ON (username, user_id) FROM users" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Ident "username"; Expr.Ident "user_id"] | ||
From = Some (Expr.Ident "users") | ||
} | ||
|
||
testSelect "SELECT COUNT(*) FROM users" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Function("COUNT", [Expr.Star]) ] | ||
From = Some (Expr.Ident "users") | ||
} | ||
|
||
testSelect "SELECT COUNT(*) FROM users WHERE last_login > @login_date" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Function("COUNT", [Expr.Star]) ] | ||
From = Some (Expr.Ident "users") | ||
Where = Some (Expr.GreaterThan(Expr.Ident "last_login", Expr.Parameter "@login_date")) | ||
} | ||
|
||
testSelect "SELECT COUNT(*) FROM users WHERE last_login > NOW();" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Function("COUNT", [Expr.Star]) ] | ||
From = Some (Expr.Ident "users") | ||
Where = Some (Expr.GreaterThan(Expr.Ident "last_login", Expr.Function("NOW", [ ]))) | ||
} | ||
|
||
testSelect "SELECT * FROM users WHERE user_id = @user_id" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Star] | ||
From = Some (Expr.Ident "users") | ||
Where = Some (Expr.Equals(Expr.Ident "user_id", Expr.Parameter "@user_id")) | ||
} | ||
|
||
testSelect """ | ||
SELECT value, timestamp | ||
FROM meters | ||
WHERE timestamp >= @from AND timestamp < @to | ||
""" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Ident "value"; Expr.Ident "timestamp"] | ||
From = Some (Expr.Ident "meters") | ||
Where = Some (Expr.And(Expr.GreaterThanOrEqual(Expr.Ident "timestamp", Expr.Parameter "@from"), Expr.LessThan(Expr.Ident "timestamp", Expr.Parameter "@to"))) | ||
} | ||
|
||
testSelect """ | ||
SELECT username, email | ||
FROM users | ||
WHERE user_id IN (SELECT id FROM user_ids WHERE id IS NOT NULL) | ||
""" { | ||
SelectExpr.Default with | ||
Columns = [Expr.Ident "username"; Expr.Ident "email"] | ||
From = Some (Expr.Ident "users") | ||
Where = Some (Expr.In(Expr.Ident "user_id", Expr.Query(TopLevelExpr.Select { | ||
SelectExpr.Default with | ||
Columns = [Expr.Ident "id"] | ||
From = Some (Expr.Ident "user_ids") | ||
Where = Some(Expr.Not(Expr.Equals(Expr.Null, Expr.Ident "id"))) | ||
}))) | ||
} | ||
] |