diff --git a/src/NpgsqlFSharpParser/Parser.fs b/src/NpgsqlFSharpParser/Parser.fs index 045374b..0ed6e9e 100644 --- a/src/NpgsqlFSharpParser/Parser.fs +++ b/src/NpgsqlFSharpParser/Parser.fs @@ -87,10 +87,11 @@ let manyCharsBetween popen pclose pchar = popen >>? manyCharsTill pchar pclose let anyStringBetween popen pclose = manyCharsBetween popen pclose anyChar // Cannot be a reserved keyword. -let stringIdentifier : Parser = +let unquotedIdentifier : Parser = let isIdentifierFirstChar token = isLetter token - let isIdentifierChar token = isLetter token || isDigit token || token = '.' || token = '_' - many1Satisfy2L isIdentifierFirstChar isIdentifierChar "identifier" .>> spaces + let isIdentifierChar token = isLetter token || isDigit token || token = '_' + + many1Satisfy2L isIdentifierFirstChar isIdentifierChar "identifier" .>> spacesOrComment >>= fun identifier -> if List.contains (identifier.ToUpper()) reserved then fail (sprintf "Identifier %s is a reserved keyword" identifier) @@ -98,11 +99,25 @@ let stringIdentifier : Parser = // Can be a reserved keyword. let quotedIdentifier : Parser = - (skipChar '\"' |> anyStringBetween <| skipChar '\"') + (skipChar '\"' |> anyStringBetween <| skipChar '\"') .>> spacesOrComment -let simpleIdentifier = +let stringIdentifier = quotedIdentifier - <|> stringIdentifier + <|> unquotedIdentifier + +let simpleIdentifier = + attempt( + stringIdentifier >>= fun schema -> + text "." >>. stringIdentifier >>= fun table -> + text "." >>. stringIdentifier >>= fun column -> + preturn (sprintf "%s.%s.%s" schema table column)) + <|> + attempt( + stringIdentifier >>= fun table -> + text "." >>. stringIdentifier >>= fun column -> + preturn (sprintf "%s.%s" table column)) + <|> + attempt stringIdentifier let identifier : Parser = simpleIdentifier |>> Expr.Ident @@ -114,7 +129,7 @@ let parameter : Parser = |>> Expr.Parameter let text value : Parser = - (optional spaces) >>. pstringCI value .>> (optional spaces) + spaces >>. pstringCI value .>> spacesOrComment let star : Parser = text "*" |>> fun _ -> Expr.Star @@ -128,11 +143,11 @@ let parens parser = between (text "(") (text ")") parser let comma = text "," let integer : Parser = - (optional spaces) >>. pint32 .>> (optional spaces) + spaces >>. pint32 .>> spacesOrComment |>> Expr.Integer let number : Parser = - (optional spaces) >>. pfloat .>> (optional spaces) + spaces >>. pfloat .>> spacesOrComment |>> Expr.Float let boolean : Parser = @@ -246,14 +261,14 @@ let optionalHavingClause = optionalExpr (text "HAVING" >>. expr) let optionalFrom = optionalExpr ( attempt ( - text "FROM " >>. (parens selectQuery) >>= fun subQuery -> + text "FROM" >>. (parens selectQuery) >>= fun subQuery -> optional (text "AS") >>= fun _ -> simpleIdentifier >>= fun alias -> preturn (Expr.As(subQuery, Expr.Ident alias)) ) <|> attempt ( - text "FROM " >>. simpleIdentifier >>= fun table -> + text "FROM" >>. simpleIdentifier >>= fun table -> optional (text "AS") >>= fun _ -> simpleIdentifier >>= fun alias -> preturn (Expr.As(Expr.Ident table, Expr.Ident alias)) @@ -350,11 +365,20 @@ let updateQuery = preturn (Expr.UpdateQuery query) -opp.AddOperator(InfixOperator("AND", spaces, 7, Associativity.Left, fun left right -> Expr.And(left, right))) -opp.AddOperator(InfixOperator("AS", spaces, 6, Associativity.Left, fun left right -> Expr.As(left, right))) -opp.AddOperator(InfixOperator("as", spaces, 6, Associativity.Left, fun left right -> Expr.As(left, right))) +let spacesOrComment = + let comment = skipString "/*" >>. (charsTillString "*/" true 8096) + let commentEol = skipString "--" >>. skipRestOfLine true + + spaces .>> + optional comment .>> + optional commentEol .>> + spaces + +opp.AddOperator(InfixOperator("AND", spacesOrComment, 7, Associativity.Left, fun left right -> Expr.And(left, right))) +opp.AddOperator(InfixOperator("AS", spacesOrComment, 6, Associativity.Left, fun left right -> Expr.As(left, right))) +opp.AddOperator(InfixOperator("as", spacesOrComment, 6, Associativity.Left, fun left right -> Expr.As(left, right))) opp.AddOperator(InfixOperator("OR", notFollowedBy (text "DER BY"), 6, 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("IN", spacesOrComment, 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))) @@ -365,8 +389,8 @@ opp.AddOperator(InfixOperator("||", spaces, 9, Associativity.Left, fun left righ opp.AddOperator(InfixOperator("::", spaces, 9, Associativity.Left, fun left right -> Expr.TypeCast(left, right))) opp.AddOperator(InfixOperator("->>", spaces, 9, Associativity.Left, fun left right -> Expr.JsonIndex(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.AddOperator(PostfixOperator("IS NULL", spacesOrComment, 8, false, fun value -> Expr.Equals(Expr.Null, value))) +opp.AddOperator(PostfixOperator("IS NOT NULL", spacesOrComment, 8, false, fun value -> Expr.Not(Expr.Equals(Expr.Null, value)))) opp.TermParser <- choice [ (attempt updateQuery) @@ -384,7 +408,7 @@ opp.TermParser <- choice [ parameter ] -let fullParser = (optional spaces) >>. expr .>> (optional spaces <|> (text ";" |>> fun _ -> ())) +let fullParser = spacesOrComment >>. expr .>> (spacesOrComment <|> (text ";" |>> ignore)) let parse (input: string) : Result = match run fullParser input with diff --git a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs index 76fb783..425da54 100644 --- a/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs +++ b/tests/NpgsqlFSharpAnalyzer.Tests/ParseSelectTests.fs @@ -427,6 +427,7 @@ let selectQueryTests = testList "Parse SELECT tests" [ Offset = Some (Expr.Integer 100) } + testSelect """ SELECT * FROM (SELECT NOW()) AS time @@ -440,4 +441,120 @@ let selectQueryTests = testList "Parse SELECT tests" [ }, Expr.Ident "time")) Limit = Some (Expr.Integer 1) } -] + + testSelect """ + SELECT * + FROM + (SELECT NOW()) time + LIMIT 1 + """ { + SelectExpr.Default with + Columns = [Expr.Star] + From = Some (Expr.As (Expr.SelectQuery { + SelectExpr.Default with + Columns = [Expr.Function ("NOW", [])] + }, Expr.Ident "time")) + Limit = Some (Expr.Integer 1) + } + + testSelect """ + SELECT "username" FROM "users" + """ { + SelectExpr.Default with + Columns = [Expr.Ident "username"] + From = Expr.Ident "users" |> Some + } + + testSelect """ + SELECT "username" AS "name" FROM "users" + """ { + SelectExpr.Default with + Columns = [ Expr.As(Expr.Ident "username", Expr.Ident "name") ] + From = Expr.Ident "users" |> Some + } + + testSelect """ + SELECT "$Table"."timestamp" FROM "$Table" + """ { + SelectExpr.Default with + Columns = [ Expr.Ident("$Table.timestamp") ] + From = Expr.Ident "$Table" |> Some + } + + testSelect """ + SELECT "$Table".timestamp AS ts FROM "$Table" + """ { + SelectExpr.Default with + Columns = [ Expr.As(Expr.Ident("$Table.timestamp"), Expr.Ident("ts")) ] + From = Expr.Ident "$Table" |> Some + } + + testSelect """ + SELECT public."$Table"."timestamp" FROM "$Table" + """ { + SelectExpr.Default with + Columns = [ Expr.Ident("public.$Table.timestamp") ] + From = Expr.Ident "$Table" |> Some + } + + testSelect """ + SELECT public."$Table"."timestamp" FROM "$Table" AS tbl + """ { + SelectExpr.Default with + Columns = [ Expr.Ident("public.$Table.timestamp") ] + From = Expr.As(Expr.Ident "$Table", Expr.Ident "tbl") |> Some + } + + testSelect """ + SELECT public."$Table"."timestamp" FROM "$Table" tbl LIMIT 100 + """ { + SelectExpr.Default with + Columns = [ Expr.Ident("public.$Table.timestamp") ] + From = Expr.As(Expr.Ident "$Table", Expr.Ident "tbl") |> Some + Limit = Some (Expr.Integer 100) + } + + testSelect "SELECT 1 -- This is a comment" { + SelectExpr.Default with Columns = [Expr.Integer 1] + } + + testSelect """ + -- This is a comment + SELECT 1""" { + SelectExpr.Default with Columns = [Expr.Integer 1] + } + + testSelect """ + /* Comment inserted first */ + SELECT 1""" { + SelectExpr.Default with Columns = [Expr.Integer 1] + } + + testSelect """ + SELECT 1 + /* Comment inserted last */ + """ { + SelectExpr.Default with Columns = [Expr.Integer 1] + } + + testSelect """ + SELECT 1; + /* Comment inserted after semicolon */ + """ { + SelectExpr.Default with Columns = [Expr.Integer 1] + } + + testSelect """ + SELECT * /* Comment inserted here */ + FROM -- Ignore + (SELECT NOW()) time /* Comment inserted here */ + LIMIT 1 -- Ignore + """ { + SelectExpr.Default with + Columns = [Expr.Star] + From = Some (Expr.As (Expr.SelectQuery { + SelectExpr.Default with + Columns = [Expr.Function ("NOW", [])] + }, Expr.Ident "time")) + Limit = Some (Expr.Integer 1) + }]