Skip to content

Commit

Permalink
Merge pull request Zaid-Ajaj#32 from dbrattli/set-declare
Browse files Browse the repository at this point in the history
Add support for timestamp, date, set and declare cursor
  • Loading branch information
Zaid-Ajaj authored May 14, 2021
2 parents 237f46e + 89d6451 commit 34e0cea
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 22 deletions.
97 changes: 93 additions & 4 deletions src/NpgsqlFSharpParser/Parser.fs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,21 @@ let simpleIdentifier =
let identifier : Parser<Expr, unit> =
simpleIdentifier |>> Expr.Ident

let datatype : Parser<Expr, unit> =
let isIdentifierFirstChar token = isLetter token
let isIdentifierChar token = isLetter token || isDigit token || token = '_' || token = ' '
let dtIdent = many1Satisfy2L isIdentifierFirstChar isIdentifierChar "datatype"

attempt(
dtIdent >>= fun ident ->
opt (pstring "[]") >>= fun brackets ->
spacesOrComment >>= fun _ ->
let isArray = brackets |> Option.map (fun _ -> true)
match (DataType.TryFromString(ident, ?isArray=isArray)) with
| Some dt -> preturn (Expr.DataType(dt))
| _ -> fail (sprintf "%s is not a valid datatype" ident)
)

let parameter : Parser<Expr, unit> =
let isIdentifierFirstChar token = token = '@'
let isIdentifierChar token = isLetter token || isDigit token || token = '_'
Expand All @@ -143,13 +158,23 @@ let parens parser = between (text "(") (text ")") parser
let comma = text ","

let integer : Parser<Expr, unit> =
spaces >>. pint32 .>> spacesOrComment
spaces >>. pint64 .>> spacesOrComment
|>> Expr.Integer

let number : Parser<Expr, unit> =
spaces >>. pfloat .>> spacesOrComment
|>> Expr.Float

let timestamp : Parser<Expr, unit> =
attempt (spaces >>. (text "TIMESTAMP") >>. spacesOrComment
>>. quotedString .>> spacesOrComment)
|>> Expr.Timestamp

let date : Parser<Expr, unit> =
attempt (spaces >>. (text "DATE") >>. spacesOrComment
>>. quotedString .>> spacesOrComment)
|>> Expr.Date

let boolean : Parser<Expr, unit> =
(text "true" |>> fun _ -> Expr.Boolean true)
<|> (text "false" |>> fun _ -> Expr.Boolean false)
Expand All @@ -160,9 +185,18 @@ let quotedString =
<|> (skipChar '\'' |> anyStringBetween <| skipChar '\'')

let stringLiteral : Parser<Expr, unit> =
quotedString
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 numeric = integer <|> number
attempt(
numeric .>> (pstring ",") >>= fun head ->
sepBy1 numeric (pstring ",") >>= fun tail ->
preturn (Expr.List (head::tail))
)

let commaSeparatedExprs = sepBy expr comma

let selections =
Expand Down Expand Up @@ -258,6 +292,13 @@ let optionalWhereClause = optionalExpr (text "WHERE" >>. expr)

let optionalHavingClause = optionalExpr (text "HAVING" >>. expr)

let optionalScope =
optionalExpr (
(text "LOCAL" |>> fun _ -> Local)
<|>
(text "SESSION" |>> fun _ -> Session)
)

