forked from juspay/euler-hs
-
Notifications
You must be signed in to change notification settings - Fork 1
/
QueryExamplesSpec.hs
399 lines (325 loc) · 11.5 KB
/
QueryExamplesSpec.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE StandaloneDeriving #-}
module SQLDB.Tests.QueryExamplesSpec where
import Data.Aeson (encode)
import qualified Data.ByteString.Lazy as BSL
import Data.Time
import EulerHS.Prelude hiding (getOption)
import Test.Hspec hiding (runIO)
import Unsafe.Coerce
import EulerHS.Interpreters
import EulerHS.Language
import EulerHS.Runtime (withFlowRuntime)
import EulerHS.Types hiding (error)
import qualified EulerHS.Language as L
import qualified EulerHS.Runtime as R
import qualified EulerHS.Types as T
import Database.Beam ((&&.), (/=.), (<-.), (<.), (==.), (>.), (>=.))
import qualified Database.Beam as B
import qualified Database.Beam.Backend.SQL as B
import qualified Database.Beam.Sqlite as BS
date1 :: LocalTime
date1 = LocalTime
{ localDay = toEnum 56195
, localTimeOfDay = defaultTimeOfDay1
}
defaultTimeOfDay1 :: TimeOfDay
defaultTimeOfDay1 = TimeOfDay
{ todHour = 1
, todMin = 1
, todSec = 1
}
-- 2012-09-26 18:08:45
date2 :: LocalTime
date2 = LocalTime
{ localDay = toEnum 56196
, localTimeOfDay = td2
}
td2 :: TimeOfDay
td2 = TimeOfDay
{ todHour = 18
, todMin = 8
, todSec = 45
}
date3 :: LocalTime
date3 = LocalTime
{ localDay = toEnum 56190
, localTimeOfDay = td3
}
date4 :: LocalTime
date4 = LocalTime
{ localDay = toEnum 56191
, localTimeOfDay = td3
}
td3 :: TimeOfDay
td3 = TimeOfDay
{ todHour = 0
, todMin = 0
, todSec = 0
}
data MemberT f = Member
{ memberId :: B.C f Int
, surName :: B.C f Text
, firstName :: B.C f Text
, address :: B.C f Text
, zipCode :: B.C f Int
, telephone :: B.C f Text
, recommendedBy :: B.C f (Maybe Int)
, joinDate :: B.C f LocalTime
} deriving (Generic, B.Beamable)
instance B.Table MemberT where
data PrimaryKey MemberT f =
MemberId (B.C f Int) deriving (Generic, B.Beamable)
primaryKey = MemberId . memberId
type Member = MemberT Identity
type MemberId = B.PrimaryKey MemberT Identity
deriving instance Show MemberId
deriving instance Eq MemberId
deriving instance ToJSON MemberId
deriving instance FromJSON MemberId
deriving instance Show Member
deriving instance Eq Member
deriving instance ToJSON Member
deriving instance FromJSON Member
membersEMod :: B.EntityModification
(B.DatabaseEntity be db) be (B.TableEntity MemberT)
membersEMod = B.modifyTableFields
B.tableModification
{ memberId = B.fieldNamed "memid"
, surName = B.fieldNamed "surname"
, firstName = B.fieldNamed "firstname"
, address = B.fieldNamed "address"
, zipCode = B.fieldNamed "zipcode"
, telephone = B.fieldNamed "telephone"
, recommendedBy = B.fieldNamed "recommendedby"
, joinDate = B.fieldNamed "joindate"
}
data FacilityT f = Facility
{ facilityId :: B.C f Int
, name :: B.C f Text
, memberCost :: B.C f Double
, guestCost :: B.C f Double
, initialOutlay :: B.C f Double
, monthlyMaintenance :: B.C f Double
} deriving (Generic, B.Beamable)
instance B.Table FacilityT where
data PrimaryKey FacilityT f =
FacrId (B.C f Int) deriving (Generic, B.Beamable)
primaryKey = FacrId . facilityId
type Facility = FacilityT Identity
type FacrId = B.PrimaryKey FacilityT Identity
deriving instance Show FacrId
deriving instance Eq FacrId
deriving instance ToJSON FacrId
deriving instance FromJSON FacrId
deriving instance Show Facility
deriving instance Eq Facility
deriving instance ToJSON Facility
deriving instance FromJSON Facility
facilitiesEMod :: B.EntityModification
(B.DatabaseEntity be db) be (B.TableEntity FacilityT)
facilitiesEMod = B.modifyTableFields
B.tableModification
{ facilityId = B.fieldNamed "facid"
, name = B.fieldNamed "name"
, memberCost = B.fieldNamed "membercost"
, guestCost = B.fieldNamed "guestcost"
, initialOutlay = B.fieldNamed "initialoutlay"
, monthlyMaintenance = B.fieldNamed "monthlymaintenance"
}
data BookingT f = Booking
{ bookId :: B.C f Int
, facId :: B.PrimaryKey FacilityT f
, bmemid :: B.PrimaryKey MemberT f
, starttime :: B.C f LocalTime
, slots :: B.C f Int
} deriving (Generic, B.Beamable)
instance B.Table BookingT where
data PrimaryKey BookingT f =
BookId (B.C f Int) deriving (Generic, B.Beamable)
primaryKey = BookId . bookId
type Booking = BookingT Identity
type BookId = B.PrimaryKey BookingT Identity
deriving instance Show Booking
deriving instance Eq Booking
deriving instance ToJSON Booking
deriving instance FromJSON Booking
bookingsEMod :: B.EntityModification
(B.DatabaseEntity be db) be (B.TableEntity BookingT)
bookingsEMod = B.modifyTableFields
B.tableModification
{ bookId = B.fieldNamed "bookid"
, facId = FacrId (B.fieldNamed "facid")
, bmemid = MemberId (B.fieldNamed "memid")
, starttime = B.fieldNamed "starttime"
, slots = B.fieldNamed "slots"
}
data ClubDB f = ClubDB
{ members :: f (B.TableEntity MemberT)
, facilities :: f (B.TableEntity FacilityT)
, bookings :: f (B.TableEntity BookingT)
} deriving (Generic, B.Database be)
clubDB :: B.DatabaseSettings be ClubDB
clubDB = B.defaultDbSettings `B.withDbModification`
B.dbModification
{ facilities = facilitiesEMod
, members = membersEMod
, bookings = bookingsEMod
}
testDBName :: String
testDBName = "./testDB/SQLDB/TestData/test.db"
testDBTemplateName :: String
testDBTemplateName = "./testDB/SQLDB/TestData/query_examples.db.template"
poolConfig = T.PoolConfig
{ stripes = 1
, keepAlive = 10
, resourcesPerStripe = 50
}
sqliteCfg = T.mkSQLitePoolConfig "clubSQliteDB" testDBName poolConfig
connectOrFail :: T.DBConfig beM -> Flow (T.SqlConn beM)
connectOrFail cfg = L.initSqlDBConnection cfg >>= \case
Left e -> error $ show e
Right conn -> pure conn
rmTestDB :: L.Flow ()
rmTestDB = void $ L.runSysCmd $ "rm -f " <> testDBName
prepareTestDB :: L.Flow ()
prepareTestDB = do
rmTestDB
void $ L.runSysCmd $ "cp " <> testDBTemplateName <> " " <> testDBName
pure ()
--Basic string searches
textSearchLike :: L.Flow (T.DBResult [Facility])
textSearchLike = do
conn <- connectOrFail sqliteCfg
L.runDB conn
$ L.findRows
$ B.select
$ B.filter_ (\f -> name f `B.like_` "%Tennis%")
$ B.all_ (facilities clubDB)
-- Matching against multiple possible values
matchMultValues :: L.Flow (T.DBResult [Facility])
matchMultValues = do
conn <- connectOrFail sqliteCfg
L.runDB conn
$ L.findRows
$ B.select
$ B.filter_ (\f -> facilityId f `B.in_` [1,5])
$ B.all_ (facilities clubDB)
searchByDate :: L.Flow (T.DBResult [Member])
searchByDate = do
conn <- connectOrFail sqliteCfg
L.runDB conn
$ L.findRows
$ B.select
$ B.filter_ (\m -> joinDate m >=. B.val_ date1)
$ B.all_ (members clubDB)
-- Removing duplicates, and ordering results
groupDistinctLimit :: L.Flow (T.DBResult [Text])
groupDistinctLimit = do
conn <- connectOrFail sqliteCfg
L.runDB conn
$ L.findRows
$ B.select
$ B.limit_ 10
$ B.nub_
$ fmap surName
$ B.orderBy_ (B.asc_ . surName)
$ B.all_ (members clubDB)
-- Combining results from multiple queries with union
selectWithUnion:: L.Flow (T.DBResult [Text])
selectWithUnion = do
conn <- connectOrFail sqliteCfg
L.runDB conn $
let
sn = fmap surName
$ B.all_ (members clubDB)
n = fmap name
$ B.all_ (facilities clubDB)
in L.findRows $ B.select $ B.limit_ 3 $ B.union_ sn n
aggregate1 :: L.Flow (T.DBResult (Maybe LocalTime))
aggregate1 = do
conn <- connectOrFail sqliteCfg
res <- L.runDB conn $
L.findRow $ B.select
$ B.aggregate_ (\m -> B.max_ (joinDate m ))
$ B.all_ (members clubDB)
pure $ join <$> res
aggregate2 :: L.Flow (T.DBResult (Maybe (Text, Text,(LocalTime))))
aggregate2 = do
conn <- connectOrFail sqliteCfg
L.runDB conn $
L.findRow $ B.select $ do
mdate <- B.aggregate_ (\ms -> ( B.max_ (joinDate ms)))
$ B.all_ (members clubDB)
lm <- B.filter_ (\m -> joinDate m ==. B.fromMaybe_ (B.val_ date2) mdate) $ B.all_ (members clubDB)
pure (firstName lm, surName lm, joinDate lm)
join1 :: L.Flow (T.DBResult [LocalTime])
join1 = do
conn <- connectOrFail sqliteCfg
L.runDB conn $
L.findRows $ B.select $ fmap starttime $ do
ms <- B.all_ (members clubDB)
bs <- B.join_ (bookings clubDB) (\book -> bmemid book ==. B.primaryKey ms)
B.guard_ (firstName ms ==. "David" &&. surName ms ==. "Farrell")
pure bs
join2 :: L.Flow (T.DBResult [(LocalTime, Text)])
join2 = do
conn <- connectOrFail sqliteCfg
L.runDB conn
$ L.findRows
$ B.select
$ B.orderBy_ (B.asc_ . fst)
$ fmap (\(fs,bs) -> (starttime bs,name fs))
$ do
fs <- B.all_ (facilities clubDB)
bs <- B.join_ (bookings clubDB) (\book -> facId book ==. B.primaryKey fs)
B.guard_ ( starttime bs >=. (B.val_ date3)
&&. starttime bs <. (B.val_ date4)
&&. name fs `B.like_` "%Tennis Court%")
pure (fs, bs)
loggerCfg = defaultLoggerConfig
{ _logToFile = True
, _logFilePath = "/tmp/euler-backend.log"
, _isAsync = True
}
withEmptyDB :: (R.FlowRuntime -> IO ()) -> IO ()
withEmptyDB act = withFlowRuntime Nothing (\rt -> do
try (runFlow rt prepareTestDB) >>= \case
Left (e :: SomeException) ->
runFlow rt rmTestDB
`finally` error ("Preparing test values failed: " <> show e)
Right _ -> act rt `finally` runFlow rt rmTestDB
)
spec :: Spec
spec =
around withEmptyDB $
describe "Query examples" $ do
it "Text search with 'like' operator " $ \rt -> do
eRes <- runFlow rt textSearchLike
eRes `shouldBe` Right [Facility {facilityId = 0, name = "Tennis Court 1", memberCost = 5.0, guestCost = 25.0, initialOutlay = 10000.0, monthlyMaintenance = 200.0},Facility {facilityId = 1, name = "Tennis Court 2", memberCost = 5.0, guestCost = 25.0, initialOutlay = 8000.0, monthlyMaintenance = 200.0},Facility {facilityId = 3, name = "Table Tennis", memberCost = 0.0, guestCost = 5.0, initialOutlay = 320.0, monthlyMaintenance = 10.0}]
it "Match against mult values" $ \rt -> do
eRes <- runFlow rt matchMultValues
eRes `shouldBe` Right [Facility {facilityId = 1, name = "Tennis Court 2", memberCost = 5.0, guestCost = 25.0, initialOutlay = 8000.0, monthlyMaintenance = 200.0},Facility {facilityId = 5, name = "Massage Room 2", memberCost = 35.0, guestCost = 80.0, initialOutlay = 4000.0, monthlyMaintenance = 3000.0}]
it "search by date" $ \rt -> do
eRes <- runFlow rt searchByDate
(length <$> eRes) `shouldBe` Right 1
it "orderDistinctLimit" $ \rt -> do
eRes <- runFlow rt groupDistinctLimit
eRes `shouldBe` Right ["Bader","Baker","Boothe","Butters","Coplin","Crumpet","Dare","Farrell","GUEST","Genting"]
it "selectWithUnion" $ \rt -> do
eRes <- runFlow rt selectWithUnion
eRes `shouldBe` Right ["Bader","Badminton Court","Baker"]
it "aggregate1" $ \rt -> do
eRes <- runFlow rt aggregate1
eRes `shouldBe` Right (Just date2)
it "aggregate2" $ \rt -> do
eRes <- runFlow rt aggregate2
eRes `shouldBe` Right (Just ("Darren","Smith", date2))
it "join1" $ \rt -> do
eRes <- runFlow rt join1
length <$> eRes `shouldBe` Right 34
it "join2" $ \rt -> do
eRes <- runFlow rt join2
eRes `shouldBe` fmap (sortOn fst) eRes
length <$> eRes `shouldBe` Right 12