on dev help sign in
better inference
d7e5675Eitan Chatav
squeel-postgresql/squeel-postgresql.cabal−1
@@ -48,7 +48,6 @@ test-suite squeel-postgresql-test
48 build-depends:
48 build-depends:
49 base
49 base
50 , Boolean
50 , Boolean
51− , bytestring
52 , hspec
51 , hspec
53 , squeel-postgresql
52 , squeel-postgresql
54 , vinyl
53 , vinyl
squeel-postgresql/src/Squeel/PostgreSQL/PQ.hs+1−1
@@ -77,7 +77,7 @@ class MonadPQ pq where
77 :: (MonadBase IO io, ToOids ps)
77 :: (MonadBase IO io, ToOids ps)
78 => ByteString
78 => ByteString
79 -> Query ps db0 db1 xs
79 -> Query ps db0 db1 xs
80− -> pq db0 db1 io (Maybe (Result []), PreparedQuery db0 db1 ps xs)
80+ -> pq db0 db1 io (Maybe (Result []), PreparedQuery ps db0 db1 xs)
81
81
82 pqExecPrepared
82 pqExecPrepared
83 :: (MonadBase IO io, ToValues xs ps)
83 :: (MonadBase IO io, ToValues xs ps)
squeel-postgresql/src/Squeel/PostgreSQL/Query.hs+88−64
@@ -6,8 +6,6 @@
6 , LambdaCase
6 , LambdaCase
7 , MagicHash
7 , MagicHash
8 , OverloadedStrings
8 , OverloadedStrings
9− , PolyKinds
10− , ScopedTypeVariables
11 , TypeFamilies
9 , TypeFamilies
12 , TypeOperators
10 , TypeOperators
13 , UndecidableInstances
11 , UndecidableInstances
@@ -62,8 +60,8 @@ instance {-# OVERLAPPING #-} KnownSymbol column
62instance {-# OVERLAPPABLE #-} (KnownSymbol column, HasColumn column table ty)
60instance {-# OVERLAPPABLE #-} (KnownSymbol column, HasColumn column table ty)
63 => HasColumn column (ty' ': table) ty
61 => HasColumn column (ty' ': table) ty
64
62
65−instance HasColumn column columns ty
63+instance (HasColumn column columns ty, tables ~ '[table ::: columns])
66− => IsLabel column (Expression params '[table ::: columns] ty) where
64+ => IsLabel column (Expression params tables ty) where
67 fromLabel = getColumn
65 fromLabel = getColumn
68
66
69(.&.)
67(.&.)
@@ -167,18 +165,18 @@ crossJoin
167 :: TableRef params schema '[table]
165 :: TableRef params schema '[table]
168 -> TableRef params schema tables
166 -> TableRef params schema tables
169 -> TableRef params schema (table ': tables)
167 -> TableRef params schema (table ': tables)
170−crossJoin table tables = UnsafeTableRef $
168+crossJoin tab tabs = UnsafeTableRef $
171− renderTableRef tables <> " CROSS JOIN " <> renderTableRef table
169+ renderTableRef tabs <> " CROSS JOIN " <> renderTableRef tab
172
170
173innerJoin
171innerJoin
174 :: TableRef params schema '[table]
172 :: TableRef params schema '[table]
175 -> Expression params (table ': tables) 'PGBool
173 -> Expression params (table ': tables) 'PGBool
176 -> TableRef params schema tables
174 -> TableRef params schema tables
177 -> TableRef params schema (table ': tables)
175 -> TableRef params schema (table ': tables)
178−innerJoin table on tables = UnsafeTableRef $
176+innerJoin tab on tabs = UnsafeTableRef $
179− renderTableRef tables
177+ renderTableRef tabs
180 <> " INNER JOIN " <>
178 <> " INNER JOIN " <>
181− renderTableRef table
179+ renderTableRef tab
182 <> " ON " <>
180 <> " ON " <>
183 renderExpression on
181 renderExpression on
184
182
@@ -206,81 +204,110 @@ project
206 . recordToList
204 . recordToList
207 . rmap (Const . renderAliased renderExpression)
205 . rmap (Const . renderAliased renderExpression)
208
206
209−data Tabulation
207+data Clauses params tables = Clauses
208+ { whereClause :: Maybe (Expression params tables 'PGBool)
209+ , limitClause :: Maybe (Expression params '[] 'PGInt8)
210+ , offsetClause :: Maybe (Expression params '[] 'PGInt8)
211+ }
212+
213+instance Monoid (Clauses params tables) where
214+ mempty = Clauses Nothing Nothing Nothing
215+ Clauses wh1 lim1 off1 `mappend` Clauses wh2 lim2 off2 = Clauses
216+ { whereClause = case (wh1,wh2) of
217+ (Nothing,Nothing) -> Nothing
218+ (Just w1,Nothing) -> Just w1
219+ (Nothing,Just w2) -> Just w2
220+ (Just w1,Just w2) -> Just (w1 &&* w2)
221+ , limitClause = case (lim1,lim2) of
222+ (Nothing,Nothing) -> Nothing
223+ (Just l1,Nothing) -> Just l1
224+ (Nothing,Just l2) -> Just l2
225+ (Just l1,Just l2) -> Just (l1 `minB` l2)
226+ , offsetClause = case (off1,off2) of
227+ (Nothing,Nothing) -> Nothing
228+ (Just o1,Nothing) -> Just o1
229+ (Nothing,Just o2) -> Just o2
230+ (Just o1,Just o2) -> Just (o1 + o2)
231+ }
232+
233+data TableExpression
210 (params :: [PGType])
234 (params :: [PGType])
211 (schema :: [(Symbol,[(Symbol,PGType)])])
235 (schema :: [(Symbol,[(Symbol,PGType)])])
212 (tables :: [(Symbol,[(Symbol,PGType)])])
236 (tables :: [(Symbol,[(Symbol,PGType)])])
213− = Tabulation
237+ = TableExpression
214− { fromClause :: TableRef params schema tables
238+ { tableRef :: TableRef params schema tables
215− , whereClause :: Maybe (Expression params tables 'PGBool)
239+ , clauses :: Clauses params tables
216− , limitClause :: Maybe (Expression params '[] 'PGInt8)
217− , offsetClause :: Maybe (Expression params '[] 'PGInt8)
218 }
240 }
219
241
242+tables
220−tabulation :: TableRef params schema tables -> Tabulation params schema tables
243+ :: TableRef params schema tables
221−tabulation tab = Tabulation tab Nothing Nothing Nothing
244+ -> TableExpression params schema tables
245+tables tref = TableExpression tref mempty
222
246
223−renderTabulation :: Tabulation params schema tables -> ByteString
247+renderTabulation :: TableExpression params schema tables -> ByteString
224−renderTabulation (Tabulation fr wh lim off)= mconcat
248+renderTabulation (TableExpression tref (Clauses wh lim off))= mconcat
225− [ renderTableRef fr
249+ [ renderTableRef tref
226 , maybe "" ((" WHERE " <>) . renderExpression) wh
250 , maybe "" ((" WHERE " <>) . renderExpression) wh
227 , maybe "" ((" LIMIT " <>) . renderExpression) lim
251 , maybe "" ((" LIMIT " <>) . renderExpression) lim
228 , maybe "" ((" OFFSET " <>) . renderExpression) off
252 , maybe "" ((" OFFSET " <>) . renderExpression) off
229 ]
253 ]
230
254
231−instance HasTable table schema columns
255+instance (HasTable table schema columns, table ~ table')
232− => IsLabel table (TableRef params schema '[table ::: columns]) where
256+ => IsLabel table (TableRef params schema '[table' ::: columns]) where
233 fromLabel = getTable
257 fromLabel = getTable
234
258
235−instance HasTable table schema columns
259+instance (HasTable table schema columns, table ~ table')
236− => IsLabel table (Tabulation params schema '[table ::: columns]) where
260+ => IsLabel table (TableExpression params schema '[table' ::: columns]) where
237− fromLabel p = Tabulation (getTable p) Nothing Nothing Nothing
261+ fromLabel p = TableExpression (getTable p) mempty
238
262
239where_
263where_
240 :: Expression params tables 'PGBool
264 :: Expression params tables 'PGBool
241− -> Tabulation params schema tables
265+ -> TableExpression params schema tables
242− -> Tabulation params schema tables
266+ -> TableExpression params schema tables
243−where_ condition tables = tables
267+where_ wh (TableExpression tabs clauses1) = TableExpression tabs
244− { whereClause = case whereClause tables of
268+ (clauses1 <> Clauses (Just wh) Nothing Nothing)
245− Nothing -> Just condition
246− Just conditions -> Just (conditions &&* condition)
247− }
248
269
249limit
270limit
250 :: Expression params '[] 'PGInt8
271 :: Expression params '[] 'PGInt8
251− -> Tabulation params schema tables
272+ -> TableExpression params schema tables
252− -> Tabulation params schema tables
273+ -> TableExpression params schema tables
253−limit n tables = tables
274+limit lim (TableExpression tabs clauses1) = TableExpression tabs
254− { limitClause = case limitClause tables of
275+ (clauses1 <> Clauses Nothing (Just lim) Nothing)
255− Nothing -> Just n
256− Just n' -> Just (n' `minB` n)
257− }
258
276
259offset
277offset
260 :: Expression params '[] 'PGInt8
278 :: Expression params '[] 'PGInt8
261− -> Tabulation params schema tables
279+ -> TableExpression params schema tables
262− -> Tabulation params schema tables
280+ -> TableExpression params schema tables
263−offset n tables = tables
281+offset off (TableExpression tabs clauses1) = TableExpression tabs
264− { offsetClause = case offsetClause tables of
282+ (clauses1 <> Clauses Nothing Nothing (Just off))
265− Nothing -> Just n
266− Just n' -> Just (n' + n)
267− }
268
283
269−newtype Selection params schema columns = UnsafeSelection
284+newtype Selection
285+ (params :: [PGType])
286+ (schema :: [(Symbol,[(Symbol,PGType)])])
287+ (columns :: [(Symbol,PGType)])
288+ = UnsafeSelection
270− { renderSelection :: ByteString }
289+ { renderSelection :: ByteString }
271
290
272from
291from
292+ :: TableExpression params schema tables
273− :: Projection params tables columns
293+ -> Projection params tables columns
274− -> Tabulation params schema tables
275 -> Selection params schema columns
294 -> Selection params schema columns
276−from projection tables = UnsafeSelection $
295+from tabs projection = UnsafeSelection $
277− renderProjection projection <> " FROM " <> renderTabulation tables
296+ renderProjection projection <> " FROM " <> renderTabulation tabs
278
297
279−newtype Query params schema0 schema1 columns = UnsafeQuery
298+newtype Query
299+ (params :: [PGType])
300+ (schema0 :: [(Symbol,[(Symbol,PGType)])])
301+ (schema1 :: [(Symbol,[(Symbol,PGType)])])
302+ (columns :: [(Symbol,PGType)])
280− { renderQuery :: ByteString }
303+ = UnsafeQuery { renderQuery :: ByteString }
281
304
282−newtype PreparedQuery params schema0 schema1 columns = UnsafePreparedQuery
305+newtype PreparedQuery
306+ (params :: [PGType])
307+ (schema0 :: [(Symbol,[(Symbol,PGType)])])
308+ (schema1 :: [(Symbol,[(Symbol,PGType)])])
309+ (columns :: [(Symbol,PGType)])
283− { renderPreparedQuery :: ByteString }
310+ = UnsafePreparedQuery { renderPreparedQuery :: ByteString }
284
311
285select
312select
286 :: Selection params schema columns
313 :: Selection params schema columns
@@ -289,18 +316,15 @@ select = UnsafeQuery . ("SELECT " <>) . (<> ";") . renderSelection
289
316
290subselect
317subselect
291 :: Aliased (Selection params schema) table
318 :: Aliased (Selection params schema) table
292− -> Tabulation params schema '[table]
319+ -> TableExpression params schema '[table]
293−subselect selection = Tabulation
320+subselect selection = TableExpression
294− { fromClause = UnsafeTableRef . ("SELECT " <>) $
321+ { tableRef = UnsafeTableRef . ("SELECT " <>) $
295 renderAliased renderSelection selection
322 renderAliased renderSelection selection
296− , whereClause = Nothing
323+ , clauses = mempty
297− , limitClause = Nothing
298− , offsetClause = Nothing
299 }
324 }
300
325
301insertInto
326insertInto
302− :: forall schema columns table params
303− . HasTable table schema columns
327+ :: HasTable table schema columns
304 => Alias table
328 => Alias table
305 -> Rec (Aliased (Expression params '[])) columns
329 -> Rec (Aliased (Expression params '[])) columns
306 -> Query params schema schema '[]
330 -> Query params schema schema '[]
squeel-postgresql/test/Squeel/PostgreSQL/QuerySpec.hs+80−80
@@ -3,14 +3,12 @@
3 , OverloadedLabels
3 , OverloadedLabels
4 , OverloadedStrings
4 , OverloadedStrings
5 , PolyKinds
5 , PolyKinds
6− , TypeApplications
7 , TypeOperators
6 , TypeOperators
8#-}
7#-}
9
8
10module Squeel.PostgreSQL.QuerySpec where
9module Squeel.PostgreSQL.QuerySpec where
11
10
12import Data.Boolean
11import Data.Boolean
13−import Data.ByteString
14import Data.Function
12import Data.Function
15import Data.Vinyl
13import Data.Vinyl
16import Test.Hspec
14import Test.Hspec
@@ -20,53 +18,82 @@ import Squeel.PostgreSQL.Schema
20
18
21spec :: Spec
19spec :: Spec
22spec = do
20spec = do
21+ let
22+ query `shouldRenderAs` str = renderQuery query `shouldBe` str
23 it "correctly renders a simple SELECT query" $ do
23 it "correctly renders a simple SELECT query" $ do
24+ let
25+ statement :: Query '[] Tables Tables SumAndCol1
26+ statement = select $
27+ project ((#col1 + #col2) `As` #sum :& #col1 :& RNil)
24− select (sumAndCol1 `from` (table1 & where_ true))
28+ & from (#table1 & where_ true)
25− `shouldRenderAs`
29+ statement `shouldRenderAs`
26− "SELECT (col1 + col2) AS sum, col1 AS col1 FROM table1 WHERE TRUE;"
30+ "SELECT (col1 + col2) AS sum, col1 AS col1 FROM table1 WHERE TRUE;"
27 it "combines WHEREs using AND" $ do
31 it "combines WHEREs using AND" $ do
32+ let
33+ statement :: Query '[] Tables Tables SumAndCol1
34+ statement = select $
35+ project ((#col1 + #col2) `As` #sum :& #col1 :& RNil)
28− select (sumAndCol1 `from` (table1 & where_ true & where_ false))
36+ & from (#table1 & where_ true & where_ false)
29− `shouldRenderAs`
37+ statement `shouldRenderAs`
30− "SELECT (col1 + col2) AS sum, col1 AS col1 FROM table1 WHERE (TRUE AND FALSE);"
38+ "SELECT (col1 + col2) AS sum, col1 AS col1 FROM table1 WHERE (TRUE AND FALSE);"
31 it "performs sub SELECTs" $ do
39 it "performs sub SELECTs" $ do
40+ let
41+ selection = project ((#col1 + #col2) `As` #sum :& #col1 :& RNil) & from #table1
42+ statement :: Query '[] Tables Tables SumAndCol1
32− select (star `from` subselect ((sumAndCol1 `from` table1) `As` (#sub :: Alias "sub")))
43+ statement = select $ star & from (subselect (selection `As` #sub))
33− `shouldRenderAs`
44+ statement `shouldRenderAs`
34− "SELECT * FROM SELECT (col1 + col2) AS sum, col1 AS col1 FROM table1 AS sub;"
45+ "SELECT * FROM SELECT (col1 + col2) AS sum, col1 AS col1 FROM table1 AS sub;"
35 it "does LIMIT clauses" $ do
46 it "does LIMIT clauses" $ do
47+ let
48+ statement :: Query '[] Tables Tables Columns
36− select (star `from` (table1 & limit 1))
49+ statement = select $ star & from (#table1 & limit 1)
37− `shouldRenderAs`
38− "SELECT * FROM table1 LIMIT 1;"
50+ statement `shouldRenderAs` "SELECT * FROM table1 LIMIT 1;"
39 it "should use the minimum of given LIMITs" $ do
51 it "should use the minimum of given LIMITs" $ do
52+ let
53+ statement :: Query '[] Tables Tables Columns
40− select (star `from` (table1 & limit 1 & limit 2))
54+ statement = select $ star & from (#table1 & limit 1 & limit 2)
41− `shouldRenderAs`
55+ statement `shouldRenderAs`
42− "SELECT * FROM table1 LIMIT CASE WHEN (1 <= 2) THEN 1 ELSE 2 END;"
56+ "SELECT * FROM table1 LIMIT CASE WHEN (1 <= 2) THEN 1 ELSE 2 END;"
43 it "should render parameters using $ signs" $ do
57 it "should render parameters using $ signs" $ do
58+ let
59+ statement :: Query '[ 'PGInt8] Tables Tables Columns
44− select (star `from` (parameterizedTable1 & limit param1))
60+ statement = select $ star & from (#table1 & limit param1)
45− `shouldRenderAs`
46− "SELECT * FROM table1 LIMIT $1;"
61+ statement `shouldRenderAs` "SELECT * FROM table1 LIMIT $1;"
47 it "does OFFSET clauses" $ do
62 it "does OFFSET clauses" $ do
63+ let
64+ statement :: Query '[] Tables Tables Columns
48− select (star `from` (table1 & offset 1))
65+ statement = select $ star & from (#table1 & offset 1)
49− `shouldRenderAs`
50− "SELECT * FROM table1 OFFSET 1;"
66+ statement `shouldRenderAs` "SELECT * FROM table1 OFFSET 1;"
51 it "should use the sum of given OFFSETs" $ do
67 it "should use the sum of given OFFSETs" $ do
68+ let
69+ statement :: Query '[] Tables Tables Columns
52− select (star `from` (table1 & offset 1 & offset 2))
70+ statement = select $ star & from (#table1 & offset 1 & offset 2)
53− `shouldRenderAs`
54− "SELECT * FROM table1 OFFSET (1 + 2);"
71+ statement `shouldRenderAs` "SELECT * FROM table1 OFFSET (1 + 2);"
55 it "correctly render simple INSERTs" $ do
72 it "correctly render simple INSERTs" $ do
56− insertInto @Tables @Columns
73+ let
57− (#table1 :: Alias "table1")
74+ statement :: Query '[] Tables Tables '[]
58− (2 `As` #col1 :& 4 `As` #col2 :& RNil)
75+ statement = insertInto #table1 $ 2 `As` #col1 :& 4 `As` #col2 :& RNil
59− `shouldRenderAs`
76+ statement `shouldRenderAs`
60− "INSERT INTO table1 (col1, col2) VALUES (2, 4);"
77+ "INSERT INTO table1 (col1, col2) VALUES (2, 4);"
61 it "should be safe against SQL injection in literal text" $ do
78 it "should be safe against SQL injection in literal text" $ do
62− insertInto @StudentsTable @StudentsColumns
79+ let
63− (#students :: Alias "students")
80+ statement :: Query '[] StudentsTable StudentsTable '[]
81+ statement = insertInto #students $
64− ("Robert'); DROP TABLE students;" `As` #name :& RNil)
82+ "Robert'); DROP TABLE students;" `As` #name :& RNil
65− `shouldRenderAs`
83+ statement `shouldRenderAs`
66− "INSERT INTO students (name) VALUES (E'Robert''); DROP TABLE students;');"
84+ "INSERT INTO students (name) VALUES (E'Robert''); DROP TABLE students;');"
67 describe "JOINs" $ do
85 describe "JOINs" $ do
86+ let
87+ vals = project $
88+ #orders .&. #orderVal `As` #orderVal
89+ :& #customers .&. #customerVal `As` #customerVal
90+ :& #shippers .&. #shipperVal `As` #shipperVal :& RNil
68 it "should render CROSS JOINs" $ do
91 it "should render CROSS JOINs" $ do
92+ let
93+ statement :: Query '[] JoinTables JoinTables ValueColumns
69− select (vals `from` crossJoins) `shouldRenderAs`
94+ statement = select $ vals & from
95+ (tables (#orders & crossJoin #customers & crossJoin #shippers))
96+ statement `shouldRenderAs`
70 "SELECT\
97 "SELECT\
71 \ orders.orderVal AS orderVal,\
98 \ orders.orderVal AS orderVal,\
72 \ customers.customerVal AS customerVal,\
99 \ customers.customerVal AS customerVal,\
@@ -75,7 +102,17 @@ spec = do
75 \ CROSS JOIN customers\
102 \ CROSS JOIN customers\
76 \ CROSS JOIN shippers;"
103 \ CROSS JOIN shippers;"
77 it "should render INNER JOINs" $ do
104 it "should render INNER JOINs" $ do
105+ let
106+ innerJoins :: TableExpression '[] JoinTables JoinTables
107+ innerJoins = tables $
108+ #orders
109+ & innerJoin #customers
110+ (#orders .&. #customerID ==* #customers .&. #customerID)
111+ & innerJoin #shippers
112+ (#orders .&. #shipperID ==* #shippers .&. #shipperID)
113+ selection :: Query '[] JoinTables JoinTables ValueColumns
78− select (vals `from` innerJoins) `shouldRenderAs`
114+ selection = select $ vals & from innerJoins
115+ selection `shouldRenderAs`
79 "SELECT\
116 "SELECT\
80 \ orders.orderVal AS orderVal,\
117 \ orders.orderVal AS orderVal,\
81 \ customers.customerVal AS customerVal,\
118 \ customers.customerVal AS customerVal,\
@@ -88,31 +125,9 @@ spec = do
88
125
89type Columns = '[ "col1" ::: 'PGInt4, "col2" ::: 'PGInt4]
126type Columns = '[ "col1" ::: 'PGInt4, "col2" ::: 'PGInt4]
90type Tables = '[ "table1" ::: Columns ]
127type Tables = '[ "table1" ::: Columns ]
91−
92−sumAndCol1 :: Projection '[] Tables
93− '["sum" ::: 'PGInt4, "col1" ::: 'PGInt4]
128+type SumAndCol1 = '[ "sum" ::: 'PGInt4, "col1" ::: 'PGInt4]
94−sumAndCol1 = project $
95− (#col1 + #col2) `As` #sum
96− :& #col1 :& RNil
97−
98−literalText :: Projection '[] Tables '["literal" ::: 'PGText]
99−literalText = project $ "\';DROP TABLE table1;" `As` #literal :& RNil
100−
101−table1 :: Tabulation '[] Tables Tables
102−table1 = #table1
103−
104−parameterizedTable1 :: Tabulation '[ 'PGInt8] Tables Tables
105−parameterizedTable1 = #table1
106−
107−shouldRenderAs
108− :: Query params schema1 schema2 columns
109− -> ByteString
110− -> Expectation
111−shouldRenderAs query str = renderQuery query `shouldBe` str
112−
113type StudentsColumns = '["name" ::: 'PGText]
129type StudentsColumns = '["name" ::: 'PGText]
114type StudentsTable = '["students" ::: StudentsColumns]
130type StudentsTable = '["students" ::: StudentsColumns]
115−
116type OrderColumns =
131type OrderColumns =
117 [ "orderID" ::: 'PGInt4
132 [ "orderID" ::: 'PGInt4
118 , "orderVal" ::: 'PGText
133 , "orderVal" ::: 'PGText
@@ -124,27 +139,12 @@ type CustomerColumns =
124type ShipperColumns =
139type ShipperColumns =
125 [ "shipperID" ::: 'PGInt4, "shipperVal" ::: 'PGBool ]
140 [ "shipperID" ::: 'PGInt4, "shipperVal" ::: 'PGBool ]
126type JoinTables =
141type JoinTables =
127− [ "shippers" ::: ShipperColumns
142+ [ "shippers" ::: ShipperColumns
128 , "customers" ::: CustomerColumns
143 , "customers" ::: CustomerColumns
129− , "orders" ::: OrderColumns
144+ , "orders" ::: OrderColumns
130 ]
145 ]
131type ValueColumns =
146type ValueColumns =
132− [ "orderVal" ::: 'PGText
147+ [ "orderVal" ::: 'PGText
133 , "customerVal" ::: 'PGFloat4
148 , "customerVal" ::: 'PGFloat4
134− , "shipperVal" ::: 'PGBool
149+ , "shipperVal" ::: 'PGBool
135 ]
150 ]
136−vals :: Projection '[] JoinTables ValueColumns
137−vals = project $
138− #orders .&. #orderVal `As` #orderVal
139− :& #customers .&. #customerVal `As` #customerVal
140− :& #shippers .&. #shipperVal `As` #shipperVal :& RNil
141−crossJoins :: Tabulation '[] JoinTables JoinTables
142−crossJoins = tabulation $
143− #orders & crossJoin #customers & crossJoin #shippers
144−innerJoins :: Tabulation '[] JoinTables JoinTables
145−innerJoins = tabulation $
146− #orders
147− & innerJoin #customers
148− (#orders .&. #customerID ==* #customers .&. #customerID)
149− & innerJoin #shippers
150− (#orders .&. #shipperID ==* #shippers .&. #shipperID)