From 2d46fb989c251afe0c25e884e73f1f78e050c349 Mon Sep 17 00:00:00 2001 From: Maxim Zimaliev Date: Fri, 10 Mar 2017 21:40:16 +0300 Subject: [PATCH] Initial stuff (#1) * constructors * wip * lenses * more lenses * ... * wip examples * search example * joggling packages wip * tests... * only tests for search interpreter is left * done * readme * Literal, need manually implement Ord1 and Eq1 * using EJsonF * updated ejson dep * capitalized * capitalize tests, polymorphic l * updated to the latest purescript-ejson, added date|time constructors|lenses * lenses --- .travis.yml | 16 +- README.md | 22 + bower.json | 22 +- package.json | 18 +- src/Main.purs | 9 - src/SqlSquare.purs | 9 + src/SqlSquare/AST.purs | 728 +++++++++++++++--------------- src/SqlSquare/BinaryOperator.purs | 37 ++ src/SqlSquare/Case.purs | 27 ++ src/SqlSquare/Constructors.purs | 130 ++++++ src/SqlSquare/GroupBy.purs | 31 ++ src/SqlSquare/JoinType.purs | 19 + src/SqlSquare/Lenses.purs | 321 +++++++++++++ src/SqlSquare/OrderBy.purs | 32 ++ src/SqlSquare/OrderType.purs | 13 + src/SqlSquare/Projection.purs | 30 ++ src/SqlSquare/Relation.purs | 109 +++++ src/SqlSquare/UnaryOperator.purs | 22 + src/SqlSquare/Utils.purs | 16 + test/src/Argonaut.purs | 137 ++++++ test/src/Constructors.purs | 64 +++ test/src/Main.purs | 34 +- test/src/Search.purs | 416 +++++++++++++++++ 23 files changed, 1861 insertions(+), 401 deletions(-) create mode 100644 README.md delete mode 100644 src/Main.purs create mode 100644 src/SqlSquare.purs create mode 100644 src/SqlSquare/BinaryOperator.purs create mode 100644 src/SqlSquare/Case.purs create mode 100644 src/SqlSquare/Constructors.purs create mode 100644 src/SqlSquare/GroupBy.purs create mode 100644 src/SqlSquare/JoinType.purs create mode 100644 src/SqlSquare/Lenses.purs create mode 100644 src/SqlSquare/OrderBy.purs create mode 100644 src/SqlSquare/OrderType.purs create mode 100644 src/SqlSquare/Projection.purs create mode 100644 src/SqlSquare/Relation.purs create mode 100644 src/SqlSquare/UnaryOperator.purs create mode 100644 src/SqlSquare/Utils.purs create mode 100644 test/src/Argonaut.purs create mode 100644 test/src/Constructors.purs create mode 100644 test/src/Search.purs diff --git a/.travis.yml b/.travis.yml index a5c107e..0dc0abd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,16 @@ language: node_js dist: trusty sudo: required -node_js: - - 6 +node_js: stable install: - - npm install pulp bower -g - - npm install && bower install + - npm install -g bower + - npm install + - bower install script: - - npm run test + - npm run -s build + - npm run -s test +after_success: +- >- + test $TRAVIS_TAG && + echo $GITHUB_TOKEN | pulp login && + echo y | pulp publish --no-push diff --git a/README.md b/README.md new file mode 100644 index 0000000..7707e1c --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# purescript-sqlsquare + +[![Latest release](http://img.shields.io/github/release/slamdata/purescript-sqlsquare.svg)](https://github.com/slamdata/purescript-sqlsquare/releases) +[![Build status](https://travis-ci.org/slamdata/purescript-sqlsquare.svg?branch=master)](https://travis-ci.org/slamdata/purescript-sqlsquare) + +AST and printer for SQL² -- query language used by [quasar](https://github.com/quasar-analytics/quasar). + +## Instalation + +``` +bower install purescript-sqlsquare +``` + +## Documentation + +There are two examples (extracted from [slamdata](https://github.com/slamdata/slamdata)) ++ Extraction sql fields from array of jsons: [here](test/src/Argonaut.purs) ++ Interpretation query language of [purescript-search](https://github.com/slamdata/purescript-search): +[here](test/src/Search.purs) + + +Module documentation is published on Pursuit: [http://pursuit.purescript.org/packages/purescript-sqlsquare](http://pursuit.purescript.org/packages/purescript-sqlsquare) diff --git a/bower.json b/bower.json index 2aba6c5..252070e 100644 --- a/bower.json +++ b/bower.json @@ -1,15 +1,31 @@ { "name": "purescript-sqlsquare", + "homepage": "https://github.com/slamdata/purescript-sqlsquare", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git://github.com/slamdata/purescript-sqlsquare.git" + }, "ignore": [ "**/.*", "node_modules", "bower_components", - "output" + "output", + "bower.json", + "package.json" ], "dependencies": { "purescript-prelude": "^2.4.0", - "purescript-matryoshka": "^0.1.1", + "purescript-matryoshka": "^0.2.0", "purescript-pathy": "^3.0.2", - "purescript-debug": "^2.0.0" + "purescript-profunctor": "^2.0.0", + "purescript-profunctor-lenses": "^2.6.0", + "purescript-ejson": "^6.0.0" + }, + "devDependencies": { + "purescript-argonaut": "^2.0.0", + "purescript-search": "^2.0.0", + "purescript-debug": "^2.0.0", + "purescript-test-unit": "^10.1.0" } } diff --git a/package.json b/package.json index 40750d5..7f6c3b1 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,13 @@ { - "name": "purescript-sqlsquare", - "license": "Apache-2.0", - "dependencies": { - "pulp": "^10.0.1", - "purescript": "^0.10.7", - "purescript-psa": "^0.4.0" - } + "name": "purescript-sqlsquare", + "license": "Apache-2.0", + "scripts": { + "build": "pulp build -- --censor-lib --strict --stash", + "test": "pulp test -- --censor-lib --strict --stash" + }, + "dependencies": { + "pulp": "^10.0.1", + "purescript": "^0.10.7", + "purescript-psa": "^0.4.0" + } } diff --git a/src/Main.purs b/src/Main.purs deleted file mode 100644 index abe68ec..0000000 --- a/src/Main.purs +++ /dev/null @@ -1,9 +0,0 @@ -module Main where - -import Prelude -import Control.Monad.Eff (Eff) -import Control.Monad.Eff.Console (CONSOLE, log) - -main :: forall e. Eff (console :: CONSOLE | e) Unit -main = do - log "Hello sailor!" diff --git a/src/SqlSquare.purs b/src/SqlSquare.purs new file mode 100644 index 0000000..72533fc --- /dev/null +++ b/src/SqlSquare.purs @@ -0,0 +1,9 @@ +module SqlSquare + ( module AST + , module Lenses + , module Constructors + ) where + +import SqlSquare.AST as AST +import SqlSquare.Lenses as Lenses +import SqlSquare.Constructors as Constructors diff --git a/src/SqlSquare/AST.purs b/src/SqlSquare/AST.purs index 2c3c49d..5db3549 100644 --- a/src/SqlSquare/AST.purs +++ b/src/SqlSquare/AST.purs @@ -1,227 +1,213 @@ -module SqlSquare.AST where +module SqlSquare.AST + ( BinopR + , UnopR + , InvokeFunctionR + , MatchR + , SwitchR + , LetR + , SelectR + , SqlF(..) + , Sql + , printF + , print + , module SqlSquare.Utils + , module OT + , module JT + , module SqlSquare.BinaryOperator + , module SqlSquare.UnaryOperator + , module SqlSquare.GroupBy + , module SqlSquare.Case + , module SqlSquare.OrderBy + , module SqlSquare.Projection + , module SqlSquare.Relation + ) where import Prelude -import Data.Bifunctor (bimap) -import Data.Either (Either, either) +import Data.Eq (class Eq1, eq1) import Data.Foldable as F +import Data.Traversable as T +import Data.Functor.Mu (Mu) +import Data.List as L import Data.Maybe (Maybe(..)) -import Data.List (List, fromFoldable) -import Data.NonEmpty as NE -import Data.Tuple (Tuple(..)) -import Data.Path.Pathy (AbsFile, RelFile, Unsandboxed, unsafePrintPath) - -import Matryoshka (class Recursive, class Corecursive, Algebra, cata, embed, Mu) - -infixr 4 type Tuple as × -infixr 1 Tuple as × - -type FUPath = Either (RelFile Unsandboxed) (AbsFile Unsandboxed) - -data OrderType - = ASC - | DESC - -printOrderType ∷ OrderType → String -printOrderType = case _ of - ASC → "asc" - DESC → "desc" - -derive instance eqOrderType ∷ Eq OrderType - -data JoinType - = LeftJoin - | RightJoin - | InnerJoin - | FullJoin - -printJoinType ∷ JoinType → String -printJoinType = case _ of - LeftJoin → "left join" - RightJoin → "right join" - FullJoin → "full join" - InnerJoin → "inner join" - -derive instance eqJoinType ∷ Eq JoinType - -data BinaryOperator - = IfUndefined - | Range - | Or - | And - | Eq - | Neq - | Ge - | Gt - | Le - | Lt - | Concat - | Plus - | Minus - | Mult - | Div - | Mod - | Pow - | In - | FieldDeref - | IndexDeref - | Limit - | Offset - | Sample - | Union - | UnionAll - | Intersect - | IntersectAll - | Except - | UnshiftMap - -derive instance eqBinaryOperator ∷ Eq BinaryOperator - -data UnaryOperator - = Not - | Exists - | Positive - | Negative - | Distinct - | FlattenMapKeys - | FlattenMapValues - | ShiftMapKeys - | ShiftMapValues - | FlattenArrayIndices - | FlattenArrayValues - | ShiftArrayIndices - | ShiftArrayValues - | UnshiftArray - -derive instance eqUnaryOperator ∷ Eq UnaryOperator - -newtype GroupBy a = GroupBy { keys ∷ List a, having ∷ Maybe a } - -printGroupBy ∷ Algebra GroupBy String -printGroupBy (GroupBy { keys, having }) = - F.intercalate ", " keys <> F.foldMap (" having " <> _) having - -derive instance functorGroupBy ∷ Functor GroupBy - -newtype Case a = Case { cond ∷ a, expr ∷ a } - -printCase ∷ Algebra Case String -printCase (Case { cond, expr }) = " when " <> cond <> " then " <> expr - -derive instance functorCase ∷ Functor Case - -newtype OrderBy a = OrderBy (NE.NonEmpty List (OrderType × a)) - -printOrderBy ∷ Algebra OrderBy String -printOrderBy (OrderBy lst) = - F.intercalate ", " $ lst <#> \(ot × a) → printOrderType ot <> a - -derive instance functorOrderBy ∷ Functor OrderBy - -newtype Projection a = Projection { expr ∷ a, alias ∷ Maybe String } - -printProjection ∷ Algebra Projection String -printProjection (Projection { expr, alias }) = expr <> F.foldMap (" as " <> _) alias - -derive instance functorProjection ∷ Functor Projection - -data SqlRelation a - = JoinRelation - { left ∷ SqlRelation a - , right ∷ SqlRelation a - , joinType ∷ JoinType - , clause ∷ a - } - | ExprRelationAST - { expr ∷ a - , aliasName ∷ String - } - | TableRelationAST - { tablePath ∷ FUPath - , alias ∷ Maybe String - } - | VariRelation - { vari ∷ String - , alias ∷ Maybe String - } - | IdentRelation - { ident ∷ String - , alias ∷ Maybe String - } - -derive instance functorSqlRelation ∷ Functor SqlRelation - -printRelation ∷ Algebra SqlRelation String -printRelation = case _ of - ExprRelationAST {expr, aliasName} → - "(" <> expr <> ") as " <> aliasName - VariRelation { vari, alias} → - vari <> F.foldMap (" as " <> _) alias - TableRelationAST { tablePath, alias } → - "`" - <> either unsafePrintPath unsafePrintPath tablePath - <> "`" - <> F.foldMap (" as " <> _) alias - IdentRelation { ident, alias } → - ident <> F.foldMap (" as " <> _) alias - JoinRelation { left, right, joinType, clause } → - printRelation left - <> " " - <> printJoinType joinType - <> " " - <> printRelation right - <> " on " - <> clause - -data SqlF a - = SetLiteral (List a) - | ArrayLiteral (List a) - | MapLiteral (List (a × a)) +import Data.Monoid (mempty) +import Data.Ord (class Ord1, compare1) + +import Data.Json.Extended.Signature (EJsonF, renderEJsonF) + +import SqlSquare.Utils (type (×), (×), (∘), (⋙)) +import SqlSquare.OrderType as OT +import SqlSquare.JoinType as JT +import SqlSquare.BinaryOperator (BinaryOperator(..)) +import SqlSquare.UnaryOperator (UnaryOperator(..)) +import SqlSquare.GroupBy (GroupBy(..), printGroupBy) +import SqlSquare.Case (Case(..), printCase) +import SqlSquare.OrderBy (OrderBy(..), printOrderBy) +import SqlSquare.Projection (Projection(..), printProjection) +import SqlSquare.Relation (Relation(..), printRelation, FUPath, JoinRelR, ExprRelR, TableRelR, VariRelR, IdentRelR) + +import Matryoshka (Algebra, cata) + +type BinopR a = + { lhs ∷ a + , rhs ∷ a + , op ∷ BinaryOperator + } + +type UnopR a = + { expr ∷ a + , op ∷ UnaryOperator + } + +type InvokeFunctionR a = + { name ∷ String + , args ∷ L.List a + } + +type MatchR a = + { expr ∷ a + , cases ∷ L.List (Case a) + , else_ ∷ Maybe a + } + +type SwitchR a = + { cases ∷ L.List (Case a) + , else_ ∷ Maybe a + } + +type LetR a = + { ident ∷ String + , bindTo ∷ a + , in_ ∷ a + } + +type SelectR a = + { isDistinct ∷ Boolean + , projections ∷ L.List (Projection a) + , relations ∷ Maybe (Relation a) + , filter ∷ Maybe a + , groupBy ∷ Maybe (GroupBy a) + , orderBy ∷ Maybe (OrderBy a) + } + +data SqlF literal a + = SetLiteral (L.List a) + | Literal (literal a) | Splice (Maybe a) - | Binop - { lhs ∷ a - , rhs ∷ a - , op ∷ BinaryOperator - } - | Unop - { expr ∷ a - , op ∷ UnaryOperator - } + | Binop (BinopR a) + | Unop (UnopR a) | Ident String - | InvokeFunction - { name ∷ String - , args ∷ List a - } - | Match - { expr ∷ a - , cases ∷ List (Case a) - , default_ ∷ Maybe a - } - | Switch - { cases ∷ List (Case a) - , default_ ∷ Maybe a - } - | Let - { ident ∷ String - , bindTo ∷ a - , in_ ∷ a - } - | IntLiteral Int - | FloatLiteral Number - | StringLiteral String - | NullLiteral - | BoolLiteral Boolean + | InvokeFunction (InvokeFunctionR a) + | Match (MatchR a) + | Switch (SwitchR a) + | Let (LetR a) | Vari String - | Select - { isDistinct ∷ Boolean - , projections ∷ List (Projection a) - , relations ∷ Maybe (SqlRelation a) - , filter ∷ Maybe a - , groupBy ∷ Maybe (GroupBy a) - , orderBy ∷ Maybe (OrderBy a) - } - -instance functorAST ∷ Functor SqlF where + | Select (SelectR a) + | Parens a + +derive instance eqSqlF ∷ (Eq a, Eq (l a)) ⇒ Eq (SqlF l a) +derive instance ordSqlF ∷ (Ord a, Ord (l a)) ⇒ Ord (SqlF l a) + +instance eq1SqlF ∷ Eq1 l ⇒ Eq1 (SqlF l) where + eq1 (Literal l) (Literal ll) = eq1 l ll + eq1 (Splice a) (Splice aa) = eq a aa + eq1 (Binop r) (Binop rr) = + r.lhs == rr.lhs + && r.rhs == rr.rhs + && r.op == rr.op + eq1 (Unop r) (Unop rr) = + r.expr == rr.expr + && r.op == rr.op + eq1 (Ident s) (Ident ss) = + s == ss + eq1 (InvokeFunction r) (InvokeFunction rr) = + r.name == rr.name + && r.args == rr.args + eq1 (Match r) (Match rr) = + r.else_ == rr.else_ + && r.cases == rr.cases + && r.expr == rr.expr + eq1 (Switch r) (Switch rr) = + r.cases == rr.cases + && r.else_ == rr.else_ + eq1 (Let r) (Let rr) = + r.in_ == r.in_ + && r.bindTo == rr.bindTo + && r.ident == rr.ident + eq1 (Vari v) (Vari vv) = + v == vv + eq1 (Parens a) (Parens aa) = + a == aa + eq1 (Select r) (Select rr) = + r.isDistinct == rr.isDistinct + && r.projections == rr.projections + && r.relations == rr.relations + && r.filter == rr.filter + && r.groupBy == rr.groupBy + && r.orderBy == rr.orderBy + eq1 _ _ = false + +instance ord1SqlF ∷ Ord1 l ⇒ Ord1 (SqlF l) where + compare1 (Literal l) (Literal ll) = compare1 l ll + compare1 (Literal _) _ = LT + compare1 _ (Literal _) = GT + compare1 (SetLiteral s) (SetLiteral ss) = compare s ss + compare1 (SetLiteral _) _ = LT + compare1 _ (SetLiteral _) = GT + compare1 (Splice a) (Splice aa) = compare a aa + compare1 (Splice _) _ = LT + compare1 _ (Splice _) = GT + compare1 (Binop r) (Binop rr) = + compare r.lhs rr.lhs + <> compare r.rhs rr.rhs + <> compare r.op rr.op + compare1 (Binop _) _ = LT + compare1 _ (Binop _) = GT + compare1 (Unop r) (Unop rr) = + compare r.op rr.op + <> compare r.expr rr.expr + compare1 (Unop _) _ = LT + compare1 _ (Unop _) = GT + compare1 (Ident s) (Ident ss) = compare s ss + compare1 (Ident s) _ = LT + compare1 _ (Ident s) = GT + compare1 (InvokeFunction r) (InvokeFunction rr) = + compare r.name rr.name + <> compare r.args rr.args + compare1 (InvokeFunction _) _ = LT + compare1 _ (InvokeFunction _) = GT + compare1 (Match r) (Match rr) = + compare r.else_ rr.else_ + <> compare r.expr rr.expr + <> compare r.cases rr.cases + compare1 (Match _) _ = LT + compare1 _ (Match _) = GT + compare1 (Switch r) (Switch rr) = + compare r.else_ rr.else_ + <> compare r.cases rr.cases + compare1 (Switch _) _ = LT + compare1 _ (Switch _) = GT + compare1 (Let r) (Let rr) = + compare r.in_ rr.in_ + <> compare r.bindTo rr.bindTo + <> compare r.ident rr.ident + compare1 (Let _) _ = LT + compare1 _ (Let _) = GT + compare1 (Vari v) (Vari vv) = compare v vv + compare1 (Vari _) _ = LT + compare1 _ (Vari _) = GT + compare1 (Parens a) (Parens aa) = compare a aa + compare1 (Parens a) _ = LT + compare1 _ (Parens _) = GT + compare1 (Select r) (Select rr) = + compare r.isDistinct rr.isDistinct + <> compare r.projections rr.projections + <> compare r.filter rr.filter + <> compare r.relations rr.relations + <> compare r.orderBy rr.orderBy + <> compare r.groupBy rr.groupBy + +instance functorAST ∷ Functor l ⇒ Functor (SqlF l) where map f = case _ of Select { isDistinct, projections, relations, filter, groupBy, orderBy } → Select { isDistinct @@ -233,23 +219,11 @@ instance functorAST ∷ Functor SqlF where } Vari s → Vari s - BoolLiteral b → - BoolLiteral b - NullLiteral → - NullLiteral - StringLiteral s → - StringLiteral s - FloatLiteral n → - FloatLiteral n - IntLiteral i → - IntLiteral i Let { ident, bindTo, in_ } → Let { ident , bindTo: f bindTo , in_: f in_ } - MapLiteral lst → - MapLiteral $ map (bimap f f) lst Splice a → Splice $ map f a Binop { lhs, rhs, op } → @@ -267,34 +241,152 @@ instance functorAST ∷ Functor SqlF where InvokeFunction { name , args: map f args } - Match { expr, cases, default_ } → + Match { expr, cases, else_ } → Match { expr: f expr , cases: map (map f) cases - , default_: map f default_ + , else_: map f else_ } - Switch { cases, default_ } → + Switch { cases, else_ } → Switch { cases: map (map f) cases - , default_: map f default_ + , else_: map f else_ } SetLiteral lst → SetLiteral $ map f lst - ArrayLiteral lst → - ArrayLiteral $ map f lst - - -printF ∷ Algebra SqlF String -printF = case _ of + Literal l → + Literal $ map f l + Parens t → + Parens $ f t + + + +instance foldableSqlF ∷ F.Foldable l ⇒ F.Foldable (SqlF l) where + foldMap f = case _ of + Ident _ → mempty + SetLiteral lst → F.foldMap f lst + Splice mbA → F.foldMap f mbA + Binop { lhs, rhs } → f lhs <> f rhs + Unop { expr } → f expr + InvokeFunction { args } → F.foldMap f args + Match { expr, cases, else_ } → f expr <> F.foldMap (F.foldMap f) cases <> F.foldMap f else_ + Switch { cases, else_} → F.foldMap (F.foldMap f) cases <> F.foldMap f else_ + Let { bindTo, in_ } → f bindTo <> f in_ + Vari _ → mempty + Select { projections, relations, filter, groupBy, orderBy } → + F.foldMap (F.foldMap f) projections + <> F.foldMap (F.foldMap f) relations + <> F.foldMap f filter + <> F.foldMap (F.foldMap f) groupBy + <> F.foldMap (F.foldMap f) orderBy + Parens a → f a + Literal l → F.foldMap f l + foldl f a = case _ of + Ident _ → a + SetLiteral lst → F.foldl f a lst + Splice mbA → F.foldl f a mbA + Binop { lhs, rhs } → f (f a lhs) rhs + Unop { expr } → f a expr + InvokeFunction { args } → F.foldl f a args + Match { expr, cases, else_ } → + F.foldl f (F.foldl (F.foldl f) (f a expr) cases) else_ + Switch { cases, else_ } → + F.foldl f (F.foldl (F.foldl f) a cases) else_ + Let { bindTo, in_} → + f (f a bindTo) in_ + Vari _ → a + Select { projections, relations, filter, groupBy, orderBy } → + F.foldl (F.foldl f) + (F.foldl (F.foldl f) + (F.foldl f + (F.foldl (F.foldl f) + (F.foldl (F.foldl f) a + projections) + relations) + filter) + groupBy) + orderBy + Parens p → f a p + Literal l → F.foldl f a l + foldr f a = case _ of + Ident _ → a + SetLiteral lst → F.foldr f a lst + Splice mbA → F.foldr f a mbA + Binop { lhs, rhs } → f rhs $ f lhs a + Unop { expr } → f expr a + InvokeFunction { args } → F.foldr f a args + Match { expr, cases, else_ } → + F.foldr f (F.foldr (flip $ F.foldr f) (f expr a) cases) else_ + Switch { cases, else_ } → + F.foldr f (F.foldr (flip $ F.foldr f) a cases) else_ + Let { bindTo, in_ } → + f bindTo $ f in_ a + Vari _ → a + Select { projections, relations, filter, groupBy, orderBy } → + F.foldr (flip $ F.foldr f) + (F.foldr (flip $ F.foldr f) + (F.foldr f + (F.foldr (flip $ F.foldr f) + (F.foldr (flip $ F.foldr f) a + projections) + relations) + filter) + groupBy) + orderBy + Parens p → f p a + Literal l → F.foldr f a l + + + +instance traversableSqlF ∷ T.Traversable l ⇒ T.Traversable (SqlF l) where + traverse f = case _ of + SetLiteral lst → map SetLiteral $ T.traverse f lst + Literal l → map Literal $ T.traverse f l + Splice mbA → map Splice $ T.traverse f mbA + Binop { lhs, rhs, op } → + map Binop $ { lhs: _, rhs: _, op } <$> f lhs <*> f rhs + Unop { op, expr } → + map Unop $ { expr: _, op } <$> f expr + Ident s → pure $ Ident s + InvokeFunction { name, args } → + map InvokeFunction $ { name, args:_ } <$> T.traverse f args + Match { expr, cases, else_ } → + map Match + $ { expr: _, cases: _, else_: _ } + <$> f expr + <*> T.traverse (T.traverse f) cases + <*> T.traverse f else_ + Switch { cases, else_ } → + map Switch + $ { cases: _, else_: _ } + <$> T.traverse (T.traverse f) cases + <*> T.traverse f else_ + Let { bindTo, in_, ident } → + map Let + $ { bindTo: _, in_: _, ident } + <$> f bindTo + <*> f in_ + Vari s → pure $ Vari s + Parens p → map Parens $ f p + Select { isDistinct, projections, relations, filter, groupBy, orderBy } → + map Select + $ { isDistinct, projections: _, relations: _, filter: _, groupBy: _, orderBy: _} + <$> T.traverse (T.traverse f) projections + <*> T.traverse (T.traverse f) relations + <*> T.traverse f filter + <*> T.traverse (T.traverse f) groupBy + <*> T.traverse (T.traverse f) orderBy + sequence = T.sequenceDefault + +printF ∷ ∀ l. Algebra l String → Algebra (SqlF l) String +printF printLiteralF = case _ of + Splice Nothing → "*" + Splice (Just s) → s <> ".*" SetLiteral lst → "(" <> F.intercalate ", " lst <> ")" - ArrayLiteral lst → "[" <> F.intercalate ", " lst <> "]" - MapLiteral tplLst → "{" <> F.intercalate ", " (map (\(k × v) → k <> ": " <> v) tplLst) <> "}" - Splice mb → case mb of - Nothing → "*" - Just a → a <> ".*" + Literal l → printLiteralF l Binop {lhs, rhs, op} → case op of IfUndefined → lhs <> " ?? " <> rhs Range → lhs <> " .. " <> rhs - Or → lhs <> " or " <> rhs - And → lhs <> " and " <> rhs + Or → lhs <> " OR " <> rhs + And → lhs <> " AND " <> rhs Eq → lhs <> " = " <> rhs Neq → lhs <> " <> " <> rhs Ge → lhs <> " >= " <> rhs @@ -308,24 +400,24 @@ printF = case _ of Div → lhs <> " / " <> rhs Mod → lhs <> " % " <> rhs Pow → lhs <> " ^ " <> rhs - In → lhs <> " in " <> rhs + In → lhs <> " IN " <> rhs FieldDeref → lhs <> "." <> rhs IndexDeref → lhs <> "[" <> rhs <> "]" - Limit → lhs <> " limit " <> rhs - Offset → lhs <> " offset " <> rhs - Sample → lhs <> " sample " <> rhs - Union → lhs <> " union " <> rhs - UnionAll → lhs <> " union all " <> rhs - Intersect → lhs <> " intersect " <> rhs - IntersectAll → lhs <> " intersect all " <> rhs - Except → lhs <> " except " <> rhs + Limit → lhs <> " LIMIT " <> rhs + Offset → lhs <> " OFFSET " <> rhs + Sample → lhs <> " SAMPLE " <> rhs + Union → lhs <> " UNION " <> rhs + UnionAll → lhs <> " UNION ALL " <> rhs + Intersect → lhs <> " INTERSECT " <> rhs + IntersectAll → lhs <> " INTERSECT ALL " <> rhs + Except → lhs <> " EXCEPT " <> rhs UnshiftMap → "{" <> lhs <> ": " <> rhs <> "...}" Unop {expr, op} → case op of - Not → "not " <> expr - Exists → "exists " <> expr + Not → "NOT " <> expr + Exists → "EXISTS " <> expr Positive → "+" <> expr Negative → "-" <> expr - Distinct → "distinct " <> expr + Distinct → "DISTINCT " <> expr FlattenMapKeys → expr <> "{*: }" FlattenMapValues → expr <> "{*}" ShiftMapKeys → expr <> "{_: }" @@ -336,121 +428,35 @@ printF = case _ of ShiftArrayValues → expr <> "[_]" UnshiftArray → "[" <> expr <> "...]" Ident s → - s + "`" <> s <> "`" InvokeFunction {name, args} → name <> "(" <> F.intercalate "," args <> ")" - Match { expr, cases, default_ } → - "case " + Match { expr, cases, else_ } → + "CASE " <> expr <> F.intercalate " " (map printCase cases) - <> F.foldMap (" else " <> _) default_ - Switch { cases, default_ } → - "case " + <> F.foldMap (" ELSE " <> _) else_ + Switch { cases, else_ } → + "CASE " <> F.intercalate " " (map printCase cases) - <> F.foldMap (" else " <> _) default_ + <> F.foldMap (" ELSE " <> _) else_ Let { ident, bindTo, in_ } → ident <> " := " <> bindTo <> "; " <> in_ - IntLiteral int → - show int - FloatLiteral n → - show n - StringLiteral s → - show s - NullLiteral → - "null" - BoolLiteral b → - show b Vari s → ":" <> s Select { isDistinct, projections, relations, filter, groupBy, orderBy } → - "select" - <> (if isDistinct then " distinct " else "") + "SELECT " + <> (if isDistinct then "DISTINCT " else "") <> (F.intercalate ", " $ map printProjection projections) <> (relations # F.foldMap \rs → - " from " <> printRelation rs) - <> (filter # F.foldMap \f → " where " <> f) - <> (groupBy # F.foldMap \gb → " group by " <> printGroupBy gb) - <> (orderBy # F.foldMap \ob → " order by " <> printOrderBy ob) - - -type Sql = Mu SqlF - -print ∷ ∀ t. Recursive t SqlF ⇒ t → String -print = cata printF - --- | constructors -vari ∷ ∀ t. Corecursive t SqlF ⇒ String → t -vari s = embed $ Vari s - -bool ∷ ∀ t. Corecursive t SqlF ⇒ Boolean → t -bool b = embed $ BoolLiteral b - -null ∷ ∀ t. Corecursive t SqlF ⇒ t -null = embed NullLiteral - -int ∷ ∀ t. Corecursive t SqlF ⇒ Int → t -int i = embed $ IntLiteral i - -num ∷ ∀ t. Corecursive t SqlF ⇒ Number → t -num i = embed $ FloatLiteral i - -unop ∷ ∀ t. Corecursive t SqlF ⇒ UnaryOperator → t → t -unop op expr = embed $ Unop { op, expr } - -binop ∷ ∀ t. Corecursive t SqlF ⇒ BinaryOperator → t → t → t -binop op lhs rhs = embed $ Binop { op, lhs, rhs } - -set ∷ ∀ t. (Corecursive t SqlF, F.Foldable f) ⇒ f t → t -set l = embed $ SetLiteral $ fromFoldable f - -array ∷ ∀ t. (Corecursive t SqlF, F.Foldable f) ⇒ f t → t -array l = embed $ ArrayLiteral $ fromFoldable l - -splice ∷ ∀ t. Corecursive t SqlF ⇒ Maybe t → t -splice m = embed $ Splice m - -ident ∷ ∀ t. Corecursive t SqlF ⇒ String → t -ident i = embed $ Ident i - -match ∷ ∀ t. Corecursive t SqlF ⇒ t → List (Case t) → Maybe t → t -match expr cases default_ = embed $ Match { expr, cases, default_ } - -switch ∷ ∀ t. Corecursive t SqlF ⇒ List (Case t) → Maybe t → t -switch cases default_ = embed $ Switch { cases, default_ } - -let_ ∷ ∀ t. Corecursive t SqlF ⇒ String → t → t → t -let_ ident bindTo in_ = embed $ Let { ident, bindTo, in_ } - --- when_ (bool true) # then_ (num 1.0) :P -when_ ∷ ∀ t. t → (t → Case t) -when_ cond = Case <<< { cond, expr: _ } - -then_ ∷ ∀ t. (t → Case t) → t → Case t -then_ f t = f t - -select - ∷ ∀ t - . Corecursive t SqlF - ⇒ Boolean - → List (Projection t) - → Maybe (SqlRelation t) - → Maybe a - → Maybe (GroupBy a) - → Maybe (OrderBy a) -select isDistinct projections relations filter groupBy orderBy = - embed $ Select { isDistinct, projections, relations, filter, groupBy, orderBy } - - --- project_ (ident "foo") # as_ "bar" --- project_ (ident "foo") -project_ ∷ ∀ t. t → Projection t -project_ expr = Projection {expr, alias: Nothing} - -as_ ∷ ∀ t. String → Projection t → Projection t -as_ s (Projection r) = Projection r { alias = Just s } + " FROM " <> printRelation rs) + <> (filter # F.foldMap \f → " WHERE " <> f) + <> (groupBy # F.foldMap \gb → " GROUP BY " <> printGroupBy gb) + <> (orderBy # F.foldMap \ob → " ORDER BY " <> printOrderBy ob) + Parens t → + "(" <> t <> ")" -groupBy_ ∷ ∀ t f. Foldable f ⇒ f t → GroupBy t -groupBy_ f = GroupBy { keys: fromFoldable f, having: Nothing } +type Sql = Mu (SqlF EJsonF) -having_ ∷ ∀ t. t → GroupBy t → GroupBy t -having_ t (GroupBy r) = GroupBy r{ having = Just t } +print ∷ Sql → String +print = cata (printF renderEJsonF) diff --git a/src/SqlSquare/BinaryOperator.purs b/src/SqlSquare/BinaryOperator.purs new file mode 100644 index 0000000..d0e9003 --- /dev/null +++ b/src/SqlSquare/BinaryOperator.purs @@ -0,0 +1,37 @@ +module SqlSquare.BinaryOperator where + +import Prelude + +data BinaryOperator + = IfUndefined + | Range + | Or + | And + | Eq + | Neq + | Ge + | Gt + | Le + | Lt + | Concat + | Plus + | Minus + | Mult + | Div + | Mod + | Pow + | In + | FieldDeref + | IndexDeref + | Limit + | Offset + | Sample + | Union + | UnionAll + | Intersect + | IntersectAll + | Except + | UnshiftMap + +derive instance eqBinaryOperator ∷ Eq BinaryOperator +derive instance ordBinaryOperator ∷ Ord BinaryOperator diff --git a/src/SqlSquare/Case.purs b/src/SqlSquare/Case.purs new file mode 100644 index 0000000..5dc8681 --- /dev/null +++ b/src/SqlSquare/Case.purs @@ -0,0 +1,27 @@ +module SqlSquare.Case where + +import Prelude + +import Data.Newtype (class Newtype) +import Data.Foldable as F +import Data.Traversable as T + +import Matryoshka (Algebra) + +newtype Case a = Case { cond ∷ a, expr ∷ a } + +derive instance functorCase ∷ Functor Case +derive instance newtypeCase ∷ Newtype (Case a) _ +derive instance eqCase ∷ Eq a ⇒ Eq (Case a) +derive instance ordCase ∷ Ord a ⇒ Ord (Case a) + +instance foldableCase ∷ F.Foldable Case where + foldMap f (Case { cond, expr }) = f expr + foldl f a (Case { cond, expr }) = f (f a cond) expr + foldr f a (Case { cond, expr }) = f cond $ f expr a +instance traversableCase ∷ T.Traversable Case where + traverse f (Case { cond, expr }) = map Case $ { cond: _, expr: _ } <$> f cond <*> f expr + sequence = T.sequenceDefault + +printCase ∷ Algebra Case String +printCase (Case { cond, expr }) = " WHEN " <> cond <> " THEN " <> expr diff --git a/src/SqlSquare/Constructors.purs b/src/SqlSquare/Constructors.purs new file mode 100644 index 0000000..07f33ff --- /dev/null +++ b/src/SqlSquare/Constructors.purs @@ -0,0 +1,130 @@ +module SqlSquare.Constructors where + +import Prelude + +import Data.Array as Arr +import Data.DateTime as DT +import Data.Json.Extended.Signature (EJsonF(..)) +import Data.Foldable as F +import Data.HugeNum as HN +import Data.List as L +import Data.Map as Map +import Data.Maybe (Maybe(..)) + +import Matryoshka (class Corecursive, embed) + +import SqlSquare.AST (SqlF(..), Relation, GroupBy(..), OrderBy, BinaryOperator, UnaryOperator, (∘), SelectR, Case(..), Projection(..)) + +vari ∷ ∀ t f. Corecursive t (SqlF f) ⇒ String → t +vari s = embed $ Vari s + +bool ∷ ∀ t. Corecursive t (SqlF EJsonF) ⇒ Boolean → t +bool b = embed $ Literal $ Boolean b + +null ∷ ∀ t. Corecursive t (SqlF EJsonF) ⇒ t +null = embed $ Literal Null + +int ∷ ∀ t. Corecursive t (SqlF EJsonF) ⇒ Int → t +int i = embed $ Literal $ Integer i + +num ∷ ∀ t. Corecursive t (SqlF EJsonF) ⇒ Number → t +num i = embed $ Literal $ Decimal $ HN.fromNumber i + +string ∷ ∀ t. Corecursive t (SqlF EJsonF) ⇒ String → t +string s = embed $ Literal $ String s + +date ∷ ∀ t. Corecursive t (SqlF EJsonF) ⇒ DT.Date → t +date d = embed $ Literal $ Date d + +time ∷ ∀ t. Corecursive t (SqlF EJsonF) ⇒ DT.Time → t +time t = embed $ Literal $ Time t + +timestamp ∷ ∀ t. Corecursive t (SqlF EJsonF) ⇒ DT.DateTime → t +timestamp dt = embed $ Literal $ Timestamp dt + +unop ∷ ∀ t f. Corecursive t (SqlF f) ⇒ UnaryOperator → t → t +unop op expr = embed $ Unop { op, expr } + +binop ∷ ∀ t f. Corecursive t (SqlF f) ⇒ BinaryOperator → t → t → t +binop op lhs rhs = embed $ Binop { op, lhs, rhs } + +set ∷ ∀ t f g. (Corecursive t (SqlF g), F.Foldable f) ⇒ f t → t +set l = embed $ SetLiteral $ L.fromFoldable l + +array ∷ ∀ t f. (Corecursive t (SqlF EJsonF), F.Foldable f) ⇒ f t → t +array l = embed $ Literal $ Array $ Arr.fromFoldable l + +map_ ∷ ∀ t. (Corecursive t (SqlF EJsonF), Ord t) ⇒ Map.Map t t → t +map_ m = embed $ Literal $ Map $ Arr.fromFoldable $ Map.toList m + +splice ∷ ∀ t f. Corecursive t (SqlF f) ⇒ Maybe t → t +splice m = embed $ Splice m + +ident ∷ ∀ t f. Corecursive t (SqlF f) ⇒ String → t +ident i = embed $ Ident i + +match ∷ ∀ t f. Corecursive t (SqlF f) ⇒ t → L.List (Case t) → Maybe t → t +match expr cases else_ = embed $ Match { expr, cases, else_ } + +switch ∷ ∀ t f. Corecursive t (SqlF f) ⇒ L.List (Case t) → Maybe t → t +switch cases else_ = embed $ Switch { cases, else_ } + +let_ ∷ ∀ t f. Corecursive t (SqlF f) ⇒ String → t → t → t +let_ id bindTo in_ = embed $ Let { ident: id, bindTo, in_ } + +invokeFunction ∷ ∀ t f. Corecursive t (SqlF f) ⇒ String → L.List t → t +invokeFunction name args = embed $ InvokeFunction {name, args} + +-- when (bool true) # then_ (num 1.0) :P +when ∷ ∀ t. t → (t → Case t) +when cond = Case ∘ { cond, expr: _ } + +then_ ∷ ∀ t. (t → Case t) → t → Case t +then_ f t = f t + +select + ∷ ∀ t f + . (Corecursive t (SqlF EJsonF), F.Foldable f) + ⇒ Boolean + → f (Projection t) + → Maybe (Relation t) + → Maybe t + → Maybe (GroupBy t) + → Maybe (OrderBy t) + → t +select isDistinct projections relations filter gb orderBy = + embed $ Select { isDistinct + , projections: L.fromFoldable projections + , relations + , filter + , groupBy: gb + , orderBy + } + + +-- project (ident "foo") # as "bar" +-- project (ident "foo") +projection ∷ ∀ t. t → Projection t +projection expr = Projection {expr, alias: Nothing} + +as ∷ ∀ t. String → Projection t → Projection t +as s (Projection r) = Projection r { alias = Just s } + +groupBy ∷ ∀ t f. F.Foldable f ⇒ f t → GroupBy t +groupBy f = GroupBy { keys: L.fromFoldable f, having: Nothing } + +having ∷ ∀ t. t → GroupBy t → GroupBy t +having t (GroupBy r) = GroupBy r{ having = Just t } + +buildSelect ∷ ∀ t f. Corecursive t (SqlF f) ⇒ (SelectR t → SelectR t) → t +buildSelect f = + embed $ Select $ f { isDistinct: false + , projections: L.Nil + , relations: Nothing + , filter: Nothing + , groupBy: Nothing + , orderBy: Nothing + } + +pars ∷ ∀ t f. Corecursive t (SqlF f) ⇒ t → t +pars = embed ∘ Parens diff --git a/src/SqlSquare/GroupBy.purs b/src/SqlSquare/GroupBy.purs new file mode 100644 index 0000000..bf6fae8 --- /dev/null +++ b/src/SqlSquare/GroupBy.purs @@ -0,0 +1,31 @@ +module SqlSquare.GroupBy where + +import Prelude + +import Data.Foldable as F +import Data.Traversable as T +import Data.List as L +import Data.Maybe (Maybe) +import Data.Newtype (class Newtype) + +import Matryoshka (Algebra) + +newtype GroupBy a = GroupBy { keys ∷ L.List a, having ∷ Maybe a } +derive instance newtypeGroupBy ∷ Newtype (GroupBy a) _ +derive instance functorGroupBy ∷ Functor GroupBy +derive instance eqGroupBy ∷ Eq a ⇒ Eq (GroupBy a) +derive instance ordGroupBy ∷ Ord a ⇒ Ord (GroupBy a) + +instance foldableGroupBy ∷ F.Foldable GroupBy where + foldMap f (GroupBy { keys, having }) = F.foldMap f keys <> F.foldMap f having + foldl f a (GroupBy { keys, having }) = F.foldl f (F.foldl f a keys) having + foldr f a (GroupBy { keys, having }) = F.foldr f (F.foldr f a having) keys + +instance traversableGroupBy ∷ T.Traversable GroupBy where + traverse f (GroupBy { keys, having }) = + map GroupBy $ {keys: _, having: _} <$> T.traverse f keys <*> T.traverse f having + sequence = T.sequenceDefault + +printGroupBy ∷ Algebra GroupBy String +printGroupBy (GroupBy { keys, having }) = + F.intercalate ", " keys <> F.foldMap (" HAVING " <> _) having diff --git a/src/SqlSquare/JoinType.purs b/src/SqlSquare/JoinType.purs new file mode 100644 index 0000000..6b8b8a6 --- /dev/null +++ b/src/SqlSquare/JoinType.purs @@ -0,0 +1,19 @@ +module SqlSquare.JoinType where + +import Prelude + +data JoinType + = LeftJoin + | RightJoin + | InnerJoin + | FullJoin + +printJoinType ∷ JoinType → String +printJoinType = case _ of + LeftJoin → "LEFT JOIN" + RightJoin → "RIGHT JOIN" + FullJoin → "FULL JOIN" + InnerJoin → "INNER JOIN" + +derive instance eqJoinType ∷ Eq JoinType +derive instance ordJoinType ∷ Ord JoinType diff --git a/src/SqlSquare/Lenses.purs b/src/SqlSquare/Lenses.purs new file mode 100644 index 0000000..e8786b0 --- /dev/null +++ b/src/SqlSquare/Lenses.purs @@ -0,0 +1,321 @@ +module SqlSquare.Lenses where + +import Prelude + +import Data.DateTime as DT +import Data.HugeNum as HN +import Data.Json.Extended as EJ +import Data.Lens (Prism', prism', Lens', lens, Iso') +import Data.Lens.Iso.Newtype (_Newtype) +import Data.List as L +import Data.Maybe as M +import Data.NonEmpty as NE + +import Matryoshka (class Recursive, class Corecursive, embed, project) + +import SqlSquare.AST as S +import SqlSquare.Utils (type (×), (∘), (⋙)) + +_GroupBy ∷ ∀ a. Iso' (S.GroupBy a) {keys ∷ L.List a, having ∷ M.Maybe a} +_GroupBy = _Newtype + +_Case ∷ ∀ a. Iso' (S.Case a) { cond ∷ a, expr ∷ a } +_Case = _Newtype + +_OrderBy ∷ ∀ a. Iso' (S.OrderBy a) (NE.NonEmpty L.List (S.OrderType × a)) +_OrderBy = _Newtype + +_Projection ∷ ∀ a. Iso' (S.Projection a) { expr ∷ a, alias ∷ M.Maybe String } +_Projection = _Newtype + +_JoinRelation ∷ ∀ a. Prism' (S.Relation a) (S.JoinRelR a) +_JoinRelation = prism' S.JoinRelation case _ of + S.JoinRelation r → M.Just r + _ → M.Nothing + +_ExprRelation ∷ ∀ a. Prism' (S.Relation a) (S.ExprRelR a) +_ExprRelation = prism' S.ExprRelation case _ of + S.ExprRelation r → M.Just r + _ → M.Nothing + +_TableRelation ∷ ∀ a. Prism' (S.Relation a) (S.TableRelR a) +_TableRelation = prism' S.TableRelation case _ of + S.TableRelation r → M.Just r + _ → M.Nothing + +_VariRelation ∷ ∀ a. Prism' (S.Relation a) (S.VariRelR a) +_VariRelation = prism' S.VariRelation case _ of + S.VariRelation r → M.Just r + _ → M.Nothing + +_IdentRelation ∷ ∀ a. Prism' (S.Relation a) S.IdentRelR +_IdentRelation = prism' S.IdentRelation case _ of + S.IdentRelation r → M.Just r + _ → M.Nothing + + +_lhs ∷ ∀ a r. Lens' { lhs ∷ a |r } a +_lhs = lens _.lhs _{ lhs = _ } + +_rhs ∷ ∀ a r. Lens' { rhs ∷ a |r } a +_rhs = lens _.rhs _{ rhs = _ } + +_op ∷ ∀ a r. Lens' { op ∷ a | r } a +_op = lens _.op _{ op = _ } + +_expr ∷ ∀ a r. Lens' { expr ∷ a|r } a +_expr = lens _.expr _{ expr = _ } + +_name ∷ ∀ a r. Lens' { name ∷ a|r } a +_name = lens _.name _{ name = _ } + +_args ∷ ∀ a r. Lens' { args ∷ a|r } a +_args = lens _.args _{ args = _ } + +_cases ∷ ∀ a r. Lens' { cases ∷ a|r } a +_cases = lens _.cases _{ cases = _ } + +_else ∷ ∀ a r. Lens' { else_ ∷ a|r } a +_else = lens _.else_ _{ else_ = _ } + +_ident ∷ ∀ a r. Lens' { ident ∷ a|r } a +_ident = lens _.ident _{ ident = _ } + +_bindTo ∷ ∀ a r. Lens' { bindTo ∷ a|r } a +_bindTo = lens _.bindTo _{ bindTo = _ } + +_in ∷ ∀ a r. Lens' { in_ ∷ a|r } a +_in = lens _.in_ _{ in_ = _ } -- __O_M_G__ + +_isDistinct ∷ ∀ a r. Lens' { isDistinct ∷ a|r } a +_isDistinct = lens _.isDistinct _{ isDistinct = _ } + +_projections ∷ ∀ a r. Lens' { projections ∷ a|r } a +_projections = lens _.projections _{ projections = _ } + +_relations ∷ ∀ a r. Lens' { relations ∷ a|r } a +_relations = lens _.relations _{ relations = _ } + +_filter ∷ ∀ a r. Lens' { filter ∷ a|r } a +_filter = lens _.filter _{ filter = _ } + +_groupBy ∷ ∀ a r. Lens' { groupBy ∷ a|r } a +_groupBy = lens _.groupBy _{ groupBy = _ } + +_orderBy ∷ ∀ a r. Lens' { orderBy ∷ a|r } a +_orderBy = lens _.orderBy _{ orderBy = _ } + +_keys ∷ ∀ a r. Lens' { keys ∷ a|r } a +_keys = lens _.keys _{ keys = _ } + +_having ∷ ∀ a r. Lens' { having ∷ a|r } a +_having = lens _.having _{ having = _ } + +_cond ∷ ∀ a r. Lens' { cond ∷ a|r } a +_cond = lens _.cond _{ cond = _ } + +_alias ∷ ∀ a r. Lens' { alias ∷ a|r } a +_alias = lens _.alias _{ alias = _ } + +_aliasName ∷ ∀ a r. Lens' { aliasName ∷ a|r } a +_aliasName = lens _.aliasName _{ aliasName = _ } + +_left ∷ ∀ a r. Lens' { left ∷ a|r } a +_left = lens _.left _{ left = _ } + +_right ∷ ∀ a r. Lens' { right ∷ a|r } a +_right = lens _.right _{ right = _ } + +_joinType ∷ ∀ a r. Lens' { joinType ∷ a|r } a +_joinType = lens _.joinType _{ joinType = _ } + +_clause ∷ ∀ a r. Lens' { clause ∷ a|r } a +_clause = lens _.clause _{ clause = _ } + +_tablePath ∷ ∀ a r. Lens' { tablePath ∷ a|r } a +_tablePath = lens _.tablePath _{ tablePath = _ } + + +_SetLiteral + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (L.List t) +_SetLiteral = prism' (embed ∘ S.SetLiteral) $ project ⋙ case _ of + S.SetLiteral lst → M.Just lst + _ → M.Nothing + +_Literal + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (f t) +_Literal = prism' (embed ∘ S.Literal) $ project ⋙ case _ of + S.Literal js → M.Just js + _ → M.Nothing + +_ArrayLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t (Array t) +_ArrayLiteral = prism' (embed ∘ S.Literal ∘ EJ.Array) $ project ⋙ case _ of + S.Literal (EJ.Array a) → M.Just a + _ → M.Nothing + +_MapLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t (Array (t × t)) +_MapLiteral = prism' (embed ∘ S.Literal ∘ EJ.Map) $ project ⋙ case _ of + S.Literal (EJ.Map tpls) → M.Just tpls + _ → M.Nothing + +_Splice + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (M.Maybe t) +_Splice = prism' (embed ∘ S.Splice) $ project ⋙ case _ of + S.Splice m → M.Just m + _ → M.Nothing + +_Binop + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (S.BinopR t) +_Binop = prism' (embed ∘ S.Binop) $ project ⋙ case _ of + S.Binop b → M.Just b + _ → M.Nothing + +_Unop + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (S.UnopR t) +_Unop = prism' (embed ∘ S.Unop) $ project ⋙ case _ of + S.Unop r → M.Just r + _ → M.Nothing + +_Ident + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t String +_Ident = prism' (embed ∘ S.Ident) $ project ⋙ case _ of + S.Ident s → M.Just s + _ → M.Nothing + +_InvokeFunction + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (S.InvokeFunctionR t) +_InvokeFunction = prism' (embed ∘ S.InvokeFunction) $ project ⋙ case _ of + S.InvokeFunction r → M.Just r + _ → M.Nothing + +_Match + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (S.MatchR t) +_Match = prism' (embed ∘ S.Match) $ project ⋙ case _ of + S.Match r → M.Just r + _ → M.Nothing + +_Switch + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (S.SwitchR t) +_Switch = prism' (embed ∘ S.Switch) $ project ⋙ case _ of + S.Switch r → M.Just r + _ → M.Nothing + +_Let + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (S.LetR t) +_Let = prism' (embed ∘ S.Let) $ project ⋙ case _ of + S.Let r → M.Just r + _ → M.Nothing + +_IntLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t Int +_IntLiteral = prism' (embed ∘ S.Literal ∘ EJ.Integer) $ project ⋙ case _ of + S.Literal (EJ.Integer r) → M.Just r + _ → M.Nothing + +_DecimalLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t HN.HugeNum +_DecimalLiteral = prism' (embed ∘ S.Literal ∘ EJ.Decimal) $ project ⋙ case _ of + S.Literal (EJ.Decimal r) → M.Just r + _ → M.Nothing + +_StringLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t String +_StringLiteral = prism' (embed ∘ S.Literal ∘ EJ.String) $ project ⋙ case _ of + S.Literal (EJ.String r) → M.Just r + _ → M.Nothing + +_NullLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t Unit +_NullLiteral = prism' (const $ embed $ S.Literal EJ.Null) $ project ⋙ case _ of + S.Literal EJ.Null → M.Just unit + _ → M.Nothing + +_BoolLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t Boolean +_BoolLiteral = prism' (embed ∘ S.Literal ∘ EJ.Boolean) $ project ⋙ case _ of + S.Literal (EJ.Boolean b) → M.Just b + _ → M.Nothing + +_Vari + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t String +_Vari = prism' (embed ∘ S.Vari) $ project ⋙ case _ of + S.Vari r → M.Just r + _ → M.Nothing + +_Select + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t (S.SelectR t) +_Select = prism' (embed ∘ S.Select) $ project ⋙ case _ of + S.Select r → M.Just r + _ → M.Nothing + +_Parens + ∷ ∀ t f + . (Recursive t (S.SqlF f), Corecursive t (S.SqlF f)) + ⇒ Prism' t t +_Parens = prism' (embed ∘ S.Parens) $ project ⋙ case _ of + S.Parens t → M.Just t + _ → M.Nothing + +_TimeLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t DT.Time +_TimeLiteral = prism' (embed ∘ S.Literal ∘ EJ.Time) $ project ⋙ case _ of + S.Literal (EJ.Time t) → M.Just t + _ → M.Nothing + +_DateLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t DT.Date +_DateLiteral = prism' (embed ∘ S.Literal ∘ EJ.Date) $ project ⋙ case _ of + S.Literal (EJ.Date d) → M.Just d + _ → M.Nothing + +_TimestampLiteral + ∷ ∀ t + . (Recursive t (S.SqlF EJ.EJsonF), Corecursive t (S.SqlF EJ.EJsonF)) + ⇒ Prism' t DT.DateTime +_TimestampLiteral = prism' (embed ∘ S.Literal ∘ EJ.Timestamp) $ project ⋙ case _ of + S.Literal (EJ.Timestamp dt) → M.Just dt + _ → M.Nothing diff --git a/src/SqlSquare/OrderBy.purs b/src/SqlSquare/OrderBy.purs new file mode 100644 index 0000000..ca2328a --- /dev/null +++ b/src/SqlSquare/OrderBy.purs @@ -0,0 +1,32 @@ +module SqlSquare.OrderBy where + +import Prelude + +import Data.Foldable as F +import Data.Traversable as T +import Data.List as L +import Data.Newtype (class Newtype) +import Data.NonEmpty as NE + +import Matryoshka (Algebra) + +import SqlSquare.OrderType as OT +import SqlSquare.Utils ((×), type (×)) + +newtype OrderBy a = OrderBy (NE.NonEmpty L.List (OT.OrderType × a)) + +derive instance functorOrderBy ∷ Functor OrderBy +derive instance newtypeOrderBy ∷ Newtype (OrderBy a) _ +derive instance eqOrderBy ∷ Eq a ⇒ Eq (OrderBy a) +derive instance ordOrderBy ∷ Ord a ⇒ Ord (OrderBy a) +instance foldableOrderBy ∷ F.Foldable OrderBy where + foldMap f (OrderBy xs) = F.foldMap (F.foldMap f) xs + foldl f a (OrderBy xs) = F.foldl (F.foldl f) a xs + foldr f a (OrderBy xs) = F.foldr (flip (F.foldr f)) a xs +instance traversableOrderBy ∷ T.Traversable OrderBy where + traverse f (OrderBy xs) = map OrderBy $ T.traverse (T.traverse f) xs + sequence = T.sequenceDefault + +printOrderBy ∷ Algebra OrderBy String +printOrderBy (OrderBy lst) = + F.intercalate ", " $ lst <#> \(ot × a) → a <> " " <> OT.printOrderType ot diff --git a/src/SqlSquare/OrderType.purs b/src/SqlSquare/OrderType.purs new file mode 100644 index 0000000..a660af7 --- /dev/null +++ b/src/SqlSquare/OrderType.purs @@ -0,0 +1,13 @@ +module SqlSquare.OrderType where + +import Prelude + +data OrderType = ASC | DESC + +printOrderType ∷ OrderType → String +printOrderType = case _ of + ASC → "ASC" + DESC → "DESC" + +derive instance eqOrderType ∷ Eq OrderType +derive instance ordOrderType ∷ Ord OrderType diff --git a/src/SqlSquare/Projection.purs b/src/SqlSquare/Projection.purs new file mode 100644 index 0000000..1b54f44 --- /dev/null +++ b/src/SqlSquare/Projection.purs @@ -0,0 +1,30 @@ +module SqlSquare.Projection where + +import Prelude + +import Data.Foldable as F +import Data.Traversable as T +import Data.Maybe (Maybe) +import Data.Newtype (class Newtype) + +import Matryoshka (Algebra) + +import SqlSquare.Utils ((∘)) + +newtype Projection a = Projection { expr ∷ a, alias ∷ Maybe String } + +derive instance functorProjection ∷ Functor Projection +derive instance newtypeProjection ∷ Newtype (Projection a) _ +derive instance eqProjection ∷ Eq a ⇒ Eq (Projection a) +derive instance ordProjection ∷ Ord a ⇒ Ord (Projection a) +instance foldableProjection ∷ F.Foldable Projection where + foldMap f (Projection { expr }) = f expr + foldl f a (Projection { expr }) = f a expr + foldr f a (Projection { expr }) = f expr a +instance traversableProjection ∷ T.Traversable Projection where + traverse f (Projection { expr, alias }) = + map (Projection ∘ { expr: _, alias}) $ f expr + sequence = T.sequenceDefault + +printProjection ∷ Algebra Projection String +printProjection (Projection { expr, alias }) = expr <> F.foldMap (" AS " <> _) alias diff --git a/src/SqlSquare/Relation.purs b/src/SqlSquare/Relation.purs new file mode 100644 index 0000000..0a5a34a --- /dev/null +++ b/src/SqlSquare/Relation.purs @@ -0,0 +1,109 @@ +module SqlSquare.Relation where + +import Prelude + +import Data.Either (Either, either) +import Data.Foldable as F +import Data.Monoid (mempty) +import Data.Traversable as T +import Data.Maybe (Maybe) +import Data.Path.Pathy (AbsFile, RelFile, Unsandboxed, unsafePrintPath) + +import Matryoshka (Algebra) + +import SqlSquare.JoinType as JT +import SqlSquare.Utils ((∘)) + +type FUPath = Either (RelFile Unsandboxed) (AbsFile Unsandboxed) + +type JoinRelR a = + { left ∷ Relation a + , right ∷ Relation a + , joinType ∷ JT.JoinType + , clause ∷ a + } + +type ExprRelR a = + { expr ∷ a + , aliasName ∷ String + } + +type TableRelR a = + { tablePath ∷ FUPath + , alias ∷ Maybe String + } + +type VariRelR a = + { vari ∷ String + , alias ∷ Maybe String + } + +type IdentRelR = + { ident ∷ String + , alias ∷ Maybe String + } + +data Relation a + = JoinRelation (JoinRelR a) + | ExprRelation (ExprRelR a) + | TableRelation (TableRelR a) + | VariRelation (VariRelR a) + | IdentRelation IdentRelR + +derive instance functorRelation ∷ Functor Relation +derive instance eqRelation ∷ Eq a ⇒ Eq (Relation a) +derive instance ordRelation ∷ Ord a ⇒ Ord (Relation a) +instance foldableRelation ∷ F.Foldable Relation where + foldMap f = case _ of + JoinRelation { left, right, clause } → F.foldMap f left <> F.foldMap f right <> f clause + ExprRelation { expr } → f expr + _ → mempty + foldl f a = case _ of + JoinRelation { left, right, clause } → + f (F.foldl f (F.foldl f a left) right) clause + ExprRelation { expr } → + f a expr + _ → a + foldr f a = case _ of + JoinRelation { left, right, clause } → + F.foldr f (F.foldr f (f clause a) right) left + ExprRelation { expr } → + f expr a + _ → a +instance traversableRelation ∷ T.Traversable Relation where + traverse f = case _ of + JoinRelation { left, right, clause, joinType } → + map JoinRelation + $ { joinType, left: _, right: _, clause: _} + <$> T.traverse f left + <*> T.traverse f right + <*> f clause + ExprRelation { expr, aliasName} → + (ExprRelation ∘ { expr: _, aliasName}) + <$> f expr + TableRelation t → pure $ TableRelation t + VariRelation v → pure $ VariRelation v + IdentRelation i → pure $ IdentRelation i + sequence = T.sequenceDefault + +printRelation ∷ Algebra Relation String +printRelation = case _ of + ExprRelation {expr, aliasName} → + "(" <> expr <> ") AS " <> aliasName + VariRelation { vari, alias} → + vari <> F.foldMap (" AS " <> _) alias + TableRelation { tablePath, alias } → + "`" + <> either unsafePrintPath unsafePrintPath tablePath + <> "`" + <> F.foldMap (" AS " <> _) alias + IdentRelation { ident, alias } → + ident <> F.foldMap (\x → " AS `" <> x <> "`") alias + JoinRelation { left, right, joinType, clause } → + printRelation left + <> " " + <> JT.printJoinType joinType + <> " " + <> printRelation right + <> " on " + <> clause diff --git a/src/SqlSquare/UnaryOperator.purs b/src/SqlSquare/UnaryOperator.purs new file mode 100644 index 0000000..e3e7b01 --- /dev/null +++ b/src/SqlSquare/UnaryOperator.purs @@ -0,0 +1,22 @@ +module SqlSquare.UnaryOperator where + +import Prelude + +data UnaryOperator + = Not + | Exists + | Positive + | Negative + | Distinct + | FlattenMapKeys + | FlattenMapValues + | ShiftMapKeys + | ShiftMapValues + | FlattenArrayIndices + | FlattenArrayValues + | ShiftArrayIndices + | ShiftArrayValues + | UnshiftArray + +derive instance eqUnaryOperator ∷ Eq UnaryOperator +derive instance ordUnaryOperator ∷ Ord UnaryOperator diff --git a/src/SqlSquare/Utils.purs b/src/SqlSquare/Utils.purs new file mode 100644 index 0000000..a770fea --- /dev/null +++ b/src/SqlSquare/Utils.purs @@ -0,0 +1,16 @@ +module SqlSquare.Utils where + +import Prelude +import Data.Tuple (Tuple(..)) +import Data.Functor.Coproduct (Coproduct, coproduct) + +infixr 4 type Tuple as × +infixr 1 Tuple as × +infixr 9 compose as ∘ +infixr 4 type Coproduct as ⨁ +infixr 5 coproduct as ⨁ + +composeFlipped ∷ ∀ a b c d. Semigroupoid a ⇒ a b c → a c d → a b d +composeFlipped f g = compose g f + +infixr 9 composeFlipped as ⋙ diff --git a/test/src/Argonaut.purs b/test/src/Argonaut.purs new file mode 100644 index 0000000..bd6c62f --- /dev/null +++ b/test/src/Argonaut.purs @@ -0,0 +1,137 @@ +-- | An example of using `purescript-sqlsquare` library +-- | Having an array of `Json`s construct a list of Sql² projections +module Test.Argonaut where + +import Prelude + +import Data.Argonaut (JCursor(..), jsonParser) +import Data.Argonaut as JS +import Data.Either (fromRight) +import Data.Foldable as F +import Data.List ((:)) +import Data.List as L +import Data.Maybe (Maybe(..)) +import Data.Set as Set +import Data.Tuple (Tuple, fst) +import Data.Json.Extended.Signature (EJsonF(..)) + +import SqlSquare as S +import SqlSquare.Utils ((×), (∘), (⋙)) + +import Matryoshka (ana, elgotPara, Coalgebra, ElgotAlgebra) + +import Test.Unit (suite, test, TestSuite) +import Test.Unit.Assert as Assert + +import Partial.Unsafe (unsafePartial) + +data UnfoldableJC = JC JCursor | S String | I Int + +jcCoalgebra ∷ Coalgebra (S.SqlF EJsonF) UnfoldableJC +jcCoalgebra = case _ of + S s → S.Ident s + I i → S.Literal (Integer i) + JC cursor → case cursor of + JCursorTop → S.Splice Nothing + JIndex i c → S.Binop { op: S.IndexDeref, lhs: JC c, rhs: I i } + JField f c → S.Binop { op: S.FieldDeref, lhs: JC c, rhs: S f } + +jcursorToSql ∷ JCursor → S.Sql +jcursorToSql = JS.insideOut ⋙ JC ⋙ ana jcCoalgebra + +fields ∷ JS.JArray → L.List S.Sql +fields arr = + map jcursorToSql $ L.fromFoldable $ F.foldMap (Set.fromFoldable ∘ map fst) $ map JS.toPrims arr + +allParentsF ∷ ElgotAlgebra (Tuple S.Sql) (S.SqlF EJsonF) (L.List S.Sql) +allParentsF (parent × sqlF) = case sqlF of + S.Splice (Just ps) → ps + S.Unop { op: S.FlattenArrayValues, expr } → parent : expr + S.Unop { op: S.FlattenMapValues, expr } → parent : expr + S.Binop { op: S.FieldDeref, lhs } → parent : lhs + S.Binop { op: S.IndexDeref, lhs } → parent : lhs + _ → L.Nil + +allParents ∷ S.Sql → L.List S.Sql +allParents = elgotPara allParentsF + +allFields ∷ JS.JArray → L.List S.Sql +allFields = + L.fromFoldable ∘ F.foldMap (Set.fromFoldable ∘ allParents) ∘ fields + +jarray ∷ JS.JArray +jarray = + map (unsafePartial fromRight ∘ jsonParser) jsonStrings + where + jsonStrings = + [ """{"foo": [{"bar": 1}, 12], "bar": {"baz": false}}""" + , """{"foo": true}""" + , """[12, null]""" + ] +testSuite ∷ ∀ e. TestSuite e +testSuite = + suite "tests for argonaut example" do + test "interpretation works" + let + expected = + "*.`foo`[1][2][0]" + : "*.`foo`.`bar`.`baz`" + : L.Nil + js = + (JField "foo" $ JIndex 1 $ JIndex 2 $ JIndex 0 $ JCursorTop) + : (JField "foo" $ JField "bar" $ JField "baz" $ JCursorTop) + : L.Nil + in + Assert.equal expected $ map (S.print ∘ jcursorToSql) js + test "extraction of fields works" + let + actualFields = + Set.fromFoldable + $ map S.print $ fields jarray + expectedFields = + Set.fromFoldable + $ "*[0]" + : "*[1]" + : "*.`foo`" + : "*.`foo`[1]" + : "*.`foo`[0].`bar`" + : "*.`bar`.`baz`" + : L.Nil + in + Assert.equal expectedFields actualFields + test "allParents extracted" + let + field = + jcursorToSql + $ JField "foo" + $ JField "bar" + $ JIndex 0 + $ JField "baz" + $ JIndex 1 + $ JCursorTop + expected = + Set.fromFoldable + $ "*.`foo`" + : "*.`foo`.`bar`" + : "*.`foo`.`bar`[0]" + : "*.`foo`.`bar`[0].`baz`" + : "*.`foo`.`bar`[0].`baz`[1]" + : L.Nil + in + Assert.equal expected $ Set.fromFoldable $ map S.print $ allParents field + test "allFields works" + let + actualFields = Set.fromFoldable $ map S.print $ allFields jarray + expectedFields = + Set.fromFoldable + $ "*[0]" + : "*[1]" + : "*.`foo`" + : "*.`foo`[1]" + : "*.`foo`[0].`bar`" + : "*.`bar`.`baz`" + : "*.`bar`" + : L.Nil + in + Assert.equal "1" "1" +-- Assert.equal expectedFields actualFields diff --git a/test/src/Constructors.purs b/test/src/Constructors.purs new file mode 100644 index 0000000..1848d5f --- /dev/null +++ b/test/src/Constructors.purs @@ -0,0 +1,64 @@ +module Test.Constructors where + +import Prelude + +import Data.Either (Either(..)) +import Data.List as L +import Data.Lens ((.~), (<>~), (?~)) +import Data.Maybe (Maybe(..)) +import Data.NonEmpty as NE +import Data.Path.Pathy as Pt + +import SqlSquare as S +import SqlSquare.Utils ((×), (∘)) + +import Test.Unit (suite, test, TestSuite) +import Test.Unit.Assert as Assert + +selectQuery ∷ S.Sql +selectQuery = + S.select + true + [ S.projection (S.ident "foo") # S.as "field" + , S.projection $ S.splice $ Just $ S.binop S.FieldDeref (S.ident "bar") (S.ident "baz") + ] + ( map + (S.TableRelation ∘ { alias: Nothing, tablePath: _ } ∘ Right) + $ Pt.parseAbsFile "/mongo/testDb/patients" ) + ( Just $ S.binop S.Eq (S.ident "quux") (S.num 12.0) ) + ( Just $ S.groupBy [ S.ident "zzz" ] # S.having ( S.binop S.Gt (S.ident "ooo") ( S.int 2)) ) + ( Just $ S.OrderBy $ NE.singleton $ S.ASC × (S.ident "zzz") ) + +buildSelectQuery ∷ S.Sql +buildSelectQuery = + S.buildSelect + $ (S._isDistinct .~ true) + ∘ (S._projections <>~ + (L.singleton + $ S.projection + $ S.splice + $ Just + $ S.binop + S.FieldDeref + (S.ident "bar") + (S.ident "baz"))) + ∘ (S._projections <>~ (L.singleton $ S.projection (S.ident "foo") # S.as "field")) + ∘ (S._relations .~ + ( map (S.TableRelation ∘ { alias: Nothing, tablePath: _} ∘ Right) + $ Pt.parseAbsFile "/mongo/testDb/patients")) + ∘ (S._filter ?~ S.binop S.Eq (S.ident "quux") (S.num 12.0)) + ∘ (S._groupBy ?~ + (S.groupBy [ S.ident "zzz" ] # S.having (S.binop S.Gt (S.ident "ooo") (S.int 2)))) + ∘ (S._orderBy ?~ S.OrderBy (NE.singleton $ S.ASC × (S.ident "zzz"))) + +expectedSqlString ∷ String +expectedSqlString = + "SELECT DISTINCT `foo` AS field, `bar`.`baz`.* FROM `/mongo/testDb/patients` WHERE `quux` = 12.0 GROUP BY `zzz` HAVING `ooo` > 2 ORDER BY `zzz` ASC" + +testSuite ∷ ∀ e. TestSuite e +testSuite = + suite "tests for sql constructors" do + test "constructing select query with multiple arguments" + $ Assert.equal expectedSqlString $ S.print selectQuery + test "building select query with lenses" + $ Assert.equal expectedSqlString $ S.print buildSelectQuery diff --git a/test/src/Main.purs b/test/src/Main.purs index 656964c..66cf0e0 100644 --- a/test/src/Main.purs +++ b/test/src/Main.purs @@ -1,24 +1,26 @@ module Test.Main where import Prelude -import Control.Monad.Eff (Eff) -import Data.List (List) -import Data.Functor.Mu (Mu) -import Debug.Trace (traceAnyA) -import SqlSquare.AST as S -import Matryoshka (class Corecursive, embed, cata) +import Control.Monad.Aff.AVar (AVAR) +import Control.Monad.Eff (Eff) +import Control.Monad.Eff.Console (CONSOLE) -num ∷ ∀ t. Corecursive t S.AST ⇒ Number → t -num n = embed (S.FloatLiteral n) +import Test.Unit.Main (runTest) +import Test.Unit.Console (TESTOUTPUT) -invokeFunction ∷ ∀ t. Corecursive t S.AST ⇒ String → List t → t -invokeFunction name args = embed (S.InvokeFunction { name, args }) +import Test.Constructors as Constructors +import Test.Argonaut as Argonaut +import Test.Search as Search -someExpr ∷ ∀ t. Corecursive t S.AST ⇒ t -someExpr = invokeFunction "foo" $ pure $ num 12.0 +type Effects = + ( testOutput ∷ TESTOUTPUT + , avar ∷ AVAR + , console ∷ CONSOLE + ) -main ∷ ∀ e. Eff e Unit -main = do - traceAnyA (someExpr ∷ Mu S.AST) - traceAnyA $ cata S.print (someExpr ∷ Mu S.AST) +main ∷ Eff Effects Unit +main = runTest do + Constructors.testSuite + Argonaut.testSuite + Search.testSuite diff --git a/test/src/Search.purs b/test/src/Search.purs new file mode 100644 index 0000000..e3e3062 --- /dev/null +++ b/test/src/Search.purs @@ -0,0 +1,416 @@ +-- | In this example `purescript-search` query is interpreted to Sql² +-- | using additional `List Sql` with projections (see `Test.Argonaut` to find out how to +-- | get it) +module Test.Search where + +import Prelude + +import Control.Alt ((<|>)) +import Control.MonadZero (guard) + +import Data.Argonaut as JS +import Data.Either (Either(..), fromRight) +import Data.Foldable as F +import Data.Int as Int +import Data.Lens ((.~), (?~)) +import Data.List ((:)) +import Data.List as L +import Data.Maybe (Maybe(..), isJust, fromJust, fromMaybe) +import Data.Newtype (unwrap) +import Data.Path.Pathy as Pt +import Data.String as Str +import Data.String.Regex as RX +import Data.String.Regex.Flags as RXF +import Data.String.Regex.Unsafe as URX +import Data.Json.Extended.Signature as EJ + +import Global (readFloat, isNaN) + +import SqlSquare as S +import SqlSquare.Utils ((∘), (×)) + +import Matryoshka (Algebra, Coalgebra, Transform, ana, cata, transAna) + +import Partial.Unsafe (unsafePartial) + +import Test.Unit (suite, test, TestSuite) +import Test.Unit.Assert as Assert + +import Test.Argonaut as Ar + +import Text.SlamSearch (mkQuery) +import Text.SlamSearch.Types as SS + +-------------------------------------------------------------------------------- +-- Guards and filters +-------------------------------------------------------------------------------- + +stringToNumber ∷ String → Maybe Number +stringToNumber s = + let n = readFloat s + in if isNaN n + then Nothing + else Just n + +stringToBoolean ∷ String → Maybe Boolean +stringToBoolean "true" = Just true +stringToBoolean "false" = Just false +stringToBoolean _ = Nothing + +needDate ∷ String → Boolean +needDate = RX.test dateRegex + where + dateRegex = + URX.unsafeRegex + """^(((19|20)([2468][048]|[13579][26]|0[48])|2000)[-]02[-]29|((19|20)[0-9]{2}[-](0[4678]|1[02])[-](0[1-9]|[12][0-9]|30)|(19|20)[0-9]{2}[-](0[1359]|11)[-](0[1-9]|[12][0-9]|3[01])|(19|20)[0-9]{2}[-]02[-](0[1-9]|1[0-9]|2[0-8])))$""" + RXF.noFlags + + +needTime ∷ String → Boolean +needTime = RX.test timeRegex + where + timeRegex = + URX.unsafeRegex + "^([0-1]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$" + RXF.noFlags + + +needDateTime ∷ String → Boolean +needDateTime = RX.test dtRegex + where + dtRegex = + URX.unsafeRegex + "^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9]) (2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?$" + RXF.noFlags + +needInterval ∷ String → Boolean +needInterval = RX.test intervalRegex + where + intervalRegex = + URX.unsafeRegex + "P((([0-9]*\\.?[0-9]*)Y)?(([0-9]*\\.?[0-9]*)M)?(([0-9]*\\.?[0-9]*)W)?(([0-9]*\\.?[0-9]*)D)?)?(T(([0-9]*\\.?[0-9]*)H)?(([0-9]*\\.?[0-9]*)M)?(([0-9]*\\.?[0-9]*)S)?)?" + RXF.noFlags + + +-------------------------------------------------------------------------------- +-- Accessors +-------------------------------------------------------------------------------- + +labelString ∷ SS.Label → String +labelString = case _ of + SS.Meta l → l + SS.Common l → l + +identOrString ∷ ∀ a. (S.SqlF EJ.EJsonF) a → Maybe String +identOrString = case _ of + S.Ident s → Just s + S.Literal (EJ.String s) → Just s + _ → Nothing + +valueToString ∷ SS.Value → String +valueToString = case _ of + SS.Text v → v + SS.Tag v → v + +-------------------------------------------------------------------------------- +-- Predicate aggregations +-------------------------------------------------------------------------------- + +ors ∷ L.List S.Sql → S.Sql +ors = case _ of + L.Nil → S.bool false + hd : L.Nil → S.pars hd + hd : tl → F.foldl (\acc sql → S.binop S.Or acc $ S.pars sql) hd tl + +ands ∷ L.List S.Sql → S.Sql +ands = case _ of + L.Nil → S.bool true + hd : L.Nil → S.pars hd + hd : tl → F.foldl (\acc sql → S.binop S.And acc $ S.pars sql) hd tl + +-------------------------------------------------------------------------------- +-- Filtering only top fields +-------------------------------------------------------------------------------- + +data TopFieldMark + = Init + | TopField + | NotTopField + +isTop ∷ TopFieldMark → Boolean +isTop = case _ of + NotTopField → false + _ → true + +topFieldF ∷ Algebra (S.SqlF EJ.EJsonF) TopFieldMark +topFieldF = case _ of + S.Splice Nothing → Init + S.Ident _ → TopField + S.Literal (EJ.Integer _) → TopField + S.Literal (EJ.String _) → TopField + S.Binop { op: S.FieldDeref, lhs: Init, rhs: TopField } → TopField + S.Binop { op: S.IndexDeref, lhs: Init, rhs: TopField } → TopField + _ → NotTopField + +isTopField ∷ S.Sql → Boolean +isTopField = isTop ∘ cata topFieldF + +-------------------------------------------------------------------------------- +-- Flattening all array derefs ( `foo[1]` → `foo[*]` ) +-------------------------------------------------------------------------------- + +flattenIndexF ∷ ∀ t. Transform t (S.SqlF EJ.EJsonF) (S.SqlF EJ.EJsonF) +flattenIndexF = case _ of + S.Binop { op: S.IndexDeref, lhs } → S.Unop { op: S.FlattenArrayValues, expr: lhs } + s → s + +flattenIndex ∷ S.Sql → S.Sql +flattenIndex = transAna flattenIndexF + +-------------------------------------------------------------------------------- +-- Searching for flatten values ({*}, [*]) +-------------------------------------------------------------------------------- + +needDistinctF ∷ Algebra (S.SqlF EJ.EJsonF) Boolean +needDistinctF = case _ of + S.SetLiteral ns → F.or ns + S.Literal (EJ.Array ns) → F.or ns + S.Literal (EJ.Map tpls) → F.any (\(a × b) → a || b) tpls + S.Splice Nothing → false + S.Splice (Just a) → a + S.Binop { lhs, rhs } → lhs || rhs + S.Unop { op: S.FlattenArrayValues } → true + S.Unop { op: S.FlattenMapValues } → true + S.Unop { expr } → expr + S.Ident _ → false + S.InvokeFunction { args } → F.or args + S.Match { expr, cases, else_ } → + expr || F.any (\(S.Case { cond, expr: e }) → e || cond) cases || fromMaybe false else_ + S.Switch { cases, else_ } → + F.any (\(S.Case { cond, expr }) → cond || expr) cases || fromMaybe false else_ + S.Let { bindTo, in_ } → + bindTo || in_ + S.Literal _ → false + S.Vari _ → false + S.Parens a → a + S.Select { projections, filter } → + F.any (\(S.Projection { expr }) → expr) projections || fromMaybe false filter + +needDistinct ∷ S.Sql → Boolean +needDistinct = cata needDistinctF + +-------------------------------------------------------------------------------- +-- Interpretation +-------------------------------------------------------------------------------- + +extractFields ∷ SS.Term → Maybe S.Sql +extractFields (SS.Term { labels }) + | L.null labels = Nothing + | otherwise = Just $ ana labelToFieldF $ map labelString $ L.reverse labels + +termToSql ∷ L.List S.Sql → SS.Term → S.Sql +termToSql fs (SS.Term { include, predicate, labels}) + | not include = + S.unop S.Not $ termToSql fields $ SS.Term { include: true, predicate, labels} + | otherwise = + ors + $ flip predicateToSql predicate + <$> (if L.null labels then fs else pure $ labelsToField labels) + + +labelToFieldF ∷ Coalgebra (S.SqlF EJ.EJsonF) (L.List String) +labelToFieldF = case _ of + L.Nil → S.Splice Nothing + hd : L.Nil → case toInt hd of + Just i → S.Literal (EJ.Integer i) + Nothing → S.Ident hd + hd : tl → case toInt hd of + Just i → S.Binop { op: S.IndexDeref, lhs: tl, rhs: pure hd } + Nothing → case hd of + "[*]" → S.Unop { op: S.FlattenArrayValues, expr: tl } + "{*}" → S.Unop { op: S.FlattenMapValues, expr: tl } + "*" → S.Unop { op: S.FlattenMapValues, expr: tl } + a → S.Binop { op: S.FieldDeref, lhs: tl, rhs: pure hd } + where + toInt ∷ String → Maybe Int + toInt s = + (Int.fromString s) + <|> (Str.stripSuffix (Str.Pattern "]") s >>= Str.stripPrefix (Str.Pattern "[") >>= Int.fromString) + + +labelsToField ∷ L.List SS.Label → S.Sql +labelsToField = ana labelToFieldF ∘ map labelString ∘ L.reverse + +-- | Getting sql field and search predicate construct sql predicate +predicateToSql ∷ S.Sql → SS.Predicate → S.Sql +predicateToSql field = case _ of + SS.Contains (SS.Text v) → + ors + $ (pure + $ S.invokeFunction "search" + $ field + : (S.string $ globToRegex $ containsToGlob v) + : S.bool true + : L.Nil + ) + <> (sqlsFromSearchStr v <#> S.binop S.Eq field) + SS.Range (SS.Text v) (SS.Text vv) → + ors + $ ( pure $ S.binop S.And + ( S.pars $ S.binop S.Ge (lower field) (lower $ S.string v)) + ( S.pars $ S.binop S.Le (lower field) (lower $ S.string vv)) + ) + <> do + start ← sqlsFromSearchStr v + end ← sqlsFromSearchStr vv + pure $ S.binop S.And + ( S.pars $ S.binop S.Ge field start ) + ( S.pars $ S.binop S.Le field end ) + SS.Range (SS.Tag val) vv → + predicateToSql field $ SS.Range (SS.Text val) vv + SS.Range val (SS.Tag vv) → + predicateToSql field $ SS.Range val (SS.Text vv) + SS.Contains (SS.Tag v) → + predicateToSql field $ SS.Contains $ SS.Text v + + SS.Eq v → renderBinRel S.Eq $ valueToString v + SS.Gt v → renderBinRel S.Gt $ valueToString v + SS.Gte v → renderBinRel S.Ge $ valueToString v + SS.Lt v → renderBinRel S.Lt $ valueToString v + SS.Lte v → renderBinRel S.Le $ valueToString v + SS.Ne v → renderBinRel S.Neq $ valueToString v + SS.Like v → + S.invokeFunction "search" + $ field : S.string v : S.bool true : L.Nil + where + renderBinRel ∷ S.BinaryOperator → String → S.Sql + renderBinRel op v = + ors + $ ( pure $ S.binop op (lower field) (lower $ S.string v)) + <> ( sqlsFromSearchStr v <#> S.binop op field) + + sqlsFromSearchStr ∷ String → L.List S.Sql + sqlsFromSearchStr v = + (flip F.foldMap (stringToNumber v) $ pure ∘ S.num) + <> (flip F.foldMap (Int.fromString v) $ pure ∘ S.int) + <> (flip F.foldMap (stringToBoolean v) $ pure ∘ S.bool) + <> ((guard ((not $ needDateTime v) && needDate v)) $> + S.invokeFunction "DATE" (S.string v : L.Nil)) + <> (guard (needTime v) $> + S.invokeFunction "TIME" (S.string v : L.Nil)) + <> (guard (needDateTime v) $> + S.invokeFunction "TIMESTAMP" (S.string v : L.Nil)) + <> (guard (needInterval v) $> + S.invokeFunction "INTERVAL" (S.string v : L.Nil)) + + lower ∷ S.Sql → S.Sql + lower = S.invokeFunction "LOWER" ∘ pure + +globToRegex ∷ String → String +globToRegex = + (\x → "^" <> x <> "$") + ∘ RX.replace askRegex "." + ∘ RX.replace starRegex ".*" + ∘ RX.replace globEscapeRegex "\\$&" + where + globEscapeRegex = + URX.unsafeRegex + "[\\-\\[\\]\\/\\{\\}\\(\\)\\+\\.\\\\\\^\\$\\|]" + RXF.global + + starRegex = + URX.unsafeRegex + "\\*" RXF.global + askRegex = + URX.unsafeRegex + "\\?" RXF.global + +containsToGlob ∷ String → String +containsToGlob v + | hasSpecialChars v = v + | otherwise = "*" <> v <> "*" + +hasSpecialChars ∷ String → Boolean +hasSpecialChars v = + isJust (Str.indexOf (Str.Pattern "*") v) || isJust (Str.indexOf (Str.Pattern "?") v) + +-------------------------------------------------------------------------------- +-- Interpreter entry point +-------------------------------------------------------------------------------- + +queryToSql + ∷ L.List S.Sql + → SS.SearchQuery + → S.FUPath + → S.Sql +queryToSql fs query path = + S.buildSelect + $ (S._isDistinct .~ isDistinct) + ∘ (S._projections .~ topFields) + ∘ (S._relations ?~ S.TableRelation { alias: Nothing, tablePath: path }) + ∘ (S._filter ?~ filter) + where + topFields = + map (S.Projection ∘ { expr: _, alias: Nothing}) $ L.filter isTopField fs + + isDistinct = needDistinct filter + + filter = + ands + $ map ors + $ unwrap + $ map (termToSql $ map flattenIndex fs) query + +-------------------------------------------------------------------------------- +-- Tests +-------------------------------------------------------------------------------- + +fields ∷ L.List S.Sql +fields = Ar.allFields jarray + where + jarray ∷ JS.JArray + jarray = map (unsafePartial fromRight ∘ JS.jsonParser) jsonStrings + + jsonStrings ∷ Array String + jsonStrings = + [ """{"foo": 1, "bar": 2}""" + , """{"foo": [1, 2], "bar": null}""" + , """{"foo": 3, "bar": { "valid": false, "value": "baz" } }""" + ] + +searchQueries ∷ L.List SS.SearchQuery +searchQueries = + F.foldMap (F.foldMap pure) $ map mkQuery searchStrings + where + searchStrings ∷ L.List String + searchStrings = + """ba""" + : """foo:"[*]":2""" + : """bar:>1""" + : """false""" + : """bar:valid:=false""" + : """"non-existing":foo""" + : L.Nil + +expectedOutput ∷ L.List String +expectedOutput = + """SELECT DISTINCT *.`bar`, *.`foo` FROM `/mongo/testDb/patients` WHERE (((search(*.`bar`,"^.*ba.*$",true)) OR ((search(*.`foo`,"^.*ba.*$",true))) OR ((search(*.`bar`.`valid`,"^.*ba.*$",true))) OR ((search(*.`bar`.`value`,"^.*ba.*$",true))) OR ((search(*.`foo`[*],"^.*ba.*$",true))) OR ((search(*.`foo`[*],"^.*ba.*$",true)))))""" + : """SELECT DISTINCT *.`bar`, *.`foo` FROM `/mongo/testDb/patients` WHERE (((search(`foo`[*],"^.*2.*$",true) OR (`foo`[*] = 2.0) OR (`foo`[*] = 2))))""" + : """SELECT *.`bar`, *.`foo` FROM `/mongo/testDb/patients` WHERE (((LOWER(`bar`) > LOWER("1") OR (`bar` > 1.0) OR (`bar` > 1))))""" + : """SELECT DISTINCT *.`bar`, *.`foo` FROM `/mongo/testDb/patients` WHERE ((search(*.`bar`,"^.*false.*$",true) OR (*.`bar` = false) OR (search(*.`foo`,"^.*false.*$",true) OR (*.`foo` = false)) OR (search(*.`bar`.`valid`,"^.*false.*$",true) OR (*.`bar`.`valid` = false)) OR (search(*.`bar`.`value`,"^.*false.*$",true) OR (*.`bar`.`value` = false)) OR (search(*.`foo`[*],"^.*false.*$",true) OR (*.`foo`[*] = false)) OR (search(*.`foo`[*],"^.*false.*$",true) OR (*.`foo`[*] = false))))""" + : """SELECT *.`bar`, *.`foo` FROM `/mongo/testDb/patients` WHERE (((LOWER(`bar`.`valid`) = LOWER("false") OR (`bar`.`valid` = false))))""" + : """SELECT *.`bar`, *.`foo` FROM `/mongo/testDb/patients` WHERE ((((search(`non-existing`,"^.*foo.*$",true)))))""" + : L.Nil + +tablePath ∷ S.FUPath +tablePath = Right $ unsafePartial fromJust $ Pt.parseAbsFile "/mongo/testDb/patients" + +testSuite ∷ ∀ e. TestSuite e +testSuite = + suite "purescript-search interpreter tests" do + test "search query is interpreted correctly" + let + querySqls = map (\sq → queryToSql fields sq tablePath) searchQueries + querySqlsStrings = map S.print querySqls + in + void $ L.zipWithA Assert.equal expectedOutput querySqlsStrings