let optionalFrom =
optionalExpr (
attempt (
Expand Down Expand Up @@ -365,6 +406,37 @@ let updateQuery =

preturn (Expr.UpdateQuery query)


let toOrEquals =
text "=" <|> text "TO"

// TODO: SET TIME ZONE value is an alias for SET timezone TO value
let setQuery =
text "SET" >>.
optionalScope >>= fun scope ->
simpleIdentifier >>= fun parameter ->
toOrEquals >>= fun _ ->
expr >>= fun value ->
let query = {
SetExpr.Default with
Parameter = parameter
Scope = defaultArg scope Session
Value = Some value
}

preturn (Expr.SetQuery query)

let declareQuery =
text "DECLARE" >>.
simpleIdentifier >>= fun parameter ->
text "CURSOR FOR" >>.
expr >>= fun query ->
let query = {
Parameter = parameter
Query = query
}
preturn (Expr.DeclareQuery (Cursor query))

let spacesOrComment =
let comment = skipString "/*" >>. (charsTillString "*/" true 8096)
let commentEol = skipString "--" >>. skipRestOfLine true
Expand All @@ -374,35 +446,52 @@ let spacesOrComment =
optional commentEol .>>
spaces

let stringOrFail = function
| Expr.StringLiteral(value) -> value
| _ -> failwith "not a string"

opp.AddOperator(InfixOperator("AND", spacesOrComment, 7, Associativity.Left, fun left right -> Expr.And(left, right)))
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("OR", notFollowedBy (text "DER BY") .>> spacesOrComment, 6, Associativity.Left, fun left right -> Expr.Or(left, right)))
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(">", 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(InfixOperator("<>", spaces, 9, Associativity.Left, fun left right -> Expr.Not(Expr.Equals(left, right))))
opp.AddOperator(InfixOperator("||", spaces, 9, Associativity.Left, fun left right -> Expr.StringConcat(left, right)))
opp.AddOperator(InfixOperator("::", spaces, 9, Associativity.Left, fun left right -> Expr.TypeCast(left, right)))
opp.AddOperator(InfixOperator("::", spacesOrComment, 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", spacesOrComment, 8, false, fun value -> 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.AddOperator(PostfixOperator("is not null", spacesOrComment, 8, false, fun value -> Expr.Not(Expr.Equals(Expr.Null, value))))
opp.AddOperator(PrefixOperator("ANY", spacesOrComment, 8, true, fun value -> Expr.Any(value)))
opp.AddOperator(PrefixOperator("any", spacesOrComment, 8, true, fun value -> Expr.Any(value)))

opp.TermParser <- choice [
(attempt updateQuery)
(attempt insertQuery)
(attempt deleteQuery)
(attempt selectQuery)
(attempt setQuery)
(attempt declareQuery)
(attempt functionExpr)
(text "(") >>. expr .>> (text ")")
valueList
star
integer
boolean
number
date
datatype
timestamp
stringLiteral
identifier
parameter
Expand Down
70 changes: 69 additions & 1 deletion src/NpgsqlFSharpParser/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ namespace rec NpgsqlFSharpParser

[<RequireQualifiedAccess>]
type Expr =
| Array of Expr list
| List of Expr list
| Null
| Star
| Ident of string
| Parameter of string
| Boolean of bool
| StringLiteral of string
| Integer of int
| Integer of int64
| Float of float
| Date of string
| Timestamp of string
| Function of name:string * arguments:Expr list
| And of left:Expr * right:Expr
| Or of left:Expr * right:Expr
Expand All @@ -18,7 +22,9 @@ type Expr =
| StringConcat of left:Expr * right:Expr
| JsonIndex of left:Expr * right:Expr
| TypeCast of left:Expr * right:Expr
| DataType of DataType
| Not of expr:Expr
| Any of expr:Expr
| Equals of left:Expr * right:Expr
| GreaterThan of left:Expr * right:Expr
| LessThan of left:Expr * right:Expr
Expand All @@ -29,6 +35,8 @@ type Expr =
| DeleteQuery of expr:DeleteExpr
| InsertQuery of expr: InsertExpr
| UpdateQuery of expr: UpdateExpr
| SetQuery of expr: SetExpr
| DeclareQuery of expr: DeclareExpr

type Ordering =
| Asc of columnName:string
Expand Down Expand Up @@ -110,3 +118,63 @@ type InsertExpr = {
ConflictResolution = [ ]
Returning = [ ]
}

type Scope =
| Local
| Session

type SetExpr = {
Parameter: string
Value: Expr option
Scope: Scope
} with
static member Default =
{
Parameter = "";
Value = None
Scope = Session
}

type CursorDeclaration = {
Parameter: string
Query: Expr
} with
static member Default =
{
Parameter = "";
Query = Expr.Null
}

type DeclareExpr =
| Cursor of CursorDeclaration

[<RequireQualifiedAccess>]
type DataType =
| Integer
| BigInt
| SmallInt
| Real
| Double
| Array of dataType:DataType * size:int option

static member TryFromString(valueType: string, ?isArray: bool, ?arraySize: int) : DataType option =
let dType =
match valueType.ToUpper () with
| "INT2"
| "SMALLINT" -> Some SmallInt
| "INT"
| "INT4"
| "INTEGER" -> Some Integer
| "INT8"
| "BIGINT" -> Some BigInt
| "FLOAT4"
| "REAL" -> Some Real
| "FLOAT8"
| "DOUBLE PRECISION" -> Some Double
| _ -> None

let isArray = isArray |> Option.bind (function | false -> None | _ -> Some true) // Note: Some false -> None.

dType
|> Option.bind (fun t -> isArray |> Option.map (fun _ -> Array(dataType=t, size=arraySize)))
|> Option.orElse dType
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<Compile Include="ParseInsertTests.fs" />
<Compile Include="ParseDeleteTests.fs" />
<Compile Include="ParseSelectTests.fs" />
<Compile Include="ParseSetTests.fs" />
<Compile Include="ParseDeclareTests.fs" />
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Analyzer.fs" />
<Compile Include="Tests.fs" />
Expand Down
35 changes: 35 additions & 0 deletions tests/NpgsqlFSharpAnalyzer.Tests/ParseDeclareTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module ParseDeclareTests

open Expecto
open NpgsqlFSharpParser

let testDeclare inputQuery expected =
test inputQuery {
match Parser.parse inputQuery with
| Ok (Expr.DeclareQuery query) ->
Expect.equal query expected "The query is parsed correctly"
| Ok somethingElse ->
failwithf "Unexpected declare statement %A" somethingElse
| Error errorMsg ->
failwith errorMsg
}

let ftestDeclare inputQuery expected =
ftest inputQuery {
match Parser.parse inputQuery with
| Ok (Expr.DeclareQuery query) ->
Expect.equal query expected "The query is parsed correctly"
| Ok somethingElse ->
failwithf "Unexpected declare statement %A" somethingElse
| Error errorMsg ->
failwith errorMsg
}

[<Tests>]
let declareQueryTests = testList "Parse DECLARE tests" [
testDeclare "DECLARE c1 CURSOR FOR SELECT NOW();" ({
Parameter = "c1"
Query = Expr.SelectQuery { SelectExpr.Default with Columns = [Expr.Function("NOW", [])] }
} |> Cursor)
]

Loading

0 comments on commit 34e0cea

Please sign in to comment.