diff --git a/esqueleto.cabal b/esqueleto.cabal
--- a/esqueleto.cabal
+++ b/esqueleto.cabal
@@ -1,5 +1,5 @@
 name:                esqueleto
-version:             2.1.3
+version:             2.2
 synopsis:            Type-safe EDSL for SQL queries on persistent backends.
 homepage:            https://github.com/prowdsponsor/esqueleto
 license:             BSD3
diff --git a/src/Database/Esqueleto/Internal/Language.hs b/src/Database/Esqueleto/Internal/Language.hs
--- a/src/Database/Esqueleto/Internal/Language.hs
+++ b/src/Database/Esqueleto/Internal/Language.hs
@@ -390,7 +390,8 @@
 infixr 5 ++.
 infix  4 ==., >=., >., <=., <., !=.
 infixr 3 &&., =., +=., -=., *=., /=.
-infixr 2 ||., `InnerJoin`, `CrossJoin`, `LeftOuterJoin`, `RightOuterJoin`, `FullOuterJoin`, `like`
+infixr 2 ||., `like`
+infixl 2 `InnerJoin`, `CrossJoin`, `LeftOuterJoin`, `RightOuterJoin`, `FullOuterJoin`
 
 -- | Syntax sugar for 'case_'.
 --
@@ -550,6 +551,7 @@
   | LeftOuterJoinKind  -- ^ @LEFT OUTER JOIN@
   | RightOuterJoinKind -- ^ @RIGHT OUTER JOIN@
   | FullOuterJoinKind  -- ^ @FULL OUTER JOIN@
+    deriving Eq
 
 
 -- | (Internal) Functions that operate on types (that should be)
diff --git a/src/Database/Esqueleto/Internal/Sql.hs b/src/Database/Esqueleto/Internal/Sql.hs
--- a/src/Database/Esqueleto/Internal/Sql.hs
+++ b/src/Database/Esqueleto/Internal/Sql.hs
@@ -152,7 +152,9 @@
           matchR = (\r' -> FromJoin l k r' onClause) <$> tryMatch expr r
           matchL = (\l' -> FromJoin l' k r onClause) <$> tryMatch expr l
           matchC = case onClause of
-                     Nothing -> return (FromJoin l k r (Just expr))
+                     Nothing | k /= CrossJoinKind
+                               -> return (FromJoin l k r (Just expr))
+                             | otherwise -> mzero
                      Just _  -> mzero
     tryMatch _ _ = mzero
 
@@ -934,19 +936,20 @@
   where
     ret = case collectOnClauses fs of
             Left expr -> throw $ mkExc expr
-            Right fs' -> keyword $ uncommas' (map (mk Never mempty) fs')
+            Right fs' -> keyword $ uncommas' (map (mk Never) fs')
     keyword = case mode of
                 UPDATE -> id
                 _      -> first ("\nFROM " <>)
 
-    mk _     onClause (FromStart i def) = base i def <> onClause
-    mk paren onClause (FromJoin lhs kind rhs monClause) =
+    mk _     (FromStart i def) = base i def
+    mk paren (FromJoin lhs kind rhs monClause) =
       first (parensM paren) $
-      mconcat [ mk Parens onClause lhs
+      mconcat [ mk Never lhs
               , (fromKind kind, mempty)
-              , mk Never (maybe mempty makeOnClause monClause) rhs
+              , mk Parens rhs
+              , maybe mempty makeOnClause monClause
               ]
-    mk _ _ (OnClause _) = error "Esqueleto/Sql/makeFrom: never here (is collectOnClauses working?)"
+    mk _ (OnClause _) = error "Esqueleto/Sql/makeFrom: never here (is collectOnClauses working?)"
 
     base ident@(I identText) def =
       let db@(DBName dbText) = entityDB def
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -51,6 +51,12 @@
 
 -- Test schema
 share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
+  Foo
+    name Int
+    Primary name
+  Bar
+    quux FooId
+
   Person
     name String
     age Int Maybe
@@ -439,6 +445,19 @@
             retArt `shouldBe` article
             retTag `shouldBe` tag
 
+      it "respects the associativity of joins" $
+        run $ do
+            insert' p1
+            ps <- select . from $
+                      \((p :: SqlExpr (Entity Person))
+                       `LeftOuterJoin`
+                        (( q :: SqlExpr (Entity Person))
+                         `InnerJoin` (r :: SqlExpr (Entity Person)))) -> do
+                on (val False) -- Inner join is empty
+                on (val True)
+                return p
+            liftIO $ (entityVal <$> ps) `shouldBe` [p1]
+
     describe "select/where_" $ do
       it "works for a simple example with (==.)" $
         run $ do
@@ -823,10 +842,11 @@
 #if defined(WITH_POSTGRESQL) || defined(WITH_MYSQL)
       it "works on PostgreSQL and MySQL with <2 arguments" $
         run $ do
-          _ :: [Value (Maybe Int)] <- select $
-               from $ \p -> do
-               return (coalesce [p ^. PersonAge])
-          return True
+          _ :: [Value (Maybe Int)] <-
+            select $
+            from $ \p -> do
+            return (coalesce [p ^. PersonAge])
+          return ()
 #else
       it "throws an exception on SQLite with <2 arguments" $
         run (select $
@@ -1183,9 +1203,18 @@
   :: (forall m. RunDbMonad m
   => SqlPersistT (R.ResourceT m) ())
 cleanDB = do
-  delete $ from $ \(blogpost :: SqlExpr (Entity BlogPost))-> return ()
-  delete $ from $ \(follow   :: SqlExpr (Entity Follow))  -> return ()
-  delete $ from $ \(person   :: SqlExpr (Entity Person))  -> return ()
+  delete $ from $ \(_ :: SqlExpr (Entity BlogPost))   -> return ()
+  delete $ from $ \(_ :: SqlExpr (Entity Follow))     -> return ()
+  delete $ from $ \(_ :: SqlExpr (Entity Person))     -> return ()
+
+  delete $ from $ \(_ :: SqlExpr (Entity ArticleTag)) -> return ()
+  delete $ from $ \(_ :: SqlExpr (Entity Article))    -> return ()
+  delete $ from $ \(_ :: SqlExpr (Entity Article2))   -> return ()
+  delete $ from $ \(_ :: SqlExpr (Entity Tag))        -> return ()
+  delete $ from $ \(_ :: SqlExpr (Entity Frontcover)) -> return ()
+
+  delete $ from $ \(_ :: SqlExpr (Entity Circle))     -> return ()
+  delete $ from $ \(_ :: SqlExpr (Entity Point))      -> return ()
 #endif
 
 
@@ -1219,7 +1248,7 @@
 #endif
   runSqlConn .
 #if defined (WITH_POSTGRESQL) || defined (WITH_MYSQL)
-  (runMigrationSilent migrateAll >>) $ (cleanDB >> act)
+  (runMigration migrateAll >>) $ (cleanDB >> act)
 #else
   (runMigrationSilent migrateAll >>) $ act
 #endif
