From af51ee92bba87d94f753d3ab412bbcb70826bd9a Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Fri, 14 May 2021 12:34:30 +0200 Subject: [PATCH 1/6] [WIP] Add fetch queries --- src/NpgsqlFSharpParser/Parser.fs | 12 +++++++ src/NpgsqlFSharpParser/Types.fs | 27 ++++++++++++++ .../NpgsqlFSharpAnalyzer.Tests.fsproj | 1 + .../ParseFetchTests.fs | 35 +++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 tests/NpgsqlFSharpAnalyzer.Tests/ParseFetchTests.fs diff --git a/src/NpgsqlFSharpParser/Parser.fs b/src/NpgsqlFSharpParser/Parser.fs index a5047ef..166decf 100644 --- a/src/NpgsqlFSharpParser/Parser.fs +++ b/src/NpgsqlFSharpParser/Parser.fs @@ -437,6 +437,17 @@ let declareQuery = } preturn (Expr.DeclareQuery (Cursor query)) +let fetchQuery = + text "FETCH" >>. + pint32 >>= fun count -> + text "FROM" >>. + simpleIdentifier >>= fun cursor -> + let query = { + CursorName = cursor + Direction = Direction.Forward count + } + preturn (Expr.FetchQuery query) + let spacesOrComment = let comment = skipString "/*" >>. (charsTillString "*/" true 8096) let commentEol = skipString "--" >>. skipRestOfLine true @@ -482,6 +493,7 @@ opp.TermParser <- choice [ (attempt selectQuery) (attempt setQuery) (attempt declareQuery) + (attempt fetchQuery) (attempt functionExpr) (text "(") >>. expr .>> (text ")") valueList diff --git a/src/NpgsqlFSharpParser/Types.fs b/src/NpgsqlFSharpParser/Types.fs index 5929be0..dd947ef 100644 --- a/src/NpgsqlFSharpParser/Types.fs +++ b/src/NpgsqlFSharpParser/Types.fs @@ -37,6 +37,7 @@ type Expr = | UpdateQuery of expr: UpdateExpr | SetQuery of expr: SetExpr | DeclareQuery of expr: DeclareExpr + | FetchQuery of expr: FetchExpr type Ordering = | Asc of columnName:string @@ -148,6 +149,32 @@ type CursorDeclaration = { type DeclareExpr = | Cursor of CursorDeclaration +[] +type Direction = + /// Fetch next row. Same as Forward + | Next + /// Fetch prior row. Same as Backward + | Prior + | Absolute of int // First = Absolute 1, Last = Absolute -1 + | Relative of int + | Forward of int // Same as count + | Backward of int + /// Fetch all remaining rows. Same as ForwardAll + | All + | BackwardAll + +type FetchExpr = { + // An open cursor name. + CursorName: string + // Defines the fetch direction. + Direction: Direction +} with + static member Default = + { + CursorName = "" + Direction = Direction.Next + } + [] type DataType = | Integer diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/NpgsqlFSharpAnalyzer.Tests.fsproj b/tests/NpgsqlFSharpAnalyzer.Tests/NpgsqlFSharpAnalyzer.Tests.fsproj index 0ddbaa7..9a3366e 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/NpgsqlFSharpAnalyzer.Tests.fsproj +++ b/tests/NpgsqlFSharpAnalyzer.Tests/NpgsqlFSharpAnalyzer.Tests.fsproj @@ -12,6 +12,7 @@ + diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/ParseFetchTests.fs b/tests/NpgsqlFSharpAnalyzer.Tests/ParseFetchTests.fs new file mode 100644 index 0000000..7e77113 --- /dev/null +++ b/tests/NpgsqlFSharpAnalyzer.Tests/ParseFetchTests.fs @@ -0,0 +1,35 @@ +module ParseFetchTests + +open Expecto +open NpgsqlFSharpParser + +let testFetch inputQuery expected = + test inputQuery { + match Parser.parse inputQuery with + | Ok (Expr.FetchQuery query) -> + Expect.equal query expected "The query is parsed correctly" + | Ok somethingElse -> + failwithf "Unexpected fetch statement %A" somethingElse + | Error errorMsg -> + failwith errorMsg + } + +let ftestFetch inputQuery expected = + ftest inputQuery { + match Parser.parse inputQuery with + | Ok (Expr.FetchQuery query) -> + Expect.equal query expected "The query is parsed correctly" + | Ok somethingElse -> + failwithf "Unexpected fetch statement %A" somethingElse + | Error errorMsg -> + failwith errorMsg + } + +[] +let fetchQueryTests = testList "Parse FETCH queries" [ + testFetch "FETCH 10 FROM c1" { + FetchExpr.Default with + CursorName = "c1" + Direction = Direction.Forward 10 + } +] From 29568783ddaf45e2999b702e87a1c3ed4ac015c4 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 5 Aug 2021 09:46:18 +0200 Subject: [PATCH 2/6] Fix string lists --- src/NpgsqlFSharpParser/Parser.fs | 28 ++++++++++++----- .../ParseSelectTests.fs | 31 +++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/NpgsqlFSharpParser/Parser.fs b/src/NpgsqlFSharpParser/Parser.fs index 166decf..abde675 100644 --- a/src/NpgsqlFSharpParser/Parser.fs +++ b/src/NpgsqlFSharpParser/Parser.fs @@ -149,7 +149,7 @@ let text value : Parser = let star : Parser = text "*" |>> fun _ -> Expr.Star -let opp = new OperatorPrecedenceParser() +let opp = OperatorPrecedenceParser() let expr = opp.ExpressionParser @@ -185,16 +185,27 @@ let quotedString = <|> (skipChar '\'' |> anyStringBetween <| skipChar '\'') let stringLiteral : Parser = - quotedString .>> spacesOrComment + spacesOrComment >>. quotedString .>> spacesOrComment |>> Expr.StringLiteral /// Parses 2 or more comma separated values. I.e (1, 2), but not (3) which will become an integer. -let valueList = +let numericList = let numeric = integer <|> number - attempt( - numeric .>> (pstring ",") >>= fun head -> - sepBy1 numeric (pstring ",") >>= fun tail -> - preturn (Expr.List (head::tail)) + + attempt ( + parens (numeric .>> (pstring ",") + >>= fun head -> + sepBy1 numeric (pstring ",") + >>= fun tail -> preturn (Expr.List(head :: tail))) + ) + +// TODO: Not sure why, but letting the parser accept spaces before a quoted string makes some tests fail +let stringList = + attempt ( + parens (stringLiteral .>> (pstring ",") // .>> spaces) + >>= fun head -> + sepBy1 stringLiteral (pstring ",") // .>> spaces) + >>= fun tail -> preturn (Expr.List(head :: tail))) ) let commaSeparatedExprs = sepBy expr comma @@ -495,8 +506,9 @@ opp.TermParser <- choice [ (attempt declareQuery) (attempt fetchQuery) (attempt functionExpr) + numericList + stringList (text "(") >>. expr .>> (text ")") - valueList star integer boolean diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs index 6c14a43..cb3d1dd 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs @@ -283,6 +283,29 @@ let selectQueryTests = testList "Parse SELECT tests" [ Where = Some (Expr.In(Expr.Ident "user_id", Expr.List([Expr.Integer 1L; Expr.Integer 2L; Expr.Integer 3L]))) } + testSelect """ + SELECT username, email + FROM users + WHERE username IN ('foo','bar') + """ { + SelectExpr.Default with + Columns = [Expr.Ident "username"; Expr.Ident "email"] + From = Some (Expr.Ident "users") + Where = Some (Expr.In(Expr.Ident "username", Expr.List([Expr.StringLiteral "foo"; Expr.StringLiteral "bar"]))) + } + + // space before `bar` + testSelect """ + SELECT username, email + FROM users + WHERE username IN ('foo', 'bar') + """ { + SelectExpr.Default with + Columns = [Expr.Ident "username"; Expr.Ident "email"] + From = Some (Expr.Ident "users") + Where = Some (Expr.In(Expr.Ident "username", Expr.List([Expr.StringLiteral "foo"; Expr.StringLiteral "bar"]))) + } + testSelect """ SELECT username, email FROM users @@ -603,6 +626,14 @@ let selectQueryTests = testList "Parse SELECT tests" [ Where = Some (Expr.GreaterThan(Expr.Ident "last_login", Expr.Date("2021-01-04 00:00:00"))) } + testSelect """ + SELECT aggregate('ID', '2020-01-10', '2020-03-10', '1d') + """ { + SelectExpr.Default with + Columns = [Expr.Function("aggregate", [Expr.StringLiteral "ID"; Expr.StringLiteral "2020-01-10"; + Expr.StringLiteral "2020-03-10"; Expr.StringLiteral "1d"]) ] + } + testSelect """ select timestamp '2021-01-04 00:00:00' """ { From 151371e883a20373197d4a3307e19dc7ba89b41d Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 5 Aug 2021 14:17:13 +0200 Subject: [PATCH 3/6] Add support for BETWEEN --- src/NpgsqlFSharpParser/Parser.fs | 12 ++++++++++++ .../ParseSelectTests.fs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/NpgsqlFSharpParser/Parser.fs b/src/NpgsqlFSharpParser/Parser.fs index abde675..6319bda 100644 --- a/src/NpgsqlFSharpParser/Parser.fs +++ b/src/NpgsqlFSharpParser/Parser.fs @@ -188,6 +188,17 @@ let stringLiteral : Parser = spacesOrComment >>. quotedString .>> spacesOrComment |>> Expr.StringLiteral +let between' : Parser = + attempt ( + spaces >>. + identifier >>= (fun value -> + text "BETWEEN" >>. + (integer <|> number <|> date) >>= (fun left -> + text "AND" >>. + expr >>= (fun right -> + preturn (Expr.Between (value, left, right))))) + ) + /// Parses 2 or more comma separated values. I.e (1, 2), but not (3) which will become an integer. let numericList = let numeric = integer <|> number @@ -506,6 +517,7 @@ opp.TermParser <- choice [ (attempt declareQuery) (attempt fetchQuery) (attempt functionExpr) + between' numericList stringList (text "(") >>. expr .>> (text ")") diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs index cb3d1dd..ec2b71f 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs @@ -687,5 +687,23 @@ let selectQueryTests = testList "Parse SELECT tests" [ ) ) } + + testSelect """ + SELECT * + FROM employees + WHERE employee_id BETWEEN 200 AND 300; + """ { + SelectExpr.Default with + Columns = [ Expr.Star ] + From = Some (Expr.Ident "employees") + Where = + Some( + Expr.Between( + Expr.Ident "employee_id", + Expr.Integer(200L), + Expr.Integer(300L) + ) + ) + } ] From 261c5073884ef385ade9c53b5f9e927c19f2cab3 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 5 Aug 2021 14:30:42 +0200 Subject: [PATCH 4/6] Remove line not needed --- src/NpgsqlFSharpParser/Parser.fs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NpgsqlFSharpParser/Parser.fs b/src/NpgsqlFSharpParser/Parser.fs index 6319bda..f328d86 100644 --- a/src/NpgsqlFSharpParser/Parser.fs +++ b/src/NpgsqlFSharpParser/Parser.fs @@ -190,7 +190,6 @@ let stringLiteral : Parser = let between' : Parser = attempt ( - spaces >>. identifier >>= (fun value -> text "BETWEEN" >>. (integer <|> number <|> date) >>= (fun left -> From dc4b5cecc2331d468c6cc38b11e565fa1d9386d8 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 2 Sep 2021 16:24:39 +0200 Subject: [PATCH 5/6] Add support for LIKE operator --- src/NpgsqlFSharpParser/Parser.fs | 2 ++ src/NpgsqlFSharpParser/Types.fs | 1 + tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/src/NpgsqlFSharpParser/Parser.fs b/src/NpgsqlFSharpParser/Parser.fs index 166decf..4d0016c 100644 --- a/src/NpgsqlFSharpParser/Parser.fs +++ b/src/NpgsqlFSharpParser/Parser.fs @@ -469,6 +469,8 @@ opp.AddOperator(InfixOperator("OR", notFollowedBy (text "DER BY") .>> spacesOrCo opp.AddOperator(InfixOperator("or", notFollowedBy (text "der by") .>> spacesOrComment, 6, Associativity.Left, fun left right -> Expr.Or(left, right))) opp.AddOperator(InfixOperator("IN", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.In(left, right))) opp.AddOperator(InfixOperator("in", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.In(left, right))) +opp.AddOperator(InfixOperator("LIKE", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.Like(left, stringOrFail(right)))) +opp.AddOperator(InfixOperator("like", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.Like(left, stringOrFail(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))) diff --git a/src/NpgsqlFSharpParser/Types.fs b/src/NpgsqlFSharpParser/Types.fs index dd947ef..52c8f63 100644 --- a/src/NpgsqlFSharpParser/Types.fs +++ b/src/NpgsqlFSharpParser/Types.fs @@ -15,6 +15,7 @@ type Expr = | Date of string | Timestamp of string | Function of name:string * arguments:Expr list + | Like of left:Expr * pattern:string | And of left:Expr * right:Expr | Or of left:Expr * right:Expr | In of left:Expr * right:Expr diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs index 6c14a43..508886c 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs @@ -656,5 +656,11 @@ let selectQueryTests = testList "Parse SELECT tests" [ ) ) } + testSelect "SELECT * FROM users WHERE user_id LIKE '%foo'" { + SelectExpr.Default with + Columns = [Expr.Star] + From = Some (Expr.Ident "users") + Where = Some (Expr.Like(Expr.Ident "user_id", "%foo")) + } ] From 19049da84ae417dd27d0bb3f1934165cf99333d7 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 2 Sep 2021 16:43:42 +0200 Subject: [PATCH 6/6] Right side of LIKE must be Expr - It can be StringLiteral or Parameter --- src/NpgsqlFSharpParser/Parser.fs | 4 ++-- src/NpgsqlFSharpParser/Types.fs | 2 +- tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NpgsqlFSharpParser/Parser.fs b/src/NpgsqlFSharpParser/Parser.fs index b6bc224..046bdec 100644 --- a/src/NpgsqlFSharpParser/Parser.fs +++ b/src/NpgsqlFSharpParser/Parser.fs @@ -490,8 +490,8 @@ opp.AddOperator(InfixOperator("OR", notFollowedBy (text "DER BY") .>> spacesOrCo opp.AddOperator(InfixOperator("or", notFollowedBy (text "der by") .>> spacesOrComment, 6, Associativity.Left, fun left right -> Expr.Or(left, right))) opp.AddOperator(InfixOperator("IN", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.In(left, right))) opp.AddOperator(InfixOperator("in", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.In(left, right))) -opp.AddOperator(InfixOperator("LIKE", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.Like(left, stringOrFail(right)))) -opp.AddOperator(InfixOperator("like", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.Like(left, stringOrFail(right)))) +opp.AddOperator(InfixOperator("LIKE", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.Like(left, right))) +opp.AddOperator(InfixOperator("like", spacesOrComment, 8, Associativity.Left, fun left right -> Expr.Like(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))) diff --git a/src/NpgsqlFSharpParser/Types.fs b/src/NpgsqlFSharpParser/Types.fs index 52c8f63..a7b3aec 100644 --- a/src/NpgsqlFSharpParser/Types.fs +++ b/src/NpgsqlFSharpParser/Types.fs @@ -15,7 +15,7 @@ type Expr = | Date of string | Timestamp of string | Function of name:string * arguments:Expr list - | Like of left:Expr * pattern:string + | Like of left:Expr * right:Expr | And of left:Expr * right:Expr | Or of left:Expr * right:Expr | In of left:Expr * right:Expr diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs index 51ba25c..b62a92f 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs @@ -692,7 +692,7 @@ let selectQueryTests = testList "Parse SELECT tests" [ SelectExpr.Default with Columns = [Expr.Star] From = Some (Expr.Ident "users") - Where = Some (Expr.Like(Expr.Ident "user_id", "%foo")) + Where = Some (Expr.Like(Expr.Ident "user_id", Expr.StringLiteral("%foo"))) } testSelect """