From 0a2cbdb3d10dc887332c9a3f2501ae366bb52131 Mon Sep 17 00:00:00 2001 From: Marc Scholten Date: Mon, 27 Sep 2021 11:31:10 +0200 Subject: [PATCH] Use explicit list of columns in `SELECT` queries instead of using `SELECT *` This avoids issues when the database schema is being migrated --- IHP/AuthSupport/Controller/Sessions.hs | 2 +- IHP/ModelSupport.hs | 10 +-- IHP/QueryBuilder.hs | 25 +++++--- Test/QueryBuilderSpec.hs | 89 +++++++++++++++----------- Test/SchemaCompilerSpec.hs | 22 ++++--- 5 files changed, 86 insertions(+), 62 deletions(-) diff --git a/IHP/AuthSupport/Controller/Sessions.hs b/IHP/AuthSupport/Controller/Sessions.hs index 027d04b47..ba09fdc78 100644 --- a/IHP/AuthSupport/Controller/Sessions.hs +++ b/IHP/AuthSupport/Controller/Sessions.hs @@ -176,6 +176,6 @@ class ( Typeable record -- -- > usersQueryBuilder = query @User |> filterWhere (#isGuest, False) -- - usersQueryBuilder :: (GetModelByTableName (GetTableName record) ~ record) => QueryBuilder (GetTableName record) + usersQueryBuilder :: (GetModelByTableName (GetTableName record) ~ record, Table record) => QueryBuilder (GetTableName record) usersQueryBuilder = query @record {-# INLINE usersQueryBuilder #-} \ No newline at end of file diff --git a/IHP/ModelSupport.hs b/IHP/ModelSupport.hs index bc65d9c15..9da4bcc66 100644 --- a/IHP/ModelSupport.hs +++ b/IHP/ModelSupport.hs @@ -447,8 +447,8 @@ class -- tableName :: Text - default tableName :: forall model. (KnownSymbol (GetTableName model)) => Text - tableName = symbolToText @(GetTableName model) + default tableName :: forall model. (KnownSymbol (GetTableName record)) => Text + tableName = symbolToText @(GetTableName record) {-# INLINE tableName #-} -- | Returns the table name of a given model as a bytestring. @@ -459,8 +459,8 @@ class -- "users" -- tableNameByteString :: ByteString - default tableNameByteString :: forall model. (KnownSymbol (GetTableName model)) => ByteString - tableNameByteString = symbolToByteString @(GetTableName model) + default tableNameByteString :: forall model. (KnownSymbol (GetTableName record)) => ByteString + tableNameByteString = symbolToByteString @(GetTableName record) {-# INLINE tableNameByteString #-} -- | Returns the list of column names for a given model @@ -470,7 +470,7 @@ class -- >>> columnNames @User -- ["id", "email", "created_at"] -- - columnNames :: [Text] + columnNames :: [ByteString] -- | Returns WHERE conditions to match an entity by it's primary key -- diff --git a/IHP/QueryBuilder.hs b/IHP/QueryBuilder.hs index f96e8758d..febbdadf5 100644 --- a/IHP/QueryBuilder.hs +++ b/IHP/QueryBuilder.hs @@ -83,9 +83,9 @@ instance {-# OVERLAPPABLE #-} DefaultScope table where {-# INLINE defaultScope #-} defaultScope queryBuilder = queryBuilder -instance Default (QueryBuilder table) where +instance Table (GetModelByTableName table) => Default (QueryBuilder table) where {-# INLINE def #-} - def = NewQueryBuilder + def = NewQueryBuilder { columns = columnNames @(GetModelByTableName table) } data MatchSensitivity = CaseSensitive | CaseInsensitive deriving (Show, Eq) @@ -193,7 +193,7 @@ instance (KnownSymbol foreignTable, foreignModel ~ GetModelByTableName foreignTa data QueryBuilder (table :: Symbol) = - NewQueryBuilder + NewQueryBuilder { columns :: ![ByteString] } | DistinctQueryBuilder { queryBuilder :: !(QueryBuilder table) } | DistinctOnQueryBuilder { queryBuilder :: !(QueryBuilder table), distinctOnColumn :: !ByteString } | FilterByQueryBuilder { queryBuilder :: !(QueryBuilder table), queryFilter :: !(ByteString, FilterOperator, Action), applyLeft :: !(Maybe ByteString), applyRight :: !(Maybe ByteString) } @@ -224,6 +224,7 @@ data SQLQuery = SQLQuery , orderByClause :: ![OrderByClause] , limitClause :: !(Maybe ByteString) , offsetClause :: !(Maybe ByteString) + , columns :: ![ByteString] } deriving (Show, Eq) -- | Needed for the 'Eq QueryBuilder' instance @@ -276,15 +277,15 @@ instance SetField "offsetClause" SQLQuery (Maybe ByteString) where setField valu -- > query @User -- > |> filterWhere (#active, True) -- > |> fetch -query :: forall model table. (table ~ GetTableName model) => DefaultScope table => QueryBuilder table -query = (defaultScope @table) NewQueryBuilder +query :: forall model table. (table ~ GetTableName model, Table model) => DefaultScope table => QueryBuilder table +query = (defaultScope @table) NewQueryBuilder { columns = columnNames @model } {-# INLINE query #-} {-# INLINE buildQuery #-} buildQuery :: forall table queryBuilderProvider joinRegister. (KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> SQLQuery buildQuery queryBuilderProvider = buildQueryHelper $ getQueryBuilder queryBuilderProvider where - buildQueryHelper NewQueryBuilder = + buildQueryHelper NewQueryBuilder { columns } = let tableName = symbolToByteString @table in SQLQuery { queryIndex = trace (Prelude.show $ getQueryIndex queryBuilderProvider) getQueryIndex queryBuilderProvider @@ -296,6 +297,7 @@ buildQuery queryBuilderProvider = buildQueryHelper $ getQueryBuilder queryBuilde , orderByClause = [] , limitClause = Nothing , offsetClause = Nothing + , columns } buildQueryHelper DistinctQueryBuilder { queryBuilder } = queryBuilder |> buildQueryHelper @@ -368,7 +370,7 @@ toSQL queryBuilderProvider = toSQL' (buildQuery queryBuilderProvider) {-# INLINE toSQL #-} toSQL' :: SQLQuery -> (ByteString, [Action]) -toSQL' sqlQuery@SQLQuery { queryIndex, selectFrom, distinctClause, distinctOnClause, orderByClause, limitClause, offsetClause } = +toSQL' sqlQuery@SQLQuery { queryIndex, selectFrom, distinctClause, distinctOnClause, orderByClause, limitClause, offsetClause, columns } = (DeepSeq.force theQuery, theParams) where !theQuery = @@ -388,8 +390,13 @@ toSQL' sqlQuery@SQLQuery { queryIndex, selectFrom, distinctClause, distinctOnCla ] selectors :: ByteString - selectors = ByteString.intercalate ", " $ catMaybes [queryIndex , Just (selectFrom <> ".*")] - + selectors = ByteString.intercalate ", " $ (catMaybes [queryIndex]) <> selectFromWithColumns + where + -- Generates a string like: `posts.id, posts.title, posts.body` + selectFromWithColumns :: [ByteString] + selectFromWithColumns = + columns + |> map (\column -> selectFrom <> "." <> column) fromClause :: ByteString fromClause = selectFrom diff --git a/Test/QueryBuilderSpec.hs b/Test/QueryBuilderSpec.hs index c444cb0a2..dee085e0a 100644 --- a/Test/QueryBuilderSpec.hs +++ b/Test/QueryBuilderSpec.hs @@ -25,6 +25,9 @@ data Post = Post type instance GetTableName Post = "posts" type instance GetModelByTableName "posts" = Post +instance Table Post where + columnNames = ["id", "title", "external_url", "created_at", "public", "created_by", "category_id"] + data Tag = Tag { id :: UUID , tagText :: Text @@ -33,6 +36,9 @@ data Tag = Tag type instance GetTableName Tag = "tags" type instance GetModelByTableName "tags" = Tag +instance Table Tag where + columnNames = ["id", "tag_text"] + data Tagging = Tagging { id :: UUID , postId :: UUID @@ -43,6 +49,9 @@ data Tagging = Tagging type instance GetTableName Tagging = "taggings" type instance GetModelByTableName "taggings" = Tagging +instance Table Tagging where + columnNames = ["id", "post_id", "tag_id"] + data User = User { id :: UUID, name :: Text @@ -51,6 +60,9 @@ data User = User type instance GetTableName User = "users" type instance GetModelByTableName "users" = User +instance Table User where + columnNames = ["id", "name"] + data FavoriteTitle = FavoriteTitle { title :: Text, @@ -60,39 +72,42 @@ data FavoriteTitle = FavoriteTitle type instance GetTableName FavoriteTitle = "favorite_title" type instance GetModelByTableName "favorite_title" = FavoriteTitle +instance Table FavoriteTitle where + columnNames = ["title", "likes"] + tests = do describe "QueryBuilder" do describe "query" do it "should provide a simple sql query" do let theQuery = query @Post - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts", []) describe "filterWhere" do it "should produce a SQL with a WHERE condition" do let theQuery = query @Post |> filterWhere (#title, "Test" :: Text) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.title = ?", [Escape "Test"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.title = ?", [Escape "Test"]) it "should use a IS operator for checking null" do let theQuery = query @Post |> filterWhere (#externalUrl, Nothing) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.external_url IS ?", [Plain "null"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.external_url IS ?", [Plain "null"]) describe "filterWhereNot" do it "should produce a SQL with a WHERE NOT condition" do let theQuery = query @Post |> filterWhereNot (#title, "Test" :: Text) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.title != ?", [Escape "Test"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.title != ?", [Escape "Test"]) it "should use a IS NOT operator for checking null" do let theQuery = query @Post |> filterWhereNot (#externalUrl, Nothing) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.external_url IS NOT ?", [Plain "null"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.external_url IS NOT ?", [Plain "null"]) describe "filterWhereIn" do it "should produce a SQL with a WHERE condition" do @@ -100,7 +115,7 @@ tests = do let theQuery = query @Post |> filterWhereIn (#title, theValues) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.title IN ?", [Many [Plain "(", Escape "first", Plain ",", Escape "second", Plain ")"]]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.title IN ?", [Many [Plain "(", Escape "first", Plain ",", Escape "second", Plain ")"]]) describe "with Maybe / NULL values" do it "should handle [Just .., Nothing]" do @@ -108,21 +123,21 @@ tests = do let theQuery = query @Post |> filterWhereIn (#categoryId, theValues) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE (posts.category_id IN ?) OR (posts.category_id IS ?)", [Many [Plain "(", Plain "'44dcf2cf-a79d-4caf-a2ea-427838ba3574'", Plain ")"], Plain "null"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE (posts.category_id IN ?) OR (posts.category_id IS ?)", [Many [Plain "(", Plain "'44dcf2cf-a79d-4caf-a2ea-427838ba3574'", Plain ")"], Plain "null"]) it "should handle [Just ..]" do let theValues :: [Maybe UUID] = ["44dcf2cf-a79d-4caf-a2ea-427838ba3574"] let theQuery = query @Post |> filterWhereIn (#categoryId, theValues) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.category_id IN ?", [Many [Plain "(", Plain "'44dcf2cf-a79d-4caf-a2ea-427838ba3574'", Plain ")"]]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.category_id IN ?", [Many [Plain "(", Plain "'44dcf2cf-a79d-4caf-a2ea-427838ba3574'", Plain ")"]]) it "should handle [Nothing]" do let theValues :: [Maybe UUID] = [Nothing] let theQuery = query @Post |> filterWhereIn (#categoryId, theValues) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.category_id IS ?", [Plain "null"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.category_id IS ?", [Plain "null"]) describe "filterWhereInJoinedTable" do @@ -132,7 +147,7 @@ tests = do |> innerJoin @Post (#name, #title) |> filterWhereInJoinedTable @Post (#title, theValues) - (toSQL theQuery) `shouldBe` ("SELECT users.* FROM users INNER JOIN posts ON users.name = posts.title WHERE posts.title IN ?", [Many [Plain "(", Escape "first", Plain ",", Escape "second", Plain ")"]]) + (toSQL theQuery) `shouldBe` ("SELECT users.id, users.name FROM users INNER JOIN posts ON users.name = posts.title WHERE posts.title IN ?", [Many [Plain "(", Escape "first", Plain ",", Escape "second", Plain ")"]]) describe "filterWhereNotIn" do @@ -141,14 +156,14 @@ tests = do let theQuery = query @Post |> filterWhereNotIn (#title, theValues) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.title NOT IN ?", [Many [Plain "(", Escape "first", Plain ",", Escape "second", Plain ")"]]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.title NOT IN ?", [Many [Plain "(", Escape "first", Plain ",", Escape "second", Plain ")"]]) it "ignore an empty value list as this causes the query to always return nothing" do let theValues :: [Text] = [] let theQuery = query @Post |> filterWhereNotIn (#title, theValues) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts", []) describe "with Maybe / NULL values" do it "should handle [Just .., Nothing]" do @@ -156,21 +171,21 @@ tests = do let theQuery = query @Post |> filterWhereNotIn (#categoryId, theValues) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE (posts.category_id NOT IN ?) AND (posts.category_id IS NOT ?)", [Many [Plain "(", Plain "'44dcf2cf-a79d-4caf-a2ea-427838ba3574'", Plain ")"], Plain "null"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE (posts.category_id NOT IN ?) AND (posts.category_id IS NOT ?)", [Many [Plain "(", Plain "'44dcf2cf-a79d-4caf-a2ea-427838ba3574'", Plain ")"], Plain "null"]) it "should handle [Just ..]" do let theValues :: [Maybe UUID] = ["44dcf2cf-a79d-4caf-a2ea-427838ba3574"] let theQuery = query @Post |> filterWhereNotIn (#categoryId, theValues) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.category_id NOT IN ?", [Many [Plain "(", Plain "'44dcf2cf-a79d-4caf-a2ea-427838ba3574'", Plain ")"]]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.category_id NOT IN ?", [Many [Plain "(", Plain "'44dcf2cf-a79d-4caf-a2ea-427838ba3574'", Plain ")"]]) it "should handle [Nothing]" do let theValues :: [Maybe UUID] = [Nothing] let theQuery = query @Post |> filterWhereNotIn (#categoryId, theValues) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.category_id IS NOT ?", [Plain "null"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.category_id IS NOT ?", [Plain "null"]) describe "filterWhereNotInJoinedTable" do it "should produce a SQL with a WHERE condition" do @@ -179,7 +194,7 @@ tests = do |> innerJoin @Post (#name, #title) |> filterWhereNotInJoinedTable @Post (#title, theValues) - (toSQL theQuery) `shouldBe` ("SELECT users.* FROM users INNER JOIN posts ON users.name = posts.title WHERE posts.title NOT IN ?", [Many [Plain "(", Escape "first", Plain ",", Escape "second", Plain ")"]]) + (toSQL theQuery) `shouldBe` ("SELECT users.id, users.name FROM users INNER JOIN posts ON users.name = posts.title WHERE posts.title NOT IN ?", [Many [Plain "(", Escape "first", Plain ",", Escape "second", Plain ")"]]) it "ignore an empty value list as this causes the query to always return nothing" do let theValues :: [Text] = [] @@ -187,14 +202,14 @@ tests = do |> innerJoin @Post (#name, #title) |> filterWhereNotInJoinedTable @Post (#title, theValues) - (toSQL theQuery) `shouldBe` ("SELECT users.* FROM users INNER JOIN posts ON users.name = posts.title", []) + (toSQL theQuery) `shouldBe` ("SELECT users.id, users.name FROM users INNER JOIN posts ON users.name = posts.title", []) describe "filterWhereILike" do it "should produce a SQL with a WHERE condition" do let searchTerm = "good" let theQuery = query @Post |> filterWhereILike (#title, "%" <> searchTerm <> "%") - (toSQL theQuery `shouldBe` ("SELECT posts.* FROM posts WHERE posts.title ILIKE ?", [Escape "%good%"])) + (toSQL theQuery `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.title ILIKE ?", [Escape "%good%"])) describe "filterWhereILikeJoinedTable" do it "should produce a SQL with a WHERE condition" do @@ -202,7 +217,7 @@ tests = do let theQuery = query @Post |> innerJoin @User (#createdBy, #id) |> filterWhereILikeJoinedTable @User (#name, "%" <> searchTerm <> "%") - (toSQL theQuery `shouldBe` ("SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name ILIKE ?", [Escape "%louis%"])) + (toSQL theQuery `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name ILIKE ?", [Escape "%louis%"])) describe "filterWhereSql" do it "should produce a SQL with a WHERE condition" do @@ -210,14 +225,14 @@ tests = do let theQuery = query @Post |> filterWhereSql (#createdAt, "< current_timestamp - interval '1 day'") - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE posts.created_at ?", [Plain "< current_timestamp - interval '1 day'"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE posts.created_at ?", [Plain "< current_timestamp - interval '1 day'"]) describe "filterWhereCaseInsensitive" do it "should produce a SQL with a WHERE LOWER() condition" do let theQuery = query @Post |> filterWhereCaseInsensitive (#title, "Test" :: Text) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE LOWER(posts.title) = LOWER(?)", [Escape "Test"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE LOWER(posts.title) = LOWER(?)", [Escape "Test"]) describe "innerJoin" do it "should provide an inner join sql query" do @@ -225,7 +240,7 @@ tests = do |> innerJoin @User (#createdBy, #id) |> innerJoin @FavoriteTitle (#title, #title) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title", []) describe "innerJoinThirdTable" do @@ -235,7 +250,7 @@ tests = do |> innerJoin @FavoriteTitle (#title, #title) |> innerJoinThirdTable @User @FavoriteTitle (#name, #title) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title INNER JOIN users ON favorite_title.title = users.name", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title INNER JOIN users ON favorite_title.title = users.name", []) describe "filterWhereJoinedTable" do it "should provide an inner join sql query" do @@ -244,7 +259,7 @@ tests = do |> innerJoin @FavoriteTitle (#title, #title) |> filterWhereJoinedTable @User (#name, "Tom" :: Text) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title WHERE users.name = ?", [Escape "Tom"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title WHERE users.name = ?", [Escape "Tom"]) describe "filterWhereNotJoinedTable" do it "should provide an inner join sql query" do @@ -253,7 +268,7 @@ tests = do |> innerJoin @FavoriteTitle (#title, #title) |> filterWhereNotJoinedTable @User (#name, "Tom" :: Text) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title WHERE users.name != ?", [Escape "Tom"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title WHERE users.name != ?", [Escape "Tom"]) describe "labelResults" do @@ -262,7 +277,7 @@ tests = do |> innerJoin @Tagging (#id, #tagId) |> innerJoinThirdTable @Post @Tagging (#id, #postId) |> labelResults @Post #id - (toSQL theQuery) `shouldBe` ("SELECT posts.id, tags.* FROM tags INNER JOIN taggings ON tags.id = taggings.tag_id INNER JOIN posts ON taggings.post_id = posts.id", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, tags.id, tags.tag_text FROM tags INNER JOIN taggings ON tags.id = taggings.tag_id INNER JOIN posts ON taggings.post_id = posts.id", []) @@ -272,42 +287,42 @@ tests = do let theQuery = query @Post |> orderByAsc #createdAt - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts ORDER BY posts.created_at", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts ORDER BY posts.created_at", []) it "should accumulate multiple ORDER BY's" do let theQuery = query @Post |> orderByAsc #createdAt |> orderByAsc #title - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts ORDER BY posts.created_at,posts.title", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts ORDER BY posts.created_at,posts.title", []) describe "orderByDesc" do it "should add a ORDER BY DESC" do let theQuery = query @Post |> orderByDesc #createdAt - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts ORDER BY posts.created_at DESC", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts ORDER BY posts.created_at DESC", []) it "should accumulate multiple ORDER BY's" do let theQuery = query @Post |> orderByDesc #createdAt |> orderByDesc #title - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts ORDER BY posts.created_at DESC,posts.title DESC", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts ORDER BY posts.created_at DESC,posts.title DESC", []) describe "limit" do it "should add a LIMIT" do let theQuery = query @Post |> limit 1337 - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts LIMIT 1337", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts LIMIT 1337", []) describe "offset" do it "should add a OFFSET" do let theQuery = query @Post |> offset 1337 - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts OFFSET 1337", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts OFFSET 1337", []) describe "queryOr" do it "should merge two conditions" do @@ -317,21 +332,21 @@ tests = do (filterWhere (#public, True)) - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts WHERE (posts.created_by = ?) OR (posts.public = ?)", [Plain "'fe41a985-36a3-4f14-b13c-c166977dc7e8'", Plain "true"]) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE (posts.created_by = ?) OR (posts.public = ?)", [Plain "'fe41a985-36a3-4f14-b13c-c166977dc7e8'", Plain "true"]) describe "distinct" do it "should add a DISTINCT" do let theQuery = query @Post |> distinct - (toSQL theQuery) `shouldBe` ("SELECT DISTINCT posts.* FROM posts", []) + (toSQL theQuery) `shouldBe` ("SELECT DISTINCT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts", []) describe "distinctOn" do it "should add a DISTINCT ON (..)" do let theQuery = query @Post |> distinctOn #title - (toSQL theQuery) `shouldBe` ("SELECT DISTINCT ON (posts.title) posts.* FROM posts", []) + (toSQL theQuery) `shouldBe` ("SELECT DISTINCT ON (posts.title) posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts", []) describe "Complex Queries" do it "should allow a query with limit and offset" do @@ -339,7 +354,7 @@ tests = do |> offset 20 |> limit 50 - (toSQL theQuery) `shouldBe` ("SELECT posts.* FROM posts LIMIT 50 OFFSET 20", []) + (toSQL theQuery) `shouldBe` ("SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts LIMIT 50 OFFSET 20", []) it "should work with multiple complex conditions" do let theQuery = query @Post @@ -355,7 +370,7 @@ tests = do |> limit 10 (toSQL theQuery) `shouldBe` ( - "SELECT posts.* FROM posts WHERE (((posts.title = ?) AND (posts.public = ?)) AND (posts.external_url IS ?)) OR (posts.created_by = ?) ORDER BY posts.created_at,posts.title LIMIT 10", + "SELECT posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id FROM posts WHERE (((posts.title = ?) AND (posts.public = ?)) AND (posts.external_url IS ?)) OR (posts.created_by = ?) ORDER BY posts.created_at,posts.title LIMIT 10", [ Escape "test" , Plain "true" , Plain "null" diff --git a/Test/SchemaCompilerSpec.hs b/Test/SchemaCompilerSpec.hs index 7b163c9e8..4cce9d866 100644 --- a/Test/SchemaCompilerSpec.hs +++ b/Test/SchemaCompilerSpec.hs @@ -194,11 +194,6 @@ tests = do builder |> QueryBuilder.filterWhere (#id, id) {-# INLINE filterWhereId #-} - - instance () => PrimaryKeyCondition (User' ) where - primaryKeyCondition User { id } = [("id", toField id)] - {-# INLINABLE primaryKeyCondition #-} - instance CanCreate User where create :: (?modelContext :: ModelContext) => User -> IO User create model = do @@ -215,6 +210,12 @@ tests = do {-# INLINE newRecord #-} newRecord = User def def 0.17 def instance Default (Id' "users") where def = Id def + instance () => Table (User' ) where + tableName = "users" + tableNameByteString = "users" + columnNames = ["id","ids","electricity_unit_price"] + primaryKeyCondition User { id } = [("id", toField id)] + {-# INLINABLE primaryKeyCondition #-} |] it "should deal with integer default values for double columns" do let statement = StatementCreateTable CreateTable @@ -252,11 +253,6 @@ tests = do builder |> QueryBuilder.filterWhere (#id, id) {-# INLINE filterWhereId #-} - - instance () => PrimaryKeyCondition (User' ) where - primaryKeyCondition User { id } = [("id", toField id)] - {-# INLINABLE primaryKeyCondition #-} - instance CanCreate User where create :: (?modelContext :: ModelContext) => User -> IO User create model = do @@ -273,6 +269,12 @@ tests = do {-# INLINE newRecord #-} newRecord = User def def 0 def instance Default (Id' "users") where def = Id def + instance () => Table (User' ) where + tableName = "users" + tableNameByteString = "users" + columnNames = ["id","ids","electricity_unit_price"] + primaryKeyCondition User { id } = [("id", toField id)] + {-# INLINABLE primaryKeyCondition #-} |] getInstanceDecl :: Text -> Text -> Text