Skip to content

Commit

Permalink
Initial SQL parser implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaid-Ajaj committed Aug 20, 2020
1 parent 3793b18 commit 5f170de
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 0 deletions.
15 changes: 15 additions & 0 deletions NpgsqlFSharpAnalyzer.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NpgsqlFSharpVs", "src\Npgsq
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "NpgsqlFSharpAnalyzer.Core", "src\NpgsqlFSharpAnalyzer.Core\NpgsqlFSharpAnalyzer.Core.fsproj", "{5964BB56-97B8-4FAE-9933-8113DB11438D}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NpgsqlFSharpParser", "src\NpgsqlFSharpParser\NpgsqlFSharpParser.fsproj", "{BC524F8E-6282-4E31-9A0E-29FCE38832E7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -103,6 +105,18 @@ Global
{5964BB56-97B8-4FAE-9933-8113DB11438D}.Release|x64.Build.0 = Release|Any CPU
{5964BB56-97B8-4FAE-9933-8113DB11438D}.Release|x86.ActiveCfg = Release|Any CPU
{5964BB56-97B8-4FAE-9933-8113DB11438D}.Release|x86.Build.0 = Release|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Debug|x64.ActiveCfg = Debug|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Debug|x64.Build.0 = Debug|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Debug|x86.ActiveCfg = Debug|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Debug|x86.Build.0 = Debug|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Release|Any CPU.Build.0 = Release|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Release|x64.ActiveCfg = Release|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Release|x64.Build.0 = Release|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Release|x86.ActiveCfg = Release|Any CPU
{BC524F8E-6282-4E31-9A0E-29FCE38832E7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -114,6 +128,7 @@ Global
{8FC9B600-2A16-4E0C-89D7-F61F5F315F3E} = {4CA3F686-DFAB-4AAF-8004-4158E18EF24E}
{37577282-1289-40DB-AD3D-24499BD09DAE} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216}
{5964BB56-97B8-4FAE-9933-8113DB11438D} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216}
{BC524F8E-6282-4E31-9A0E-29FCE38832E7} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BC821061-2FB3-4ABD-9FA1-044D4C59C475}
Expand Down
16 changes: 16 additions & 0 deletions src/NpgsqlFSharpParser/NpgsqlFSharpParser.fsproj
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>
111 changes: 111 additions & 0 deletions src/NpgsqlFSharpParser/Parser.fs
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
82 changes: 82 additions & 0 deletions src/NpgsqlFSharpParser/Types.fs
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<Compile Include="ParserTests.fs" />
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Analyzer.fs" />
<Compile Include="Tests.fs" />
Expand All @@ -14,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\NpgsqlFSharpAnalyzer\NpgsqlFSharpAnalyzer.fsproj" />
<ProjectReference Include="..\..\src\NpgsqlFSharpAnalyzer.Core\NpgsqlFSharpAnalyzer.Core.fsproj" />
<ProjectReference Include="..\..\src\NpgsqlFSharpParser\NpgsqlFSharpParser.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Expecto" Version="8.13.1" />
Expand Down
112 changes: 112 additions & 0 deletions tests/NpgsqlFSharpAnalyzer.Tests/ParserTests.fs
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")))
})))
}
]

0 comments on commit 5f170de

Please sign in to comment.