packages feed

selda 0.2.0.0 → 0.3.0.0

raw patch · 32 files changed

+1621/−1632 lines, 32 filesdep ~base

Dependency ranges changed: base

Files

ChangeLog.md view
@@ -1,6 +1,21 @@ # Revision history for Selda  +## 0.3.0.0 -- 2018-08-05++* Support for Stack and GHC 8.4.+* Precedence fix for selector index (!) operator.+* Accept INT and SMALLINT columns in user-created PostgreSQL tables.+* Add combinator for turning off foreign key checking.+* Rename unsafeRowId/unsafeId to toRowId/rowId.+* Add typed row identifiers.+* More generic type for sum_.+* Table validation against current database.+* Basic migration support.+* Basic index support.+* Remove ad hoc tables; only generic tables from now on.++ ## 0.2.0.0 -- 2018-04-02  * Support custom column names for generic tables.
LICENSE view
@@ -1,20 +1,21 @@-Copyright (c) 2017 Anton Ekblad+MIT License -Permission is hereby granted, free of charge, to any person obtaining-a copy of this software and associated documentation files (the-"Software"), to deal in the Software without restriction, including-without limitation the rights to use, copy, modify, merge, publish,-distribute, sublicense, and/or sell copies of the Software, and to-permit persons to whom the Software is furnished to do so, subject to-the following conditions:+Copyright (c) 2017-2018 Anton Ekblad -The above copyright notice and this permission notice shall be included-in all copies or substantial portions of the Software.+Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.+The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
README.md view
@@ -42,7 +42,7 @@     $ cabal update     $ cabal install selda selda-sqlite selda-postgresql -Then, read the tutorial.+Then, read [the tutorial](https://selda.link/tutorial). The [API documentation](http://hackage.haskell.org/package/selda) will probably also come in handy. @@ -50,675 +50,11 @@ Requirements ============ -Selda requires GHC 7.10+, as well as SQLite 3.7.11+ or PostgreSQL 9+.+Selda requires GHC 7.10+, as well as SQLite 3.7.11+ or PostgreSQL 9.6+. To build the SQLite backend, you need a C compiler installed. To build the PostgreSQL backend, you need the `libpq` development libraries installed (`libpq-dev` on Debian-based Linux distributions). --A brief tutorial-================--Defining a schema--------------------To work productively with Selda, you will need to enable the `TypeOperators` and-`OverloadedStrings` extensions.--Table schemas are defined as the product of one or more columns, stitched-together using the `:*:` operator.-A table is parameterized over the types of its columns, with the column types-also separated by the `:*:` operator. This, by the way, is why you need-`TypeOperators`.--```-people :: Table (Text :*: Int :*: Maybe Text)-people = table "people" $ primary "name" :*: required "age" :*: optional "pet"--addresses :: Table (Text :*: Text)-addresses = table "addresses" $ required "name" :*: required "city"-```--Columns may be either `required` or `optional`.-Although the SQL standard supports nullable primary keys, Selda primary keys-are always required.---Running queries------------------Selda operations are run in the `SeldaT` monad transformer, which can be layered-on top of any `MonadIO`. Throughout this tutorial, we will simply use the Selda-monad `SeldaM`, which is just a synonym for `SeldaT IO`.-`SeldaT` is entered using a backend-specific `withX` function. For instance,-the SQLite backend uses the `withSQLite` function:--```-main :: IO ()-main = withSQLite "my_database.sqlite" $ do-  people <- getAllPeople-  liftIO (print people)--getAllPeople :: SeldaM [Text :*: Int :*: Maybe Text]-getAllPeople = query (select people)-```--This will open the `my_database.sqlite` database for the duration of the-computation. If the computation terminates normally, or if it raises an-exception, the database is automatically closed.--Note the somewhat weird return type of `getAllPeople`. In Selda, queries are-represented using *inductive tuples*: a list of values, separated-by the `:*:` operator, but where each element can have a different type.-You can think of them as tuples with a slightly different syntax.-In this example, `getAllPeople` having a return type of-`[Text :*: Int :*: Maybe Text]` means that it returns a list of "3-tuples",-where the three elements have the types `Text`, `Int` and `Maybe Text`-respectively.--You can pattern match on these values as you would on normal tuples:--```-firstOfThree :: (a :*: b :*: c) -> a-firstOfThree (a :*: b :*: c) = a-```--Since inductive tuples are inductively defined, you may also choose to pattern-match on just the first few elements:--```-firstOfN :: (a :*: rest) -> a-firstOfN (a :*: _) = a-```--Throughout the rest of this tutorial, we will simply use inductive tuples as if-they were "normal" tuples.---Creating and deleting databases----------------------------------You can use a table definition to create the corresponding table in your-database backend, as well as delete it.--```-setup :: SeldaM ()-setup = do-  createTable people-  createTable addresses--teardown :: SeldaM ()-teardown = do-  tryDropTable people-  tryDropTable addresses-```--Both creating and deleting tables comes in two variants: the `try` version-which is a silent no-op when attempting to create a table that already exists-or delete one that doesn't, and the "plain" version which raises an error.---Inserting data-----------------Data insertion is done in batches. To insert a batch of rows, pass a list of-rows where each row is an inductive tuple matching the type of the table.-Optional values are encoded as `Maybe` values.--```-populate :: SeldaM ()-populate = do-  insert_ people-    [ "Link"      :*: 125 :*: Just "horse"-    , "Velvet"    :*: 19  :*: Nothing-    , "Kobayashi" :*: 23  :*: Just "dragon"-    , "Miyu"      :*: 10  :*: Nothing-    ]-  insert_ addresses-    [ "Link"      :*: "Kakariko"-    , "Kobayashi" :*: "Tokyo"-    , "Miyu"      :*: "Fuyukishi"-    ]-```--Insertions come in two variants: the "plain" version which reports back the-number of inserted rows, and one appended with an underscore which returns `()`.-Use the latter to explicitly indicate your intent to ignore the return value.--The following example inserts a few rows into a table with an-auto-incrementing primary key:--```-people' :: Table (RowID :*: Text :*: Int :*: Maybe Text)-people' = table "people_with_ids"-  $   autoPrimary "id"-  :*: required "name"-  :*: required "age"-  :*: optional "pet"--populate' :: SeldaM ()-populate' = do-  insert_ people'-    [ def :*: "Link"      :*: 125 :*: Just "horse"-    , def :*: "Velvet"    :*: 19  :*: Nothing-    , def :*: "Kobayashi" :*: 23  :*: Just "dragon"-    , def :*: "Miyu"      :*: 10  :*: Nothing-    ]-```--Note the use of the `def` value for the `id` field. This indicates that the-default value for the column should be used in lieu of any user-provided value.-Since the `id` field is an auto-incrementing primary key, it will automatically-be assigned a unique, increasing value.-Thus, the resulting table would look like this:--```-id | name      | age | pet------------------------------- 0 | Link      | 125 | horse- 1 | Velvet    | 19  |- 2 | Kobayashi | 23  | dragon- 3 | Miyu      | 10  |-```--Auto-incrementing primary keys must always have the type `RowID`.---Updating rows----------------To update a table, pass the table and two functions to the `update` function.-The first is a predicate over table columns. The second is a mapping over table -columns, specifying how to update each row. Only rows satisfying the predicate -are updated.--```-age10Years :: SeldaM ()-age10Years = do-  update_ people (\(name :*: _ :*: _) -> name ./= "Link")-                 (\(name :*: age :*: pet) -> name :*: age + 10 :*: pet)-```--Note that you can use arithmetic, logic and other standard SQL operations on-the columns in either function. Columns implement the appropriate numeric-type classes. For operations with less malleable types -- logic and-comparisons, for instance -- the standard Haskell operators are prefixed-with a period (`.`).---Deleting rows----------------Deleting rows is quite similar to updating them. The only difference is that-the `deleteFrom` operation takes a table and a predicate, specifying which rows-to delete.-The following example deletes all minors from the `people` table:--```-byeMinors :: SeldaM ()-byeMinors = deleteFrom_ people (\(_ :*: age :*: _) -> age .< 20)-```---Basic queries----------------Queries are written in the `Query` monad, in which you can query tables,-restrict the result set, and perform inner, aggregate queries.-Queries are executed in some Selda monad using the `query` function.--The following example uses the `select` operation to draw each row from the-`people` table, and the `restrict` operation to remove out all rows except-those having an `age` column with a value greater than 20.---```-grownups :: Query s (Col s Text)-grownups = do-  (name :*: age :*: _) <- select people-  restrict (age .> 20)-  return name--printGrownups :: SeldaM ()-printGrownups = do-  names <- query grownups-  liftIO (print names)-```--You may have noticed that in addition to the return type of a query,-the `Query` type has an additional type parameter `s`.-We'll cover this parameter in more detail when we get to-aggregating queries, so for now you can just ignore it.---Selector functions---------------------It's often annoying to explicitly take the tuples returned by queries apart.-For this reason, Selda provides a function `selectors` to generate-*selectors*: identifiers which can be used with the `!` operator to access-elements of inductive tuples similar to how record selectors are used to access-fields of standard Haskell record types.--Rewriting the previous example using selector functions:--```-name :*: age :*: pet = selectors people--grownups :: Query s (Col s Text)-grownups = do-  p <- select people-  restrict (p ! age .> 20)-  return (p ! name)--printGrownups :: SeldaM ()-printGrownups = do-  names <- query grownups-  liftIO (print names)-```--For added convenience, the `tableWithSelectors` function creates both a table-and its selector functions at the same time:--```-posts :: Table (RowID :*: Maybe Text :*: Text)-(posts, postId :*: author :*: content)-  =   tableWithSelectors "posts"-  $   autoPrimary "id"-  :*: optional "author"-  :*: required "content"--allAuthors :: Query s Text-allAuthors = do-  p <- select posts-  return (p ! author)-```--You can also use selectors with the `with` function to update columns in a tuple.-`with` takes a tuple and a list of *assignments*, where each assignment is a-selector-value pair. For each assignment, the column indicated by the selector-will be set to the corresponding value, on the given tuple.--```-grownupsIn10Years :: Query s (Col s Text)-grownupsIn10Years = do-  p <- select people-  let p' = p `with` [age := p ! age + 10]-  restrict (p' ! age .> 20)-  return (p' ! name)-```--Of course, selectors can be used for updates and deletions as well.--For the remainder of this tutorial, we'll keep matching on the tuples-explicitly.---Products and joins---------------------Of course, data can be drawn from multiple tables. The unfiltered result set-is essentially the cartesian product of all queried tables.-For this reason, `restrict` calls should be made as early as possible, to avoid-creating an unnecessarily large result set.--Arbitrary Haskell values can be injected into queries. As injected values are-passed as parameters to prepared statements under the hood, there is no need-to escape data; SQL injection is impossible by construction.--The following example uses data from two tables to find all grown-ups who-reside in Tokyo. Note the use of the `text` function, to convert a Haskell-`Text` value into an SQL column literal, as well as the use of `name .== name'`-to remove all elements from the result set where the name in the `people` table-does not match the one in the `addresses` table.--```-grownupsIn :: Text -> Query s (Col s Text)-grownupsIn city = do-  (name :*: age :*: _) <- select people-  restrict (age .> 20)-  (name' :*: home) <- select addresses-  restrict (home .== text city .&& name .== name')-  return name--printGrownupsInTokyo :: SeldaM ()-printGrownupsInTokyo = do-  names <- query (grownupsIn "Tokyo")-  liftIO (print names)-```--Also note that this is slightly different from an SQL join. If, for instance,-you wanted to get a list of all people and their addresses, you might do-something like this:--```-allPeople :: Query s (Col s Text :*: Col s Text)-allPeople = do-  (people_name :*: _ :*: _) <- select people-  (addresses_name :*: city) <- select addresses-  restrict (people_name .== addresses_name)-  return (people_name :*: city)-```--This will give you the list of everyone who has an address, resulting in the-following result set:--```-name      | city-----------------------Link      | Kakariko-Kobayashi | Tokyo-Miyu      | Fuyukishi-```--Note the absence of Velvet in this result set. Since there is no entry for-Velvet in the `addresses` table, there can be no entry in the product table-`people × addresses` where both `people_name` and `addresses_name` are equal-to `"Velvet"`. To produce a table like the above but with a `NULL` column for-Velvet's address (or for anyone else who does not have an entry in the-`addresses` table), you would have to use a join:--```-allPeople' :: Query s (Col s Text :*: Col s (Maybe Text))-allPeople' = do-  (name :*: _ :*: _) <- select people-  (_ :*: city) <- leftJoin (\(name' :*: _) -> name .== name')-                           (select addresses)-  return (name :*: city)-```--This gives us the result table we want:--```-name      | city-----------------------Link      | Kakariko-Velvet    |-Kobayashi | Tokyo-Miyu      | Fuyukishi--```--The `leftJoin` function left joins its query argument to the current result set-for all rows matching its predicate argument.-Note that all columns returned from the inner (or right) query are converted by-`leftJoin` into nullable columns. As there may not be a right counter part for-every element in the result set, SQL and Selda alike set any missing joined-columns to `NULL`.---Aggregate queries, grouping and sorting------------------------------------------You can also perform queries that sum, count, or otherwise aggregate their-result sets. This is done using the `aggregate` function.-This is where the additional type parameter to `Query` comes into play.-When used as an inner query, aggregate queries must not depend on any columns-from the outer query. To enforce this, the `aggregate` function forces all-operations to take place in the `Query (Inner s)` monad, if the outer query-takes place in the `Query s` monad. This ensures that aggregate inner queries-can only communicate with their outside query by returning some value.--Like in standard SQL, aggregate queries can be grouped by column name or by-some arbitrary expression.-An aggregate subquery must return at least one aggregate column, obtained using-`sum_`, `avg`, `count`, or one of the other provided aggregate functions.-Note that aggregate columns, having type `Aggr s a`, are different from normal-columns of type `Col s a`.-Since SQL does not allow aggregate functions in `WHERE` clauses, Selda prevents-them from being used in arguments to `restrict`.--The following example uses an aggregate query to calculate how many home each-person has, and order the result set with the most affluent homeowners at the-top.--```-countHomes :: Query s (Col s Text :*: Col s Int)-countHomes = do-  (name :*: _ :*: _) <- select people-  (owner :*: homes) <- aggregate $ do-    (owner :*: city) <- select addresses-    owner' <- groupBy owner-    return (owner' :*: count city)-  restrict (owner .== name)-  order homes descending-  return (owner :*: homes)-```--Note how `groupBy` returns an aggregate version of its argument, which can be-returned from the aggregate query. In this example, returning `owner` instead of-`owner'` wouldn't work since the former is a plain column and not an aggregate.---Transactions---------------All databases supported by Selda guarantee that each query is atomic: either-the entire query is performed in one go, with no observable intermediate state,-or the whole query fails without leaving a trace in the database.-However, sometimes this guarantee is not enough.-Consider, for instance, a money transfer from Alice's bank account to Bob's.-This involves at least two queries: one to remove the money from-Alice's account, and one to add the same amount to Bob's.-Clearly, it would be *bad* if this operation were to be interrupted after-withdrawing the money from Alice's account but before depositing it into Bob's.--The solution to this problem is *transactions*: a mechanism by which-*a list of queries* gain the same atomicity guarantees as a single query always-enjoys. Using transactions in Selda is super easy:--```-transferMoney :: Text -> Text -> Double -> SeldaM ()-transferMoney from to amount = do-  transaction $ do-    update_ accounts (\(owner :*: _) -> owner .== text from)-                     (\(owner :*: money) -> owner :*: money - float amount)-    update_ accounts (\(owner :*: _) -> owner .== text to)-                     (\(owner :*: money) -> owner :*: money + float amount)-```--This is all there is to it: pass the entire computation to the `transaction`-function, and the whole computation is guaranteed to either execute atomically,-or to fail without leaving a trace in the database.-If an exception is raised during the computation, it will of course be rolled-back.--Do be careful, however, to avoid performing IO within a query.-While they will not affect the atomicity of the computation as far as the-database is concerned, the computations themselves can obviously not be-rolled back.---In-process caching---------------------In many applications, read operations are orders of magnitude more common than-write operations. For such applications, it is often useful to *cache* the-results of a query, to avoid having the database perform the same, potentially-heavy, query over and over even though we *know* we'll get the same result-every time.--Selda supports automatic caching of query results out of the box.-However, it is turned off by default.-To enable caching, use the `setLocalCache` function.--```-main = withPostgreSQL connection_info $ do-  setLocalCache 1000-  ...-```--This will enable local caching of up to 1,000 different results.-When that limit is reached, the least recently used result will be discarded,-so the next request for that result will need to actually execute the query-on the database backend.-If caching was already enabled, changing the maximum number of cached results-will discard the cache's previous contents.-Setting the cache limit to 0 disables caching again.--To make sure that the cache is always consistent with the underlying database,-Selda keeps track of which tables each query depends on.-Whenever an insert, update, delete or drop is issued on a table `t`, all cached-queries that depend on `t` will be discarded.--This guarantees consistency between cache and database, but *only* under the-assumption that *no other process will modify the database*.-If this assumption does not hold for your application, you should avoid using-in-process caching.-It is perfectly fine, however, to have multiple *threads* within the same-application modifying the same database as long as they're all using Selda-to do it, as the cache is shared between all Selda computations-running in the same process.---Generic tables and queries-----------------------------Selda also supports building tables and queries from (almost) arbitrary-data types, using the `Database.Selda.Generic` module.-Re-implementing the ad hoc `people` and `addresses` tables from before in a-more disciplined manner in this way is quite easy:--```-data Person = Person-  { personName :: Text-  , age        :: Int-  , pet        :: Maybe Int-  } deriving Generic--data Address = Address-  { addrName :: Text-  , city     :: Text-  } deriving Generic---people :: GenTable Person-people = genTable "people" [personName :- primaryGen]--addresses :: GenTable Address-addresses = genTable "addresses" [personName :- primaryGen]-```--This will declare two tables with the same structure as their ad hoc-predecessors. Creating the tables is similarly easy:--```-create :: SeldaM ()-create = do-  createTable (gen people)-  createTable (gen addresses)-```--Note the use of the `gen` function here, to extract the underlying table of-columns from the generic table.--However, queries over generic tables aren't magic; they still consist of the-same collections of columns as queries over non-generic tables.--```-genericGrownups2 :: Query s (Col s Text)-genericGrownups2 = do-  (name :*: age :*: _) <- select (gen people)-  restrict (age .> 20)-  return name-```--Finally, with generics it's also quite easy to re-assemble Haskell objects-from the results of a query using the `fromRel` function.--```-getPeopleOfAge :: Int -> SeldaM [Person]-getPeopleOfAge yrs = do-  ps <- query $ do-    (name :*: age :*: _) <- select (gen people)-    restrict (age .== yrs)-    return p-  return (map fromRel ps)-```---Prepared statements----------------------While Selda makes use of prepared statements internally to ensure that any and-all input is safely escaped, it does not reuse those statements by default.-Every query is recompiler and replanned each time it is executed.-To improve the performance of your code, you should make use of the `prepared`-function, to mark performance-critical queries as reusable.--The `prepared` function converts any function `f` in the `Query` monad into an-equivalent function `f'` in some `MonadSelda`, provided that all of `f`'s-arguments are column expressions.-When `f'` is called for the first time during a connection to a database, it-automatically gets compiled, prepared and cached before being executed.-Any subsequent calls to `f'` from the same connection will reuse the prepared-version.--Note that since most database engines don't allow prepared statements to persist-across connections, a previously cached statement will get prepared once more if-called from another connection.--As an example, we modify the `grownupsIn` function we saw earlier to use prepared-statements.--```-preparedGrownupsIn :: Text -> SeldaM [Text]-preparedGrownupsIn = prepared $ \city -> do-  (name :*: age :*: _) <- select people-  restrict (age .> 20)-  (name' :*: home) <- select addresses-  restrict (home .== city .&& name .== name')-  return name-```--Note that the type of the `city` argument is `Col s Text` within the query, but-when *calling* `preparedGrownupsIn`, we instead pass in a value of type `Text`;-for convenience, `prepared` automatically converts all arguments to-prepared functions into their equivalent column types.---Foreign keys---------------To add a foreign key constraint on a column, use the `fk` function.-This function takes two parameters: a column of the table being defined, and-a tuple of the `(table, column)` the foreign key refers to.-The table identifier is simply a value of type `Table t`, while the column-is specified using a selector of type `Selector t a`.--The following example creates a table to store users, and one to store blog-posts. The `users` table stores a name, a password, and a unique identifier-for each user.-The `posts` table stores, for each post, the post body, a unique-post identifier, and the identifier of the user who wrote the post.-The column storing a post's author has a foreign key constraint on the `userid`-column of the `users` table, to ensure that each post has a valid author.--```-users :: Table (RowID :*: Text)-users = table "users"-  $   primary "userid"-  :*: required "username"-  :*: required "password"-(userId :*: userName :*: userPass) = selectors users--posts :: Table (RowID :*: RowID :*: Text)-posts = table "posts"-  $   primary "postid"-  :*: required "authorid" `fk` (users, userId)-  :*: required "post_body"-```--Note that a foreign key can *only* refer to a column which is either-a primary key or has a unique constraint. This is not specific to Selda, but-a restriction of SQL.--And with that, we conclude this tutorial. Hopefully it has been enough to get-you comfortably started using Selda.-For a more detailed API reference, please see Selda's-[Haddock documentation](http://hackage.haskell.org/package/selda).-- Hacking ======= @@ -780,7 +116,7 @@     server running on your machine while the VM is running. * Boot your VM and note the password displayed on the login screen. * Create the file `selda-tests/PGConnectInfo.hs` with the following content:-    ```+    ```haskell     {-# LANGUAGE OverloadedStrings #-}     module PGConnectInfo where     import Database.Selda.PostgreSQL@@ -807,9 +143,6 @@  * Monadic if/else. * Streaming-* Type-safe migrations-* `SELECT INTO`.-* Database schema upgrades.-* Stack build. * MySQL/MariaDB backend.+* MSSQL backend. * Automatically sanity check changelog, versions and date before release.
selda.cabal view
@@ -1,12 +1,12 @@ name:                selda-version:             0.2.0.0+version:             0.3.0.0 synopsis:            Multi-backend, high-level EDSL for interacting with SQL databases. description:         This package provides an EDSL for writing portable, type-safe, high-level                      database code. Its feature set includes querying and modifying databases,                      automatic, in-process caching with consistency guarantees, and transaction                      support. -                     See the package readme for a brief usage tutorial.+                     See the project website for a comprehensive tutorial.                                            To use this package you need at least one backend package, in addition to                      this package. There are currently two different backend packages:@@ -20,7 +20,7 @@ build-type:          Simple extra-source-files:  ChangeLog.md cabal-version:       >=1.10-tested-with:         GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.1+tested-with:         GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.1, GHC == 8.4.1  extra-source-files:   README.md@@ -46,8 +46,9 @@   exposed-modules:     Database.Selda     Database.Selda.Backend-    Database.Selda.Generic+    Database.Selda.Migrations     Database.Selda.Unsafe+    Database.Selda.Validation   other-modules:     Database.Selda.Backend.Internal     Database.Selda.Caching@@ -55,6 +56,7 @@     Database.Selda.Compile     Database.Selda.Exp     Database.Selda.Frontend+    Database.Selda.Generic     Database.Selda.Inner     Database.Selda.Prepared     Database.Selda.Query@@ -63,10 +65,12 @@     Database.Selda.SQL     Database.Selda.SQL.Print     Database.Selda.SQL.Print.Config+    Database.Selda.SqlRow     Database.Selda.SqlType     Database.Selda.Table     Database.Selda.Table.Compile-    Database.Selda.Table.Foreign+    Database.Selda.Table.Type+    Database.Selda.Table.Validation     Database.Selda.Transform     Database.Selda.Types   other-extensions:@@ -108,3 +112,6 @@     Haskell2010   ghc-options:     -Wall+  if impl(ghc > 8.0)+    ghc-options:+      -fno-warn-redundant-constraints
src/Database/Selda.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE OverloadedStrings, FlexibleInstances, UndecidableInstances #-} {-# LANGUAGE ScopedTypeVariables, TypeOperators, GADTs, FlexibleContexts #-}+{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, TypeFamilies, CPP #-}+{-# LANGUAGE DataKinds #-} -- | Selda is not LINQ, but they're definitely related. -- --   Selda is a high-level EDSL for interacting with relational databases.@@ -15,75 +17,38 @@ --   This includes database connection errors, uniqueness constraint errors, --   etc. -----   The following example shows off Selda's most basic features -- creating,---   populating, modifying and querying tables -- and is intended to act as a---   Hello World-ish quickstart.------ > {-# LANGUAGE TypeOperators, OverloadedStrings #-}--- > import Data.Text (Text, unpack)--- > import Database.Selda--- > import Database.Selda.SQLite--- >--- > people :: Table (Text :*: Int :*: Maybe Text)--- > (people, pName :*: pAge :*: pPet)--- >   = tableWithSelectors "people"--- >   $   primary "name"--- >   :*: required "age"--- >   :*: optional "pet"--- >--- > main = withSQLite "people.sqlite" $ do--- >   createTable people--- >--- >   insert_ people--- >     [ "Velvet"    :*: 19 :*: Nothing--- >     , "Kobayashi" :*: 23 :*: Just "dragon"--- >     , "Miyu"      :*: 10 :*: Nothing--- >     ]--- >--- >   update_ people--- >     (\person -> person ! pName .== "Velvet")--- >     (\person -> person `with` [pPet := just "orthros"])--- >--- >   adults <- query $ do--- >     person <- select people--- >     restrict (person ! pAge .> 20)--- >     return (person ! pName :*: person ! pAge)--- >--- >   n <- deleteFrom people (\person -> isNull (person ! pPet))--- >--- >   liftIO $ do--- >     putStrLn "The adults in the room are:"--- >     mapM_ printPerson adults--- >     putStrLn $ show n ++ " people were deleted for having no pets."--- >--- > printPerson :: Text :*: Int -> IO ()--- > printPerson (name :*: age) = putStrLn $ unpack name ++ ", age " ++ show age------   Please see <http://hackage.haskell.org/package/selda/#readme>---   for a more comprehensive tutorial.+--   See <https://selda.link/tutorial> for a tutorial covering the language+--   basics. module Database.Selda   ( -- * Running queries     MonadSelda   , SeldaError (..), ValidationError-  , SeldaT, SeldaM, Table, Query, Col, Res, Result-  , query, transaction, setLocalCache+  , SeldaT, SeldaM+  , Relational, Only (..), The (..)+  , Table, Query, Row, Col, Res, Result+  , query, queryInto+  , transaction, setLocalCache, withoutForeignKeyEnforcement     -- * Constructing queries-  , Selector, (!), Assignment(..), with-  , SqlType (..), SqlEnum (..)-  , Cols, Columns+  , Selector, Source, Selected, (!), Assignment ((:=)), with+  , (+=), (-=), (*=), (||=), (&&=), ($=)+  , SqlType (..), SqlRow (..), SqlEnum (..)+  , Columns   , Order (..)   , (:*:)(..)   , select, selectValues, from, distinct   , restrict, limit-  , order , ascending, descending+  , order, ascending, descending+  , orderRandom   , inner, suchThat     -- * Expressions over columns   , Set (..)-  , RowID, invalidRowId, isInvalidRowId, fromRowId+  , ID, invalidId, isInvalidId, untyped, toId+  , RowID, invalidRowId, isInvalidRowId, fromRowId, toRowId   , (.==), (./=), (.>), (.<), (.>=), (.<=), like   , (.&&), (.||), not_   , literal, int, float, text, true, false, null_   , roundTo, length_, isNull, ifThenElse, matchNull+  , new, only     -- * Converting between column types   , round_, just, fromBool, fromInt, toString     -- * Inner queries@@ -92,7 +57,6 @@   , aggregate, groupBy   , count, avg, sum_, max_, min_     -- * Modifying tables-  , Insert   , insert, insert_, insertWithPK, tryInsert, insertUnless, insertWhen, def   , update, update_, upsert   , deleteFrom, deleteFrom_@@ -100,17 +64,15 @@   , Preparable, Prepare   , prepared     -- * Defining schemas-  , TableSpec, ColSpecs, ColSpec, TableName, ColName-  , NonNull-  , Append (..), (:++:)-  , Selectors, HasSelectors-  , table, tableWithSelectors, selectors-  , required, optional-  , primary, autoPrimary-  , fk, optFk, unique+  , Generic+  , TableName, ColName, Attr (..), Attribute+  , Selectors, ForeignKey (..)+  , table, tableFieldMod, tableWithSelectors, selectors+  , primary, autoPrimary, untypedAutoPrimary, unique+  , IndexMethod (..), index, indexUsing     -- * Creating and dropping tables   , createTable, tryCreateTable-  , validateTable, dropTable, tryDropTable+  , dropTable, tryDropTable     -- * Compiling and inspecting queries   , OnError (..)   , compile@@ -123,44 +85,128 @@   , MonadIO, liftIO   , Text, Day, TimeOfDay, UTCTime   ) where+import Data.Typeable (Typeable) import Database.Selda.Backend import Database.Selda.Column import Database.Selda.Compile import Database.Selda.Frontend+import Database.Selda.Generic import Database.Selda.Inner import Database.Selda.Prepared import Database.Selda.Query import Database.Selda.Query.Type import Database.Selda.Selectors import Database.Selda.SQL hiding (distinct)+import Database.Selda.SqlRow import Database.Selda.SqlType import Database.Selda.Table import Database.Selda.Table.Compile-import Database.Selda.Table.Foreign+import Database.Selda.Table.Validation import Database.Selda.Types import Database.Selda.Unsafe-import Control.Exception (throw)+import Data.Proxy+import Data.String (IsString) import Data.Text (Text) import Data.Time (Day, TimeOfDay, UTCTime) import Data.Typeable (eqT, (:~:)(..))+import GHC.Generics (Rep) import Unsafe.Coerce +#if MIN_VERSION_base(4, 9, 0)+import GHC.TypeLits as TL+#endif+ -- | Any column type that can be used with the 'min_' and 'max_' functions. class SqlType a => SqlOrd a instance {-# OVERLAPPABLE #-} (SqlType a, Num a) => SqlOrd a+instance SqlOrd RowID instance SqlOrd Text instance SqlOrd Day instance SqlOrd UTCTime instance SqlOrd TimeOfDay instance SqlOrd a => SqlOrd (Maybe a)+instance Typeable a => SqlOrd (ID a) --- | Validate a table schema.---   Throws a 'ValidationError' if the schema does not validate.---   Currently does not check the schema against what's actually in the---   current database.-validateTable :: MonadSelda m => Table a -> m ()-validateTable t = validate (tableName t) (tableCols t) `seq` return ()+-- | Wrapper for single column tables.+--   Use this when you need a table with only a single column, with 'table' or+--   'selectValues'.+newtype Only a = Only a+  deriving+    ( Generic+    , Show+    , Read+    , Eq+    , Ord+    , Enum+    , Num+    , Integral+    , Fractional+    , Real+    , IsString+    )+instance SqlType a => SqlRow (Only a) +#if MIN_VERSION_base(4, 9, 0)+instance (TypeError+  ( 'TL.Text "'Only " ':<>: 'ShowType a ':<>: 'TL.Text "' is not a proper SQL type."+    ':$$: 'TL.Text "Use 'the' to access the value of the column."+  ), Typeable a) => SqlType (Only a) where+  mkLit = error "unreachable"+  sqlType = error "unreachable"+  fromSql = error "unreachable"+  defaultValue = error "unreachable"+#endif++-- | Add the given column to the column pointed to by the given selector.+(+=) :: (SqlType a, Num (Col s a)) => Selector t a -> Col s a -> Assignment s t+s += c = s $= (+ c)+infixl 2 +=++-- | Subtract the given column from the column pointed to by the given selector.+(-=) :: (SqlType a, Num (Col s a)) => Selector t a -> Col s a -> Assignment s t+s -= c = s $= (\x -> x - c)+infixl 2 -=++-- | Multiply the column pointed to by the given selector, by the given column.+(*=) :: (SqlType a, Num (Col s a)) => Selector t a -> Col s a -> Assignment s t+s *= c = s $= (* c)+infixl 2 *=++-- | Logically @OR@ the column pointed to by the given selector with+--   the given column.+(||=) :: Selector t Bool -> Col s Bool -> Assignment s t+s ||= c = s $= (.|| c)+infixl 2 ||=++-- | Logically @AND@ the column pointed to by the given selector with+--   the given column.+(&&=) :: Selector t Bool -> Col s Bool -> Assignment s t+s &&= c = s $= (.&& c)+infixl 2 &&=++class The a where+  type TheOnly a+  -- | Extract the value of a row from a singleton table.+  the :: a -> TheOnly a++instance The (Only a) where+  type TheOnly (Only a) = a+  the (Only x) = x++instance The (Row s (Only a)) where+  type TheOnly (Row s (Only a)) = Col s a+  the (Many [Untyped x]) = One (unsafeCoerce x)+  the (Many _)           = error "BUG: non-singleton Only-column"++-- | Create a singleton table column from an appropriate value.+only :: SqlType a => Col s a -> Row s (Only a)+only (One x)  = Many [Untyped x]++-- | Create a new column with the given fields.+--   Any unassigned fields will contain their default values.+new :: forall s a. Relational a => [Assignment s a] -> Row s a+new fields = Many (gNew (Proxy :: Proxy (Rep a))) `with` fields+ -- | Convenient shorthand for @fmap (! sel) q@. --   The following two queries are quivalent: --@@ -168,10 +214,10 @@ -- > q2 = do -- >   person <- select people -- >   return (person ! name)-from :: ToDyn (Cols () a)-     => Selector a b-     -> Query s (Cols s a)-     -> Query s (Col s b)+from :: (Typeable a, SqlType b)+     => Selector (Source a) b+     -> Query s (Row s a)+     -> Query s (Col s (Selected a b)) from s q = (! s) <$> q infixr 7 `from` @@ -218,14 +264,18 @@ infixl 4 .<=  -- | Is the given column null?-isNull :: Col s (Maybe a) -> Col s Bool+isNull :: SqlType a => Col s (Maybe a) -> Col s Bool isNull = liftC $ UnOp IsNull  -- | Applies the given function to the given nullable column where it isn't null, --   and returns the given default value where it is. -- --   This is the Selda equivalent of 'maybe'.-matchNull :: SqlType a => Col s b -> (Col s a -> Col s b) -> Col s (Maybe a) -> Col s b+matchNull :: (SqlType a, SqlType b)+          => Col s b+          -> (Col s a -> Col s b)+          -> Col s (Maybe a)+          -> Col s b matchNull nullvalue f x = ifThenElse (isNull x) nullvalue (f (cast x))  -- | Any container type for which we can check object membership.@@ -235,12 +285,11 @@ infixl 4 `isIn`  instance Set [] where-  -- TODO: use safe coercions instead of unsafeCoerce   isIn _ []     = false-  isIn (C x) xs = C $ InList x (unsafeCoerce xs)+  isIn (One x) xs = One $ InList x [c | One c <- xs]  instance Set (Query s) where-  isIn (C x) = C . InQuery x . snd . compQueryWithFreshScope+  isIn (One x) = One . InQuery x . snd . compQueryWithFreshScope  (.&&), (.||) :: Col s Bool -> Col s Bool -> Col s Bool (.&&) = liftC2 $ BinOp And@@ -253,23 +302,21 @@ ascending = Asc descending = Desc --- | The default value for a column during insertion.---   For an auto-incrementing primary key, the default value is the next key.------   Using @def@ in any other context than insertion results in a runtime error.-def :: SqlType a => a-def = throw DefaultValueException- -- | Lift a non-nullable column to a nullable one. --   Useful for creating expressions over optional columns: ----- > people :: Table (Text :*: Int :*: Maybe Text)--- > people = table "people" $ required "name" :*: required "age" :*: optional "pet"+-- > data Person = Person {name :: Text, age :: Int, pet :: Maybe Text}+-- >   deriving Generic+-- > instance SqlRow Person -- >+-- > people :: Table Person+-- > people = table "people" []+-- > sName :*: sAge :*: sPet = selectors people+-- > -- > peopleWithCats = do--- >   name :*: _ :*: pet <- select people--- >   restrict (pet .== just "cat")--- >   return name+-- >   person <- select people+-- >   restrict (person ! sPet .== just "cat")+-- >   return (name ! sName) just :: SqlType a => Col s a -> Col s (Maybe a) just = cast @@ -315,12 +362,12 @@ max_ = aggr "MAX"  -- | The smallest value in the given column. Texts are compared lexically.-min_  :: SqlOrd a => Col s a -> Aggr s a+min_ :: SqlOrd a => Col s a -> Aggr s a min_ = aggr "MIN"  -- | Sum all values in the given column.-sum_ :: (SqlType a, Num a) => Col s a -> Aggr s a-sum_ = aggr "SUM"+sum_ :: forall a b s. (SqlType a, SqlType b, Num a, Num b) => Col s a -> Aggr s b+sum_ = castAggr . aggr "SUM"  -- | Round a value to the nearest integer. Equivalent to @roundTo 0@. round_ :: forall s a. (SqlType a, Num a) => Col s Double -> Col s a@@ -349,10 +396,10 @@ fromInt :: (SqlType a, Num a) => Col s Int -> Col s a fromInt = cast --- | Convert any column to a string.-toString :: Col s a -> Col s Text+-- | Convert any SQL type to a string.+toString :: SqlType a => Col s a -> Col s Text toString = cast  -- | Perform a conditional on a column-ifThenElse :: Col s Bool -> Col s a -> Col s a -> Col s a+ifThenElse :: SqlType a => Col s Bool -> Col s a -> Col s a -> Col s a ifThenElse = liftC3 If
src/Database/Selda/Backend.hs view
@@ -4,17 +4,23 @@   ( MonadSelda (..), SeldaT, SeldaM, SeldaError (..)   , StmtID, BackendID (..), QueryRunner, SeldaBackend (..), SeldaConnection   , SqlType (..), SqlValue (..), SqlTypeRep (..)+  , IndexMethod (..)   , Param (..), Lit (..), ColAttr (..)   , PPConfig (..), defPPConfig+  , TableName, ColName, ColumnInfo (..)+  , columnInfo, fromColInfo+  , mkTableName, mkColName, fromTableName, fromColName, rawTableName   , newConnection, allStmts, seldaBackend   , runSeldaT, seldaClose   , sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat   ) where-import Database.Selda.Backend.Internal import Control.Monad import Control.Monad.Catch import Control.Monad.IO.Class import Data.IORef+import Database.Selda.Backend.Internal+import Database.Selda.Table (IndexMethod (..))+import Database.Selda.Types  -- | Close a reusable Selda connection. --   Closing a connection while in use is undefined.
src/Database/Selda/Backend/Internal.hs view
@@ -8,18 +8,20 @@   , Param (..), Lit (..), ColAttr (..)   , SqlType (..), SqlValue (..), SqlTypeRep (..)   , PPConfig (..), defPPConfig+  , ColumnInfo (..), columnInfo, fromColInfo   , sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat   , freshStmtId   , invalidate   , newConnection, allStmts   , runSeldaT, seldaBackend   ) where-import Database.Selda.Caching (invalidate)+import Database.Selda.Caching (invalidate, setMaxItems) import Database.Selda.SQL (Param (..)) import Database.Selda.SqlType-import Database.Selda.Table (Table, ColAttr (..), tableName)+import Database.Selda.Table (Table (..), ColAttr (..), tableName)+import qualified Database.Selda.Table as Table (ColInfo (..)) import Database.Selda.SQL.Print.Config-import Database.Selda.Types (TableName)+import Database.Selda.Types (TableName, ColName) import Control.Concurrent import Control.Exception (throw) import Control.Monad.Catch@@ -80,7 +82,7 @@    --   starting at 0.    --   Backends implementing @runPrepared@ should probably ignore this field.  , stmtParams :: ![Either Int Param]-   +    -- | All tables touched by the statement.  , stmtTables :: ![TableName]  }@@ -119,6 +121,47 @@ allStmts =   fmap (map (\(k, v) -> (k, stmtHandle v)) . M.toList) . readIORef . connStmts +-- | Comprehensive information about a column.+data ColumnInfo = ColumnInfo+  { -- | Name of the column.+    colName :: ColName+    -- | Selda type of the column, or the type name given by the database+    --   if Selda couldn't make sense of the type.+  , colType :: Either Text SqlTypeRep+    -- | Is the given column the primary key of its table?+  , colIsPK :: Bool+    -- | Is the given column auto-incrementing?+  , colIsAutoIncrement :: Bool+    -- | Is the column unique, either through a UNIQUE constraint or by virtue+    --   of being a primary key?+  , colIsUnique :: Bool+    -- | Can the column be NULL?+  , colIsNullable :: Bool+    -- | Does the column have an index?+  , colHasIndex :: Bool+    -- | Any foreign key (table, column) pairs referenced by this column.+  , colFKs :: [(TableName, ColName)]+  } deriving (Show, Eq)++-- | Convert a 'Table.ColInfo' into a 'ColumnInfo'.+fromColInfo :: Table.ColInfo -> ColumnInfo+fromColInfo ci = ColumnInfo+    { colName = Table.colName ci+    , colType = Right $ Table.colType ci+    , colIsPK = Primary `elem` Table.colAttrs ci+    , colIsAutoIncrement = AutoIncrement `elem` Table.colAttrs ci+    , colIsUnique = Unique `elem` Table.colAttrs ci+    , colIsNullable = Optional `elem` Table.colAttrs ci+    , colHasIndex = not $ null [() | Indexed _ <- Table.colAttrs ci]+    , colFKs = map fk (Table.colFKs ci)+    }+  where+    fk (Table tbl _ _, col) = (tbl, col)++-- | Get the column information for each column in the given table.+columnInfo :: Table a -> [ColumnInfo]+columnInfo = map fromColInfo . tableCols+ -- | A collection of functions making up a Selda backend. data SeldaBackend = SeldaBackend   { -- | Execute an SQL statement.@@ -135,6 +178,11 @@     -- | Execute a prepared statement.   , runPrepared :: Dynamic -> [Param] -> IO (Int, [[SqlValue]]) +    -- | Get a list of all columns in the given table, with the type and any+    --   modifiers for each column.+    --   Return an empty list if the given table does not exist.+  , getTableInfo :: TableName -> IO [ColumnInfo]+     -- | SQL pretty-printer configuration.   , ppConfig :: PPConfig @@ -143,6 +191,15 @@      -- | Unique identifier for this backend.   , backendId :: BackendID++    -- | Turn on or off foreign key checking, and initiate/commit+    --   a transaction.+    --+    --   When implementing this function, it is safe to assume that+    --   @disableForeignKeys True@+    --   will always be called exactly once before each+    --   @disableForeignKeys False@.+  , disableForeignKeys :: Bool -> IO ()   }  data SeldaState = SeldaState@@ -156,13 +213,19 @@   }  -- | Some monad with Selda SQL capabilitites.-class MonadIO m => MonadSelda m where+--+--   Note that the default implementations of 'invalidateTable' and+--   'wrapTransaction' flush the entire cache and disable caching when+--   invoked. If you want to use Selda's built-in caching mechanism, you will+--   need to implement these operations yourself.+class (MonadIO m, MonadMask m) => MonadSelda m where   -- | Get the connection in use by the computation.   seldaConnection :: m SeldaConnection    -- | Invalidate the given table as soon as the current transaction finishes.   --   Invalidate the table immediately if no transaction is ongoing.   invalidateTable :: Table a -> m ()+  invalidateTable _ = liftIO $ setMaxItems 0    -- | Safely wrap a transaction. To ensure consistency of the in-process cache,   --   it is important that any cached tables modified during a transaction are@@ -176,14 +239,17 @@   --   2. Start bookkeeping of tables invalidated during the transaction.   --   3. Perform the transaction, with async exceptions restored.   --   4. Commit transaction, invalidate tables, and disable bookkeeping; OR-  --   5. If an exception was raised, rollback transaction and-  --      disable bookkeeping.-  ---  --   See the instance for 'SeldaT' for an example of how to do this safely.+  --   5. If an exception was raised, rollback transaction,+  --      disable bookkeeping, and re-throw the exception.   wrapTransaction :: m () -- ^ Signal transaction commit to SQL backend.                   -> m () -- ^ Signal transaction rollback to SQL backend.                   -> m a  -- ^ Transaction to perform.                   -> m a+  wrapTransaction commit rollback act = do+    bracketOnError (pure ())+                   (const rollback)+                   (const (act <* commit <* liftIO (setMaxItems 0)))+  {-# MINIMAL seldaConnection #-}  -- | Get the backend in use by the computation. seldaBackend :: MonadSelda m => m SeldaBackend
src/Database/Selda/Caching.hs view
@@ -22,10 +22,6 @@  type CacheKey = (Text, Text, [Param]) --- | Reduce all parts of a cache key to HNF.-seqCK :: CacheKey -> a -> a-seqCK (db, q, ps) x = db `seq` q `seq` ps `seq` x- #ifdef NO_LOCALCACHE  cache :: Typeable a => [TableName] -> CacheKey -> a -> IO ()@@ -41,6 +37,10 @@ setMaxItems _ = return ()  #else+-- | Reduce all parts of a cache key to HNF.+seqCK :: CacheKey -> a -> a+seqCK (db, q, ps) x = db `seq` q `seq` ps `seq` x+ instance Hashable Param where   hashWithSalt s (Param x) = hashWithSalt s x instance Hashable (Lit a) where
src/Database/Selda/Column.hs view
@@ -1,59 +1,76 @@-{-# LANGUAGE GADTs, TypeFamilies, TypeOperators, PolyKinds, FlexibleInstances #-}+{-# LANGUAGE GADTs, TypeFamilies, TypeOperators, PolyKinds #-}+{-# LANGUAGE FlexibleInstances, OverloadedStrings, ScopedTypeVariables #-} -- | Columns and associated utility functions, specialized to 'SQL'. module Database.Selda.Column-  ( Cols, Columns-  , Col (..), SomeCol (..), Exp (..), UnOp (..), BinOp (..)+  ( Columns+  , Row (..), Col (..), SomeCol (..), UntypedCol (..)+  , Exp (..), NulOp (..), UnOp (..), BinOp (..)   , toTup, fromTup, liftC, liftC2, liftC3   , allNamesIn+  , hideRenaming   , literal   ) where import Database.Selda.Exp import Database.Selda.SQL import Database.Selda.SqlType+import Database.Selda.SqlRow import Database.Selda.Types+import Data.Proxy import Data.String import Data.Text (Text) --- | Convert a tuple of Haskell types to a tuple of column types.-type family Cols s a where-  Cols s (a :*: b)      = Col s a :*: Cols s b-  Cols s a              = Col s a- -- | Any column tuple. class Columns a where   toTup :: [ColName] -> a-  fromTup :: a -> [SomeCol SQL]+  fromTup :: a -> [UntypedCol SQL] -instance Columns b => Columns (Col s a :*: b) where-  toTup (x:xs) = C (Col x) :*: toTup xs-  toTup _      = error "too few elements to toTup"-  fromTup (C x :*: xs) = Some x : fromTup xs+instance (SqlType a, Columns b) => Columns (Col s a :*: b) where+  toTup (x:xs) = One (Col x) :*: toTup xs+  toTup []     = error "too few elements to toTup"+  fromTup (One x :*: xs) = Untyped x : fromTup xs +instance (SqlRow a, Columns b) => Columns (Row s a :*: b) where+  toTup xs =+    case nestedCols (Proxy :: Proxy a) of+      n -> Many (map (Untyped . Col) (take n xs)) :*: toTup (drop n xs)+  fromTup (Many xs :*: xss) = xs ++ fromTup xss+ instance Columns (Col s a) where-  toTup [x] = C (Col x)+  toTup [x] = One (Col x)   toTup []  = error "too few elements to toTup"-  toTup xs  = C (TblCol xs)-  fromTup (C x) = [Some x]+  toTup _   = error "too many elements to toTup"+  fromTup (One x) = [Untyped x] +instance Columns (Row s a) where+  toTup xs = Many (map (Untyped . Col) xs)+  fromTup (Many xs) = xs+ -- | A database column. A column is often a literal column table, but can also --   be an expression over such a column or a constant expression.-newtype Col s a = C {unC :: Exp SQL a}+newtype Col s a = One (Exp SQL a) +-- | A database row. A row is a collection of one or more columns.+newtype Row s a = Many [UntypedCol SQL]+ -- | A literal expression. literal :: SqlType a => a -> Col s a-literal = C . Lit . mkLit+literal = One . Lit . mkLit  instance IsString (Col s Text) where   fromString = literal . fromString -liftC3 :: (Exp SQL a -> Exp SQL b -> Exp SQL c -> Exp SQL d) -> Col s a -> Col s b -> Col s c -> Col s d-liftC3 f (C a) (C b) (C c) = C (f a b c)+liftC3 :: (Exp SQL a -> Exp SQL b -> Exp SQL c -> Exp SQL d)+       -> Col s a+       -> Col s b+       -> Col s c+       -> Col s d+liftC3 f (One a) (One b) (One c) = One (f a b c)  liftC2 :: (Exp SQL a -> Exp SQL b -> Exp SQL c) -> Col s a -> Col s b -> Col s c-liftC2 f (C a) (C b) = C (f a b)+liftC2 f (One a) (One b) = One (f a b)  liftC :: (Exp SQL a -> Exp SQL b) -> Col s a -> Col s b-liftC f = C . f . unC+liftC f (One x) = One (f x)  instance (SqlType a, Num a) => Num (Col s a) where   fromInteger = literal . fromInteger@@ -75,11 +92,11 @@  instance Fractional (Col s Double) where   fromRational = literal . fromRational-  (/) = liftC2 $ BinOp Div  +  (/) = liftC2 $ BinOp Div  instance Fractional (Col s (Maybe Double)) where   fromRational = literal . Just . fromRational-  (/) = liftC2 $ BinOp Div  +  (/) = liftC2 $ BinOp Div  instance Fractional (Col s Int) where   fromRational = literal . (truncate :: Double -> Int) . fromRational
src/Database/Selda/Compile.hs view
@@ -1,18 +1,22 @@ {-# LANGUAGE GADTs, TypeOperators, TypeFamilies, ScopedTypeVariables #-} {-# LANGUAGE FlexibleInstances, FlexibleContexts, UndecidableInstances #-}+{-# LANGUAGE ConstraintKinds #-} -- | Selda SQL compilation. module Database.Selda.Compile   ( Result, Res-  , toRes, compQuery, compQueryWithFreshScope+  , buildResult, compQuery, compQueryWithFreshScope   , compile, compileWith, compileWithTables   , compileInsert, compileUpdate, compileDelete   )   where+import Control.Monad (liftM2) import Database.Selda.Column+import Database.Selda.Generic import Database.Selda.Query.Type import Database.Selda.SQL import Database.Selda.SQL.Print import Database.Selda.SQL.Print.Config+import Database.Selda.SqlRow import Database.Selda.SqlType import Database.Selda.Table import Database.Selda.Table.Compile@@ -48,7 +52,7 @@ -- | Compile an @INSERT@ query, given the keyword representing default values --   in the target SQL dialect, a table and a list of items corresponding --   to the table.-compileInsert :: Insert a => PPConfig -> Table a -> [a] -> [(Text, [Param])]+compileInsert :: Relational a => PPConfig -> Table a -> [a] -> [(Text, [Param])] compileInsert _ _ [] =   [(empty, [])] compileInsert cfg tbl rows =@@ -65,25 +69,28 @@         (x, xs') -> x : chunk chunksize xs'  -- | Compile an @UPDATE@ query.-compileUpdate :: forall s a. (Columns (Cols s a), Result (Cols s a))-              => PPConfig                 -- ^ SQL pretty-printer config.-              -> Table a                  -- ^ The table to update.-              -> (Cols s a -> Cols s a)   -- ^ Update function.-              -> (Cols s a -> Col s Bool) -- ^ Predicate: update only when true.+compileUpdate :: forall s a. (Relational a, SqlRow a)+              => PPConfig+              -> Table a                 -- ^ Table to update.+              -> (Row s a -> Row s a)    -- ^ Update function.+              -> (Row s a -> Col s Bool) -- ^ Predicate.               -> (Text, [Param]) compileUpdate cfg tbl upd check =     compUpdate cfg (tableName tbl) predicate updated   where     names = map colName (tableCols tbl)-    cs = toTup names+    cs = tableExpr tbl     updated = zip names (finalCols (upd cs))-    C predicate = check cs+    One predicate = check cs  -- | Compile a @DELETE FROM@ query.-compileDelete :: Columns (Cols s a)-              => PPConfig -> Table a -> (Cols s a -> Col s Bool) -> (Text, [Param])+compileDelete :: Relational a+              => PPConfig+              -> Table a+              -> (Row s a -> Col s Bool)+              -> (Text, [Param]) compileDelete cfg tbl check = compDelete cfg (tableName tbl) predicate-  where C predicate = check $ toTup $ map colName $ tableCols tbl+  where One predicate = check $ toTup $ map colName $ tableCols tbl  -- | Compile a query to an SQL AST. --   Groups are ignored, as they are only used by 'aggregate'.@@ -108,6 +115,9 @@   s <- atomicModifyIORef' scopeSupply (\s -> (s+1, s))   return $ compQuery s q +buildResult :: Result r => Proxy r -> [SqlValue] -> Res r+buildResult p = runResultReader (toRes p)+ -- | An acceptable query result type; one or more columns stitched together --   with @:*:@. class Typeable (Res r) => Result r where@@ -118,20 +128,27 @@   --   The given list must contain exactly as many elements as dictated by   --   the @Res r@. If the result is @a :*: b :*: c@, then the list must   --   contain exactly three values, for instance.-  toRes :: Proxy r -> [SqlValue] -> Res r+  toRes :: Proxy r -> ResultReader (Res r)    -- | Produce a list of all columns present in the result.   finalCols :: r -> [SomeCol SQL]  instance (SqlType a, Result b) => Result (Col s a :*: b) where   type Res (Col s a :*: b) = a :*: Res b-  toRes _ (x:xs) = fromSql x :*: toRes (Proxy :: Proxy b) xs-  toRes _ _      = error "backend bug: too few result columns to toRes"+  toRes _ = liftM2 (:*:) (fromSql <$> next) (toRes (Proxy :: Proxy b))   finalCols (a :*: b) = finalCols a ++ finalCols b +instance (SqlRow a, Result b) => Result (Row s a :*: b) where+  type Res (Row s a :*: b) = a :*: Res b+  toRes _ = liftM2 (:*:) nextResult (toRes (Proxy :: Proxy b))+  finalCols (a :*: b) = finalCols a ++ finalCols b+ instance SqlType a => Result (Col s a) where   type Res (Col s a) = a-  toRes _ [x] = fromSql x-  toRes _ []  = error "backend bug: too few result columns to toRes"-  toRes _ _   = error "backend bug: too many result columns to toRes"-  finalCols (C c) = [Some c]+  toRes _ = fromSql <$> next+  finalCols (One c) = [Some c]++instance SqlRow a => Result (Row s a) where+  type Res (Row s a) = a+  toRes _ = nextResult+  finalCols (Many cs) = [Some c | Untyped c <- cs]
src/Database/Selda/Exp.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE GADTs #-}+{-# LANGUAGE GADTs, FlexibleContexts, FlexibleInstances #-}+{-# LANGUAGE TypeOperators, CPP, DataKinds #-} -- | The expression type underlying 'Col'. module Database.Selda.Exp where import Database.Selda.SqlType@@ -11,14 +12,24 @@   Some  :: !(Exp sql a) -> SomeCol sql   Named :: !ColName -> !(Exp sql a) -> SomeCol sql +data UntypedCol sql where+  Untyped :: !(Exp sql a) -> UntypedCol sql++-- | Turn a renamed column back into a regular one.+--   If the column was renamed, it will be represented by a literal column,+--   and not its original expression.+hideRenaming :: SomeCol sql -> UntypedCol sql+hideRenaming (Named n _) = Untyped (Col n)+hideRenaming (Some c)    = Untyped c+ -- | Underlying column expression type, parameterised over the type of --   SQL queries. data Exp sql a where   Col     :: !ColName -> Exp sql a-  TblCol  :: ![ColName] -> Exp sql a   Lit     :: !(Lit a) -> Exp sql a   BinOp   :: !(BinOp a b) -> !(Exp sql a) -> !(Exp sql a) -> Exp sql b   UnOp    :: !(UnOp a b) -> !(Exp sql a) -> Exp sql b+  NulOp   :: !(NulOp a) -> Exp sql a   Fun2    :: !Text -> !(Exp sql a) -> !(Exp sql b) -> Exp sql c   If      :: !(Exp sql Bool) -> !(Exp sql a) -> !(Exp sql a) -> Exp sql a   Cast    :: !SqlTypeRep -> !(Exp sql a) -> Exp sql b@@ -26,6 +37,9 @@   InList  :: !(Exp sql a) -> ![Exp sql a] -> Exp sql Bool   InQuery :: !(Exp sql a) -> !sql -> Exp sql Bool +data NulOp a where+  Fun0   :: Text -> NulOp a+ data UnOp a b where   Abs    :: UnOp a a   Not    :: UnOp Bool Bool@@ -58,11 +72,11 @@   allNamesIn = concatMap allNamesIn  instance Names sql => Names (Exp sql a) where-  allNamesIn (TblCol ns)   = ns   allNamesIn (Col n)       = [n]   allNamesIn (Lit _)       = []   allNamesIn (BinOp _ a b) = allNamesIn a ++ allNamesIn b   allNamesIn (UnOp _ a)    = allNamesIn a+  allNamesIn (NulOp _)     = []   allNamesIn (Fun2 _ a b)  = allNamesIn a ++ allNamesIn b   allNamesIn (If a b c)    = allNamesIn a ++ allNamesIn b ++ allNamesIn c   allNamesIn (Cast _ x)    = allNamesIn x@@ -73,3 +87,6 @@ instance Names sql => Names (SomeCol sql) where   allNamesIn (Some c)    = allNamesIn c   allNamesIn (Named n c) = n : allNamesIn c++instance Names sql => Names (UntypedCol sql) where+  allNamesIn (Untyped c) = allNamesIn c
src/Database/Selda/Frontend.hs view
@@ -2,23 +2,24 @@ -- | API for running Selda operations over databases. module Database.Selda.Frontend   ( Result, Res, MonadIO (..), MonadSelda (..), SeldaT-  , query+  , query, queryInto   , insert, insert_, insertWithPK, tryInsert, insertWhen, insertUnless   , update, update_, upsert   , deleteFrom, deleteFrom_   , createTable, tryCreateTable   , dropTable, tryDropTable-  , transaction, setLocalCache+  , transaction, setLocalCache, withoutForeignKeyEnforcement   ) where import Database.Selda.Backend.Internal import Database.Selda.Caching import Database.Selda.Column import Database.Selda.Compile+import Database.Selda.Generic import Database.Selda.Query.Type-import Database.Selda.SQL-import Database.Selda.SqlType (RowID, invalidRowId, unsafeRowId)+import Database.Selda.SqlType (ID, invalidId, toId) import Database.Selda.Table import Database.Selda.Table.Compile+import Database.Selda.Types (fromTableName) import Data.Proxy import Data.Text (Text) import Control.Monad@@ -34,6 +35,20 @@   backend <- seldaBackend   queryWith (runStmt backend) q +-- | Perform the given query, and insert the result into the given table.+--   Returns the number of inserted rows.+queryInto :: (MonadSelda m, Relational a)+          => Table a+          -> Query s (Row s a)+          -> m Int+queryInto tbl q = do+    backend <- seldaBackend+    let (qry, ps) = compileWith (ppConfig backend) q+        qry' = mconcat ["INSERT INTO ", tblName, " ", qry]+    fmap fst . liftIO $ runStmt backend qry' ps+  where+    tblName = fromTableName (tableName tbl)+ -- | Insert the given values into the given table. All columns of the table --   must be present. If your table has an auto-incrementing primary key, --   use the special value 'def' for that column to get the auto-incrementing@@ -42,23 +57,27 @@ -- --   To insert a list of tuples into a table with auto-incrementing primary key: ----- > people :: Table (Int :*: Text :*: Int :*: Maybe Text)--- > people = table "ppl"--- >        $   autoPrimary "id"--- >        :*: required "name"--- >        :*: required "age"--- >        :*: optional "pet"+-- > data Person = Person+-- >   { id :: ID Person+-- >   , name :: Text+-- >   , age :: Int+-- >   , pet :: Maybe Text+-- >   } deriving Generic+-- > instance SqlResult Person -- >+-- > people :: Table Person+-- > people = table "people" [autoPrimary :- id]+-- > -- > main = withSQLite "my_database.sqlite" $ do -- >   insert_ people--- >     [ def :*: "Link"  :*: 125 :*: Just "horse"--- >     , def :*: "Zelda" :*: 119 :*: Nothing+-- >     [ Person def "Link" 125 (Just "horse")+-- >     , Person def "Zelda" 119 Nothing -- >     , ... -- >     ] -- --   Note that if one or more of the inserted rows would cause a constraint --   violation, NO rows will be inserted; the whole insertion fails atomically.-insert :: (MonadSelda m, Insert a) => Table a -> [a] -> m Int+insert :: (MonadSelda m, Relational a) => Table a -> [a] -> m Int insert _ [] = do   return 0 insert t cs = do@@ -73,7 +92,7 @@ -- --   Like 'insert', if even one of the inserted rows would cause a constraint --   violation, the whole insert operation fails.-tryInsert :: (MonadCatch m, MonadSelda m, Insert a) => Table a -> [a] -> m Bool+tryInsert :: (MonadSelda m, Relational a) => Table a -> [a] -> m Bool tryInsert tbl row = do   mres <- try $ insert tbl row   case mres of@@ -90,17 +109,14 @@ -- --   Note that this may perform two separate queries: one update, potentially --   followed by one insert.-upsert :: ( MonadCatch m-          , MonadSelda m-          , Insert a-          , Columns (Cols s a)-          , Result (Cols s a)+upsert :: ( MonadSelda m+          , Relational a           )        => Table a-       -> (Cols s a -> Col s Bool)-       -> (Cols s a -> Cols s a)+       -> (Row s a -> Col s Bool)+       -> (Row s a -> Row s a)        -> [a]-       -> m (Maybe RowID)+       -> m (Maybe (ID a)) upsert tbl check upd rows = transaction $ do   updated <- update tbl check upd   if updated == 0@@ -114,30 +130,24 @@ --   If called on a table which doesn't have an auto-incrementing primary key, --   @Just id@ is always returned on successful insert, where @id@ is a row --   identifier guaranteed to not match any row in any table.-insertUnless :: ( MonadCatch m-                , MonadSelda m-                , Insert a-                , Columns (Cols s a)-                , Result (Cols s a)+insertUnless :: ( MonadSelda m+                , Relational a                 )              => Table a-             -> (Cols s a -> Col s Bool)+             -> (Row s a -> Col s Bool)              -> [a]-             -> m (Maybe RowID)+             -> m (Maybe (ID a)) insertUnless tbl check rows = upsert tbl check id rows  -- | Like 'insertUnless', but performs the insert when at least one row matches --   the predicate.-insertWhen :: ( MonadCatch m-              , MonadSelda m-              , Insert a-              , Columns (Cols s a)-              , Result (Cols s a)+insertWhen :: ( MonadSelda m+              , Relational a               )            => Table a-           -> (Cols s a -> Col s Bool)+           -> (Row s a -> Col s Bool)            -> [a]-           -> m (Maybe RowID)+           -> m (Maybe (ID a)) insertWhen tbl check rows = transaction $ do   matches <- update tbl check id   if matches > 0@@ -146,14 +156,14 @@  -- | Like 'insert', but does not return anything. --   Use this when you really don't care about how many rows were inserted.-insert_ :: (MonadSelda m, Insert a) => Table a -> [a] -> m ()+insert_ :: (MonadSelda m, Relational a) => Table a -> [a] -> m () insert_ t cs = void $ insert t cs  -- | Like 'insert', but returns the primary key of the last inserted row. --   Attempting to run this operation on a table without an auto-incrementing --   primary key will always return a row identifier that is guaranteed to not --   match any row in any table.-insertWithPK :: (MonadSelda m, Insert a) => Table a -> [a] -> m RowID+insertWithPK :: (MonadSelda m, Relational a) => Table a -> [a] -> m (ID a) insertWithPK t cs = do   b <- seldaBackend   if tableHasAutoPK t@@ -161,17 +171,17 @@       res <- liftIO $ do         mapM (uncurry (runStmtWithPK b)) $ compileInsert (ppConfig b) t cs       invalidateTable t-      return $ unsafeRowId (last res)+      return $ toId (last res)     else do       insert_ t cs-      return invalidRowId+      return invalidId  -- | Update the given table using the given update function, for all rows --   matching the given predicate. Returns the number of updated rows.-update :: (MonadSelda m, Columns (Cols s a), Result (Cols s a))-       => Table a                  -- ^ The table to update.-       -> (Cols s a -> Col s Bool) -- ^ Predicate.-       -> (Cols s a -> Cols s a)   -- ^ Update function.+update :: (MonadSelda m, Relational a)+       => Table a                 -- ^ Table to update.+       -> (Row s a -> Col s Bool) -- ^ Predicate.+       -> (Row s a -> Row s a)    -- ^ Update function.        -> m Int update tbl check upd = do   cfg <- ppConfig <$> seldaBackend@@ -180,17 +190,19 @@   return res  -- | Like 'update', but doesn't return the number of updated rows.-update_ :: (MonadSelda m, Columns (Cols s a), Result (Cols s a))+update_ :: (MonadSelda m, Relational a)        => Table a-       -> (Cols s a -> Col s Bool)-       -> (Cols s a -> Cols s a)+       -> (Row s a -> Col s Bool)+       -> (Row s a -> Row s a)        -> m () update_ tbl check upd = void $ update tbl check upd  -- | From the given table, delete all rows matching the given predicate. --   Returns the number of deleted rows.-deleteFrom :: (MonadSelda m, Columns (Cols s a))-           => Table a -> (Cols s a -> Col s Bool) -> m Int+deleteFrom :: (MonadSelda m, Relational a)+           => Table a+           -> (Row s a -> Col s Bool)+           -> m Int deleteFrom tbl f = do   cfg <- ppConfig <$> seldaBackend   res <- uncurry exec $ compileDelete cfg tbl f@@ -198,21 +210,23 @@   return res  -- | Like 'deleteFrom', but does not return the number of deleted rows.-deleteFrom_ :: (MonadSelda m, Columns (Cols s a))-            => Table a -> (Cols s a -> Col s Bool) -> m ()+deleteFrom_ :: (MonadSelda m, Relational a)+            => Table a+            -> (Row s a -> Col s Bool)+            -> m () deleteFrom_ tbl f = void $ deleteFrom tbl f  -- | Create a table from the given schema. createTable :: MonadSelda m => Table a -> m () createTable tbl = do   cfg <- ppConfig <$> seldaBackend-  void . flip exec [] $ compileCreateTable cfg Fail tbl+  mapM_ (flip exec []) $ compileCreateTable cfg Fail tbl  -- | Create a table from the given schema, unless it already exists. tryCreateTable :: MonadSelda m => Table a -> m () tryCreateTable tbl = do   cfg <- ppConfig <$> seldaBackend-  void . flip exec [] $ compileCreateTable cfg Ignore tbl+  mapM_ (flip exec []) $ compileCreateTable cfg Ignore tbl  -- | Drop the given table. dropTable :: MonadSelda m => Table a -> m ()@@ -223,13 +237,29 @@ tryDropTable = withInval $ void . flip exec [] . compileDropTable Ignore  -- | Perform the given computation atomically.---   If an exception is raised during its execution, the enture transaction---   will be rolled back, and the exception re-thrown.-transaction :: (MonadSelda m, MonadCatch m) => m a -> m a+--   If an exception is raised during its execution, the entire transaction+--   will be rolled back and the exception re-thrown, even if the exception+--   is caught and handled within the transaction.+transaction :: MonadSelda m => m a -> m a transaction m =   wrapTransaction (void $ exec "COMMIT" []) (void $ exec "ROLLBACK" []) $ do     exec "BEGIN TRANSACTION" [] *> m +-- | Run the given computation as a transaction without enforcing foreign key+--   constraints.+--+--   If the computation finishes with the database in an inconsistent state+--   with regards to foreign keys, the resulting behavior is undefined.+--   Use with extreme caution, preferably only for migrations.+--+--   On the PostgreSQL backend, at least PostgreSQL 9.6 is required.+withoutForeignKeyEnforcement :: MonadSelda m => m a -> m a+withoutForeignKeyEnforcement m = do+  b <- seldaBackend+  bracket_ (liftIO $ disableForeignKeys b True)+           (liftIO $ disableForeignKeys b False)+           m+ -- | Set the maximum local cache size to @n@. A cache size of zero disables --   local cache altogether. Changing the cache size will also flush all --   entries. Note that the cache is shared among all Selda computations running@@ -265,7 +295,7 @@  -- | Generate the final result of a query from a list of untyped result rows. mkResults :: Result a => Proxy a -> [[SqlValue]] -> [Res a]-mkResults p = map (toRes p)+mkResults p = map (buildResult p)  -- | Run the given computation over a table after invalidating all cached --   results depending on that table.
src/Database/Selda/Generic.hs view
@@ -2,43 +2,38 @@ {-# LANGUAGE TypeFamilies, TypeOperators, FlexibleInstances #-} {-# LANGUAGE UndecidableInstances, MultiParamTypeClasses, OverloadedStrings #-} {-# LANGUAGE FlexibleContexts, ScopedTypeVariables, ConstraintKinds #-}-{-# LANGUAGE GADTs #-}--- | Build tables and database operations from (almost) any Haskell type.------   While the types in this module may look somewhat intimidating, the rules---   for generic tables and queries are quite simple:------     * Any record type with a single data constructor, where all fields are---       instances of 'SqlType', can be used for generic tables and queries---       if it derives 'Generic'.---     * To use the standard functions from "Database.Selda" on a generic table,---       it needs to be unwrapped using 'gen'.---     * Performing a 'select' on a generic table returns all the table's fields---       as an inductive tuple.---     * Tuples obtained this way can be handled either as any other tuple, or---       using the '!' operator together with any record selector for the---       tuple's corresponding type.---     * Relations obtained from a query can be re-assembled into their---       corresponding data type using 'fromRel'.+{-# LANGUAGE GADTs, CPP, DeriveGeneric, DataKinds #-}+-- | Generics utilities. module Database.Selda.Generic   ( Relational, Generic-  , GenAttr (..), GenTable (..), Attribute, Relation-  , genTable, genTableFieldMod, toRel, toRels, fromRel, fromRels-  , insertGen, insertGen_, insertGenWithPK-  , primaryGen, autoPrimaryGen, uniqueGen, fkGen+  , tblCols, mkDummy, identify, params, def, gNew   ) where import Control.Monad.State import Data.Dynamic-import Data.Proxy-import Data.Text (pack)+import Data.Text as Text (Text, pack)+#if MIN_VERSION_base(4, 10, 0) import Data.Typeable+#endif import GHC.Generics hiding (R, (:*:), Selector) import qualified GHC.Generics as G ((:*:)(..), Selector)+#if MIN_VERSION_base(4, 9, 0)+import qualified GHC.TypeLits as TL+import qualified GHC.Generics as G ((:+:)(..))+#endif import Unsafe.Coerce-import Database.Selda hiding (from)-import Database.Selda.Table+import Control.Exception (Exception (..), try, throw)+import System.IO.Unsafe import Database.Selda.Types import Database.Selda.Selectors+import Database.Selda.SqlType+import Database.Selda.SqlRow (SqlRow)+import Database.Selda.Table.Type+import Database.Selda.SQL (Param (..))+import Database.Selda.Exp (Exp (Col, Lit), UntypedCol (..))+import qualified Database.Selda.Column as C (Col)+#if !MIN_VERSION_base(4, 11, 0)+import Data.Monoid+#endif  -- | Any type which has a corresponding relation. --   To make a @Relational@ instance for some type, simply derive 'Generic'.@@ -49,210 +44,15 @@ --   obey those constraints will result in a very confusing type error. type Relational a =   ( Generic a+  , SqlRow a   , GRelation (Rep a)-  , GFromRel (Rep a)-  , ToDyn (Relation a)-  , Insert (Relation a)+  , GSelectors a (Rep a)   ) --- | A generic table. Needs to be unpacked using @gen@ before use with---   'select', 'insert', etc.-newtype GenTable a = GenTable {gen :: Table (Relation a)}---- | The relation corresponding to the given Haskell type.---   This relation simply corresponds to the fields in the data type, from---   left to right. For instance:------ > data Foo = Foo--- >   { bar :: Int--- >   , baz :: Text--- >   }------   In this example, @Relation Foo@ is @(Int :*: Text)@, as the first field---   of @Foo@ has type @Int@, and the second has type @Text@.-type Relation a = Rel (Rep a)---- | A generic column attribute.---   Essentially a pair or a record selector over the type @a@ and a column---   attribute.-data GenAttr a where-  (:-) :: (a -> b) -> Attribute -> GenAttr a---- | Generate a table from the given table name and list of column attributes.---   All @Maybe@ fields in the table's type will be represented by nullable---   columns, and all non-@Maybe@ fields fill be represented by required---   columns.---   For example:------ > data Person = Person--- >   { id   :: Int--- >   , name :: Text--- >   , age  :: Int--- >   , pet  :: Maybe Text--- >   }--- >   deriving Generic--- >--- > people :: GenTable Person--- > people = genTable "people" [(name, autoPrimaryGen)]------   This example will create a table with the column types---   @Int :*: Text :*: Int :*: Maybe Text@, where the first field is---   an auto-incrementing primary key.-genTable :: forall a. Relational a-         => TableName-         -> [GenAttr a]-         -> GenTable a-genTable tn attrs = genTableFieldMod tn attrs id---- | Generate a table from the given table name,---   a list of column attributes and a function---   that maps from field names to column names.---   Ex.:------ > data Person = Person--- >   { personId   :: Int--- >   , personName :: Text--- >   , personAge  :: Int--- >   , personPet  :: Maybe Text--- >   }--- >   deriving Generic--- >--- > people :: GenTable Person--- > people = genTableFieldMod "people" [(personName, autoPrimaryGen)] (stripPrefix "person")------   This will create a table with the columns named---   "Id", "Name", "Age" and "Pet".-genTableFieldMod :: forall a. Relational a-                 => TableName-                 -> [GenAttr a]-                 -> (String -> String)-                 -> GenTable a-genTableFieldMod tn attrs fieldMod = GenTable $ Table tn (validate tn (map tidy cols)) apk-  where-    dummy = mkDummy-    cols = zipWith addAttrs [0..] (tblCols (Proxy :: Proxy a) fieldMod)-    apk = or [AutoIncrement `elem` as | _ :- Attribute as <- attrs]-    addAttrs n ci = ci-      { colAttrs = colAttrs ci ++ concat-          [ as-          | f :- Attribute as <- attrs-          , identify dummy f == n-          ]-      , colFKs = colFKs ci ++-          [ thefk-          | f :- ForeignKey thefk <- attrs-          , identify dummy f == n-          ]-      }---- | Convert a generic type into the corresponding database relation.---   A type's corresponding relation is simply the inductive tuple consisting---   of all of the type's fields.------ > data Person = Person--- >   { id   :: Int--- >   , name :: Text--- >   , age  :: Int--- >   , pet  :: Maybe Text--- >   }--- >   deriving Generic--- >--- > somePerson = Person 0 "Velvet" 19 Nothing--- > (theId :*: theName :*: theAge :*: thePet) = toRel somePerson------   This is mainly useful when inserting values into a table using 'insert'---   and the other functions from "Database.Selda".-toRel :: Relational a => a -> Relation a-toRel = gToRel . from---- | Convenient synonym for @map toRel@.-toRels :: Relational a => [a] -> [Relation a]-toRels = map toRel---- | Re-assemble a generic type from its corresponding relation. This can be---   done either for ad hoc queries or for queries over generic tables:------ > data SimplePerson = SimplePerson--- >   { name :: Text--- >   , age  :: Int--- >   }--- >   deriving Generic--- >--- > demoPerson :: SimplePerson--- > demoPerson = fromRel ("Miyu" :*: 10)--- >--- > adhoc :: Table (Text :*: Int)--- > adhoc = table "adhoc" $ required "name" :*: required "age"--- >--- > getPersons1 :: MonadSelda m => m [SimplePerson]--- > getPersons1 = map fromRel <$> query (select adhoc)--- >--- > generic :: GenTable SimplePerson--- > generic = genTable "generic" []--- >--- > getPersons2 :: MonadSelda m => m [SimplePerson]--- > getPersons2 = map fromRel <$> query (select (gen generic))------   Applying @toRel@ to an inductive tuple which isn't the corresponding---   relation of the return type is a type error.-fromRel :: Relational a => Relation a -> a-fromRel = to . fst . gFromRel . toDyns---- | Convenient synonym for @map fromRel@.-fromRels :: Relational a => [Relation a] -> [a]-fromRels = map fromRel---- | Like 'insertWithPK', but accepts a generic table and---   its corresponding data type.-insertGenWithPK :: (Relational a, MonadSelda m) => GenTable a -> [a] -> m RowID-insertGenWithPK t = insertWithPK (gen t) . map toRel---- | Like 'insert', but accepts a generic table and its corresponding data type.-insertGen :: (Relational a, MonadSelda m) => GenTable a -> [a] -> m Int-insertGen t = insert (gen t) . map toRel---- | Like 'insert_', but accepts a generic table and its corresponding data type.-insertGen_ :: (Relational a, MonadSelda m) => GenTable a -> [a] -> m ()-insertGen_ t = void . insertGen t---- | Some attribute that may be set on a table column.-data Attribute-  = Attribute [ColAttr]-  | ForeignKey (Table (), ColName)---- | A primary key which does not auto-increment.-primaryGen :: Attribute-primaryGen = Attribute [Primary, Required, Unique]---- | An auto-incrementing primary key.-autoPrimaryGen :: Attribute-autoPrimaryGen = Attribute [Primary, AutoIncrement, Required, Unique]---- | A table-unique value.-uniqueGen :: Attribute-uniqueGen = Attribute [Unique]---- | A foreign key constraint referencing the given table and column.-fkGen :: Table t -> Selector t a -> Attribute-fkGen (Table tn tcs tapk) (Selector i) =-  ForeignKey (Table tn tcs tapk, colName (tcs !! i))- -- | A dummy of some type. Encapsulated to avoid improper use, since all of --   its fields are 'unsafeCoerce'd ints. newtype Dummy a = Dummy a --- | Extract all column names from the given type.---   If the type is not a record, the columns will be named @col_1@,---   @col_2@, etc.-tblCols :: forall a. (GRelation (Rep a)) => Proxy a -> (String -> String) -> [ColInfo]-tblCols _ fieldMod = zipWith pack' [0 :: Int ..] $ gTblCols (Proxy :: Proxy (Rep a)) fieldMod-  where-    pack' n ci = ci-      { colName = if colName ci == ""-                    then mkColName . pack $ "col_" ++ show n-                    else colName ci-      }- -- | Create a dummy of the given type. mkDummy :: (Generic a, GRelation (Rep a)) => Dummy a mkDummy = Dummy $ to $ evalState gMkDummy 0@@ -261,64 +61,109 @@ identify :: Dummy a -> (a -> b) -> Int identify (Dummy d) f = unsafeCoerce $ f d --- | The relation corresponding to the given type.-type family Rel (rep :: * -> *) where-  Rel (M1 t c a)  = Rel a-  Rel (K1 i a)    = a-  Rel (a G.:*: b) = Rel a :++: Rel b+-- | Extract all insert parameters from a generic value.+params :: Relational a => a -> [Either Param Param]+params = unsafePerformIO . gParams . from +-- | Extract all column names from the given type.+--   If the type is not a record, the columns will be named @col_1@,+--   @col_2@, etc.+tblCols :: forall a. Relational a => Proxy a -> (Text -> Text) -> [ColInfo]+tblCols _ fieldMod =+    evalState (gTblCols (Proxy :: Proxy (Rep a)) Nothing rename) 0+  where+    rename n Nothing     = mkColName $ fieldMod ("col_" <> pack (show n))+    rename _ (Just name) = modColName name fieldMod++-- | Exception indicating the use of a default value.+--   If any values throwing this during evaluation of @param xs@ will be+--   replaced by their default value.+data DefaultValueException = DefaultValueException+  deriving Show+instance Exception DefaultValueException++-- | The default value for a column during insertion.+--   For an auto-incrementing primary key, the default value is the next key.+--+--   Using @def@ in any other context than insertion results in a runtime error.+def :: SqlType a => a+def = throw DefaultValueException+ class GRelation f where-  -- | Convert a value from its Haskell type into the corresponding relation.-  gToRel   :: f a -> Rel f+  -- | Generic worker for 'params'.+  gParams :: f a -> IO [Either Param Param]    -- | Compute all columns needed to represent the given type.-  gTblCols :: Proxy f -> (String -> String) -> [ColInfo]+  gTblCols :: Proxy f+           -> Maybe ColName+           -> (Int -> Maybe ColName -> ColName)+           -> State Int [ColInfo]    -- | Create a dummy value where all fields are replaced by @unsafeCoerce@'d   --   ints. See 'mkDummy' and 'identify' for more information.   gMkDummy :: State Int (f a) -instance GRelation a => GRelation (M1 C c a) where-  gToRel (M1 x)   = gToRel x-  gTblCols _ = gTblCols (Proxy :: Proxy a)-  gMkDummy = M1 <$> gMkDummy+  -- | Create a new value with all default fields.+  gNew :: Proxy f -> [UntypedCol sql] -instance GRelation a => GRelation (M1 D c a) where-  gToRel (M1 x)   = gToRel x+instance {-# OVERLAPPABLE #-} GRelation a => GRelation (M1 t c a) where+  gParams (M1 x) = gParams x   gTblCols _ = gTblCols (Proxy :: Proxy a)   gMkDummy = M1 <$> gMkDummy+  gNew _ = gNew (Proxy :: Proxy a) -instance (G.Selector c, GRelation a) => GRelation (M1 S c a) where-  gToRel (M1 x)     = gToRel x-  gTblCols _ fieldMod = [ci']+instance {-# OVERLAPPING #-} (G.Selector c, GRelation a) =>+         GRelation (M1 S c a) where+  gParams (M1 x) = gParams x+  gTblCols _ _ = gTblCols (Proxy :: Proxy a) name     where-      [ci] = gTblCols (Proxy :: Proxy a) fieldMod-      ci' = ColInfo-        { colName = mkColName . pack $ fieldMod (selName ((M1 undefined) :: M1 S c a b))-        , colType = colType ci-        , colAttrs = colAttrs ci-        , colFKs = colFKs ci-        }+      name =+        case selName ((M1 undefined) :: M1 S c a b) of+          "" -> Nothing+          s  -> Just (mkColName $ pack s)   gMkDummy = M1 <$> gMkDummy+  gNew _ = gNew (Proxy :: Proxy a)  instance (Typeable a, SqlType a) => GRelation (K1 i a) where-  gToRel (K1 x) = x-  gTblCols _ _  = [ColInfo "" (sqlType (Proxy :: Proxy a)) optReq []]+  gParams (K1 x) = do+    res <- try $ return $! x+    return $ case res of+      Right x'                   -> [Right $ Param (mkLit x')]+      Left DefaultValueException -> [Left $ Param (defaultValue :: Lit a)]++  gTblCols _ name rename = do+    n <- get+    put (n+1)+    let name' = rename n name+    return+      [ ColInfo+        { colName = name'+        , colType = sqlType (Proxy :: Proxy a)+        , colAttrs = optReq+        , colFKs = []+        , colExpr = Untyped (Col name')+        }+      ]     where       -- workaround for GHC 8.2 not resolving overlapping instances properly       maybeTyCon = typeRepTyCon (typeRep (Proxy :: Proxy (Maybe ())))       optReq         | typeRepTyCon (typeRep (Proxy :: Proxy a)) == maybeTyCon = [Optional]         | otherwise                                               = [Required]+   gMkDummy = do     n <- get     put (n+1)     return $ unsafeCoerce n -instance (Append (Rel a) (Rel b), GRelation a, GRelation b) =>-         GRelation (a G.:*: b) where-  gToRel (a G.:*: b)   = gToRel a `app` gToRel b-  gTblCols _ f = gTblCols a f ++ gTblCols b f+  gNew _ = [Untyped (Lit (defaultValue :: Lit a))]++instance (GRelation a, GRelation b) => GRelation (a G.:*: b) where+  gParams (a G.:*: b) = liftM2 (++) (gParams a) (gParams b)+  gTblCols _ _ rename = do+      as <- gTblCols a Nothing rename+      bs <- gTblCols b Nothing rename+      return (as ++ bs)     where       a = Proxy :: Proxy a       b = Proxy :: Proxy b@@ -326,22 +171,28 @@     a <- gMkDummy :: State Int (a x)     b <- gMkDummy :: State Int (b x)     return (a G.:*: b)--class GFromRel f where-  -- | Convert a value to a Haskell type from the type's corresponding relation.-  gFromRel :: [Dynamic] -> (f a, [Dynamic])--instance (GFromRel a, GFromRel b) => GFromRel (a G.:*: b) where-  gFromRel xs =-      (x G.:*: y, xs'')-    where-      (x, xs') = gFromRel xs-      (y, xs'') = gFromRel xs'+  gNew _ = gNew (Proxy :: Proxy a) ++ gNew (Proxy :: Proxy b) -instance Typeable a => GFromRel (K1 i a) where-  gFromRel (x:xs) = (K1 (fromDyn x (error "impossible")), xs)-  gFromRel _      = error "impossible: too few elements to gFromRel"+#if MIN_VERSION_base(4, 9, 0)+instance+  (TL.TypeError+    ( 'TL.Text "Selda currently does not support creating tables from sum types."+      'TL.:$$:+      'TL.Text "Restrict your table type to a single data constructor."+    )) => GRelation (a G.:+: b) where+  gParams = error "unreachable"+  gTblCols = error "unreachable"+  gMkDummy = error "unreachable"+  gNew = error "unreachable" -instance GFromRel a => GFromRel (M1 t c a) where-  gFromRel xs = (M1 x, xs')-    where (x, xs') = gFromRel xs+instance {-# OVERLAPS #-}+  (TL.TypeError+    ( 'TL.Text "Columns are now allowed to nest other columns."+      'TL.:$$:+      'TL.Text "Remove any fields of type 'Col s a' from your table type."+    )) => GRelation (K1 i (C.Col s a)) where+  gParams = error "unreachable"+  gTblCols = error "unreachable"+  gMkDummy = error "unreachable"+  gNew = error "unreachable"+#endif
src/Database/Selda/Inner.hs view
@@ -5,11 +5,13 @@ module Database.Selda.Inner where import Database.Selda.Column import Database.Selda.SQL (SQL)+import Database.Selda.SqlType (SqlType) import Database.Selda.Types import Data.Text (Text) import Data.Typeable-import GHC.Exts+#if MIN_VERSION_base(4, 9, 0) import GHC.TypeLits as TL+#endif  -- | A single aggregate column. --   Aggregate columns may not be used to restrict queries.@@ -17,6 +19,9 @@ --   converted into a non-aggregate column. newtype Aggr s a = Aggr {unAggr :: Exp SQL a} +liftAggr :: (Exp SQL a -> Exp SQL b) -> Aggr s a -> Aggr s b+liftAggr f = Aggr . f . unAggr+ -- | Denotes an inner query. --   For aggregation, treating sequencing as the cartesian product of queries --   does not work well.@@ -34,8 +39,8 @@ -- | Create a named aggregate function. --   Like 'fun', this function is generally unsafe and should ONLY be used --   to implement missing backend-specific functionality.-aggr :: Text -> Col s a -> Aggr s b-aggr f = Aggr . AggrEx f . unC+aggr :: SqlType a => Text -> Col s a -> Aggr s b+aggr f (One x)  = Aggr (AggrEx f x)  -- | Convert one or more inner column to equivalent columns in the outer query. --   @OuterCols (Aggr (Inner s) a :*: Aggr (Inner s) b) = Col s a :*: Col s b@,@@ -43,15 +48,18 @@ type family OuterCols a where   OuterCols (Col (Inner s) a :*: b)  = Col s a :*: OuterCols b   OuterCols (Col (Inner s) a)        = Col s a+  OuterCols (Row (Inner s) a :*: b)  = Row s a :*: OuterCols b+  OuterCols (Row (Inner s) a)        = Row s a #if MIN_VERSION_base(4, 9, 0)   OuterCols (Col s a) = TypeError-    ( TL.Text "An inner query can only return columns from its own scope."+    ( 'TL.Text "An inner query can only return rows and columns from its own scope."     )-#endif-#if MIN_VERSION_base(4, 9, 0)+  OuterCols (Row s a) = TypeError+    ( 'TL.Text "An inner query can only return rows and columns from its own scope."+    )   OuterCols a = TypeError-    ( TL.Text "Only (inductive tuples of) columns can be returned from" :$$:-      TL.Text "an inner query."+    ( 'TL.Text "Only (inductive tuples of) row and columns can be returned from" ':$$:+      'TL.Text "an inner query."     ) #endif @@ -60,14 +68,12 @@   AggrCols (Aggr (Inner s) a)       = Col s a #if MIN_VERSION_base(4, 9, 0)   AggrCols (Aggr s a) = TypeError-    ( TL.Text "An aggregate query can only return columns from its own" :$$:-      TL.Text "scope."+    ( 'TL.Text "An aggregate query can only return columns from its own" ':$$:+      'TL.Text "scope."     )-#endif-#if MIN_VERSION_base(4, 9, 0)   AggrCols a = TypeError-    ( TL.Text "Only (inductive tuples of) aggregates can be returned from" :$$:-      TL.Text "an aggregate query."+    ( 'TL.Text "Only (inductive tuples of) aggregates can be returned from" ':$$:+      'TL.Text "an aggregate query."     ) #endif @@ -84,17 +90,22 @@   LeftCols (Col (Inner s) a :*: b)         = Col s (Maybe a) :*: LeftCols b   LeftCols (Col (Inner s) (Maybe a))       = Col s (Maybe a)   LeftCols (Col (Inner s) a)               = Col s (Maybe a)++  LeftCols (Row (Inner s) (Maybe a) :*: b) = Row s (Maybe a) :*: LeftCols b+  LeftCols (Row (Inner s) a :*: b)         = Row s (Maybe a) :*: LeftCols b+  LeftCols (Row (Inner s) (Maybe a))       = Row s (Maybe a)+  LeftCols (Row (Inner s) a)               = Row s (Maybe a) #if MIN_VERSION_base(4, 9, 0)   LeftCols a = TypeError-    ( TL.Text "Only (inductive tuples of) columns can be returned" :$$:-      TL.Text "from a join."+    ( 'TL.Text "Only (inductive tuples of) rows and columns can be returned" ':$$:+      'TL.Text "from a join."     ) #endif  -- | One or more aggregate columns. class Aggregates a where-  unAggrs :: a -> [SomeCol SQL]+  unAggrs :: a -> [UntypedCol SQL] instance Aggregates (Aggr (Inner s) a) where-  unAggrs (Aggr x) = [Some x]+  unAggrs (Aggr x) = [Untyped x] instance Aggregates b => Aggregates (Aggr (Inner s) a :*: b) where-  unAggrs (Aggr a :*: b) = Some a : unAggrs b+  unAggrs (Aggr a :*: b) = Untyped a : unAggrs b
+ src/Database/Selda/Migrations.hs view
@@ -0,0 +1,133 @@+{-# LANGUAGE OverloadedStrings, GADTs, FlexibleContexts #-}+-- | Functionality for upgrading a table from one schema to another.+module Database.Selda.Migrations+  ( Migration (..)+  , migrate, migrateM, migrateAll, autoMigrate+  ) where+import Control.Monad (void, when)+import Control.Monad.Catch+import Database.Selda hiding (from)+import Database.Selda.Backend.Internal+import Database.Selda.Table.Type (tableName)+import Database.Selda.Table.Validation (ValidationError (..))+import Database.Selda.Types (mkTableName, fromTableName, rawTableName)+import Database.Selda.Validation++-- | Wrapper for user with 'migrateAll', enabling multiple migrations to be+--   packed into the same list:+--+-- > migrateAll+-- >   [ Migration m1_from m1_to m1_upgrade+-- >   , Migration m2_from m2_to m2_upgrade+-- >   , ...+-- >   ]+data Migration where+  Migration :: (Relational a, Relational b)+            => Table a+            -> Table b+            -> (Row s a -> Query s (Row s b))+            -> Migration++-- | A migration step is zero or more migrations that need to be performed in+--   a single transaction in order to keep the database consistent.+type MigrationStep = [Migration]++-- | Migrate the first table into the second, using the given function to+--   migrate all records to the new schema.+--   Both table schemas are validated before starting the migration, and the+--   source table is validated against what's currently in the database.+--+--   The migration is performed as a migration, ensuring that either the entire+--   migration passes, or none of it does.+migrate :: (MonadSelda m, Relational a, Relational b)+        => Table a -- ^ Table to migrate from.+        -> Table b -- ^ Table to migrate to.+        -> (Row () a -> Row () b)+                   -- ^ Mapping from old to new table.+        -> m ()+migrate t1 t2 upg = migrateM t1 t2 ((pure :: a -> Query () a) . upg)++-- | Like 'migrate', but allows the column upgrade to access+--   the entire database.+migrateM :: (MonadSelda m, Relational a, Relational b)+         => Table a+         -> Table b+         -> (Row s a -> Query s (Row s b))+         -> m ()+migrateM t1 t2 upg = migrateAll True [Migration t1 t2 upg]++wrap :: MonadSelda m => Bool -> m a -> m a+wrap enforceFKs+  | enforceFKs = transaction+  | otherwise  = withoutForeignKeyEnforcement++-- | Perform all given migrations as a single transaction.+migrateAll :: MonadSelda m+           => Bool -- ^ Enforce foreign keys during migration?+           -> MigrationStep -- ^ Migration step to perform.+           -> m ()+migrateAll fks =+  wrap fks . mapM_ (\(Migration t1 t2 upg) -> migrateInternal t1 t2 upg)++-- | Given a list of migration steps in ascending chronological order, finds+--   the latest migration step starting state that matches the current database,+--   and performs all migrations from that point until the end of the list.+--   The whole operation is performed as a single transaction.+--+--   If no matching starting state is found, a 'ValidationError' is thrown.+--   If the database is already in the state specified by the end state of the+--   final step, no migration is performed.+--+--   Note that when looking for a matching starting state, index methods for+--   indexed columns are not taken into account. Two columns @c1@ and @c2@ are+--   considered to be identical if @c1@ is indexed with index method @foo@ and+--   @c2@ is indexed with index method @bar@.+autoMigrate :: MonadSelda m+            => Bool -- ^ Enforce foreign keys during migration?+            -> [MigrationStep] -- ^ Migration steps to perform.+            -> m ()+autoMigrate _ [] = do+  return ()+autoMigrate fks steps = wrap fks $ do+    diffs <- sequence finalState+    when (any (/= TableOK) diffs) $ do+      steps' <- reverse <$> calculateSteps revSteps+      mapM_ performStep steps'+  where+    revSteps = reverse steps+    finalState = [diffTable to | Migration _ to _ <- head revSteps]++    calculateSteps (step:ss) = do+      diffs <- mapM (\(Migration from _ _) -> diffTable from) step+      if all (== TableOK) diffs+        then return [step]+        else (step:) <$> calculateSteps ss+    calculateSteps [] = do+      throwM $ ValidationError "no starting state matches the current state of the database"++    performStep = mapM_ (\(Migration t1 t2 upg) -> migrateInternal t1 t2 upg)++-- | Workhorse for migration.+--   Is NOT performed as a transaction, so exported functions need to+--   properly wrap calls this function.+migrateInternal :: (MonadSelda m, Relational a, Relational b)+                => Table a+                -> Table b+                -> (Row s a -> Query s (Row s b))+                -> m ()+migrateInternal t1 t2 upg = do+    validateTable t1+    validateSchema t2+    backend <- seldaBackend+    createTable t2'+    void . queryInto t2' $ select t1 >>= upg+    void . liftIO $ runStmt backend (dropQuery (tableName t1)) []+    void . liftIO $ runStmt backend renameQuery []+  where+    t2' = t2 {tableName = mkTableName newName} `asTypeOf` t2+    newName = mconcat ["__selda_migration_", rawTableName (tableName t2)]+    renameQuery = mconcat+      [ "ALTER TABLE ", newName+      , " RENAME TO ", fromTableName (tableName t2), ";"+      ]+    dropQuery t = mconcat ["DROP TABLE ", fromTableName t, ";"]
src/Database/Selda/Prepared.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE TypeFamilies, FlexibleInstances, ScopedTypeVariables #-} {-# LANGUAGE MultiParamTypeClasses, FlexibleContexts #-}+{-# LANGUAGE UndecidableInstances #-} -- | Building and executing prepared statements. module Database.Selda.Prepared (Preparable, Prepare, prepared) where import Database.Selda.Backend.Internal@@ -15,6 +16,7 @@ import Data.IORef import Data.Proxy import Data.Text (Text)+import Data.Typeable import System.IO.Unsafe  data Placeholder = Placeholder Int@@ -58,7 +60,7 @@ instance (SqlType a, Prepare q b) => Prepare q (a -> b) where   mkFun ref sid qry ps x = mkFun ref sid qry (param x : ps) -instance (MonadSelda m, a ~ Res (ResultT q), Result (ResultT q)) =>+instance (Typeable a, MonadSelda m, a ~ Res (ResultT q), Result (ResultT q)) =>          Prepare q (m [a]) where   -- This function uses read/writeIORef instead of atomicModifyIORef.   -- For once, this is actually safe: the IORef points to a single compiled@@ -109,11 +111,11 @@           _ -> do             res <- runPrepared backend hdl ps             cache (stmtTables stm) key res-            return $ map (toRes (Proxy :: Proxy (ResultT q))) (snd res)+            return $ map (buildResult (Proxy :: Proxy (ResultT q))) (snd res)  instance (SqlType a, Preparable b) => Preparable (Col s a -> b) where   mkQuery n f ts = mkQuery (n+1) (f x) (sqlType (Proxy :: Proxy a) : ts)-    where x = C $ Lit $ LCustom (throw (Placeholder n) :: Lit a)+    where x = One $ Lit $ LCustom (throw (Placeholder n) :: Lit a)  instance Result a => Preparable (Query s a) where   mkQuery _ q types = do
src/Database/Selda/Query.hs view
@@ -2,44 +2,43 @@ -- | Query monad and primitive operations. module Database.Selda.Query   ( select, selectValues, Database.Selda.Query.distinct-  , restrict, groupBy, limit, order+  , restrict, groupBy, limit, order, orderRandom   , aggregate, leftJoin, innerJoin   ) where import Data.Maybe (isNothing) import Database.Selda.Column+import Database.Selda.Generic import Database.Selda.Inner import Database.Selda.Query.Type import Database.Selda.SQL as SQL+import Database.Selda.SqlType (SqlType) import Database.Selda.Table import Database.Selda.Transform import Control.Monad.State.Strict import Unsafe.Coerce --- | Query the given table. Result is returned as an inductive tuple, i.e.---   @first :*: second :*: third <- query tableOfThree@.-select :: Columns (Cols s a) => Table a -> Query s (Cols s a)+-- | Query the given table.+select :: Relational a => Table a -> Query s (Row s a) select (Table name cs _) = Query $ do-    rns <- mapM (rename . Some . Col) cs'-    st <- get-    put $ st {sources = sqlFrom rns (TableName name) : sources st}-    return $ toTup [n | Named n _ <- rns]-  where-    cs' = map colName cs+  rns <- renameAll $ map colExpr cs+  st <- get+  put $ st {sources = sqlFrom rns (TableName name) : sources st}+  return $ Many (map hideRenaming rns)  -- | Query an ad hoc table of type @a@. Each element in the given list represents --   one row in the ad hoc table.-selectValues :: (Insert a, Columns (Cols s a)) => [a] -> Query s (Cols s a)+selectValues :: Relational a => [a] -> Query s (Row s a) selectValues [] = Query $ do   st <- get   put $ st {sources = sqlFrom [] EmptyTable : sources st}-  return $ toTup (repeat "NULL")+  return $ Many (repeat (Untyped $ Col "NULL")) selectValues (row:rows) = Query $ do     names <- mapM (const freshName) firstrow     let rns = [Named n (Col n) | n <- names]         row' = mkFirstRow names     s <- get     put $ s {sources = sqlFrom rns (Values row' rows') : sources s}-    return $ toTup [n | Named n _ <- rns]+    return $ Many (map hideRenaming rns)   where     firstrow = map defToVal $ params row     mkFirstRow ns =@@ -52,7 +51,7 @@  -- | Restrict the query somehow. Roughly equivalent to @WHERE@. restrict :: Col s Bool -> Query s ()-restrict (C p) = Query $ do+restrict (One p) = Query $ do     st <- get     put $ case sources st of       [] ->@@ -87,18 +86,18 @@ -- > -- SELECT COUNT(name) AS c, address FROM housing GROUP BY name HAVING c > 1 -- > -- > numPpl = do--- >   (num_tenants :*: address) <- aggregate $ do--- >     (_ :*: address) <- select housing--- >     groupBy address--- >     return (count address :*: some address)+-- >   (num_tenants :*: theAddress) <- aggregate $ do+-- >     h <- select housing+-- >     theAddress <- groupBy (h ! address)+-- >     return (count (h ! address) :*: theAddress) -- >  restrict (num_tenants .> 1)--- >  return (num_tenants :*: address)+-- >  return (num_tenants :*: theAddress) aggregate :: (Columns (AggrCols a), Aggregates a)           => Query (Inner s) a           -> Query s (AggrCols a) aggregate q = Query $ do   (gst, aggrs) <- isolate q-  cs <- mapM rename $ unAggrs aggrs+  cs <- renameAll $ unAggrs aggrs   let sql = (sqlFrom cs (Product [state2sql gst])) {groups = groupCols gst}   modify $ \st -> st {sources = sql : sources st}   pure $ toTup [n | Named n _ <- cs]@@ -147,12 +146,12 @@          -> Query s a' someJoin jointype check q = Query $ do   (join_st, res) <- isolate q-  cs <- mapM rename $ fromTup res+  cs <- renameAll $ fromTup res   st <- get   let nameds = [n | Named n _ <- cs]       left = state2sql st       right = sqlFrom cs (Product [state2sql join_st])-      C on = check $ toTup nameds+      One on = check $ toTup nameds       outCols = [Some $ Col n | Named n _ <- cs] ++ allCols [left]   put $ st {sources = [sqlFrom outCols (Join jointype on left right)]}   pure $ toTup nameds@@ -164,11 +163,11 @@ --   how many people have a pet at home: -- -- > aggregate $ do--- >   (name :*: pet_name) <- select people--- >   name' <- groupBy name--- >   return (name' :*: count(pet_name) .> 0)-groupBy :: Col (Inner s) a -> Query (Inner s) (Aggr (Inner s) a)-groupBy (C c) = Query $ do+-- >   person <- select people+-- >   name' <- groupBy (person ! name)+-- >   return (name' :*: count(person ! pet_name) .> 0)+groupBy :: SqlType a => Col (Inner s) a -> Query (Inner s) (Aggr (Inner s) a)+groupBy (One c) = Query $ do   st <- get   put $ st {groupCols = Some c : groupCols st}   return (Aggr c)@@ -193,12 +192,12 @@ --   To get a list of persons sorted primarily on age and secondarily on name: -- -- > peopleInAgeAndNameOrder = do--- >   (name :*: age) <- select people--- >   order name ascending--- >   order age ascending--- >   return name+-- >   person <- select people+-- >   order (person ! name) ascending+-- >   order (person ! age) ascending+-- >   return (person ! name) -----   For a table @["Alice" :*: 20, "Bob" :*: 20, "Eve" :*: 18]@, this query+--   For a table @[("Alice", 20), ("Bob", 20), ("Eve", 18)]@, this query --   will always return @["Eve", "Alice", "Bob"]@. -- --   The reason for later orderings taking precedence and not the other way@@ -207,13 +206,17 @@ --   is buried somewhere deep in an earlier query. --   However, the ordering must always be stable, to ensure that previous --   calls to order are not simply erased.-order :: Col s a -> Order -> Query s ()-order (C c) o = Query $ do+order :: SqlType a => Col s a -> Order -> Query s ()+order (One c) o = Query $ do   st <- get   case sources st of     [sql] -> put st {sources = [sql {ordering = (o, Some c) : ordering sql}]}-    ss    -> put st {sources = [sql {ordering = [(o,Some c)]}]}+    ss    -> put st {sources = [sql {ordering = [(o, Some c)]}]}       where sql = sqlFrom (allCols ss) (Product ss)++-- | Sort the result rows in random order.+orderRandom :: Query s ()+orderRandom = order (One (NulOp (Fun0 "RANDOM") :: Exp SQL Int)) Asc  -- | Remove all duplicates from the result set. distinct :: Query s a -> Query s a
src/Database/Selda/Query/Type.hs view
@@ -1,7 +1,9 @@-{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-}+{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings, CPP #-} module Database.Selda.Query.Type where import Control.Monad.State.Strict+#if !MIN_VERSION_base(4, 11, 0) import Data.Monoid+#endif import Data.Text (pack) import Database.Selda.SQL import Database.Selda.Column@@ -57,18 +59,19 @@   , nameScope  = scope   } +renameAll :: [UntypedCol sql] -> State GenState [SomeCol sql]+renameAll = fmap concat . mapM rename+ -- | Generate a unique name for the given column.-rename :: SomeCol sql -> State GenState (SomeCol sql)-rename (Some col) = do+rename :: UntypedCol sql -> State GenState [SomeCol sql]+rename (Untyped col) = do     n <- freshId-    return $ Named (newName n) col+    return [Named (newName n) col]   where     newName ns =       case col of         Col n -> addColSuffix n $ "_" <> pack (show ns)         _     -> mkColName $ "tmp_" <> pack (show ns)-rename col@(Named _ _) = do-  return col  -- | Get a guaranteed unique identifier. freshId :: State GenState Name
src/Database/Selda/SQL.hs view
@@ -1,14 +1,14 @@ {-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables, RecordWildCards #-} {-# LANGUAGE TypeOperators, FlexibleInstances, UndecidableInstances #-}-{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RankNTypes, CPP #-} -- | SQL AST and parameters for prepared statements. module Database.Selda.SQL where import Database.Selda.Exp import Database.Selda.SqlType import Database.Selda.Types-import Control.Exception+#if !MIN_VERSION_base(4, 11, 0) import Data.Monoid hiding (Product)-import System.IO.Unsafe+#endif  -- | A source for an SQL query. data SqlSource@@ -85,31 +85,3 @@ -- | The SQL type of the given parameter. paramType :: Param -> SqlTypeRep paramType (Param p) = litType p---- | Exception indicating the use of a default value.---   If any values throwing this during evaluation of @param xs@ will be---   replaced by their default value.-data DefaultValueException = DefaultValueException-  deriving Show-instance Exception DefaultValueException---- | An inductive tuple of Haskell-level values (i.e. @Int :*: Maybe Text@)---   which can be inserted into a table.-class Insert a where-  params :: a -> [Either Param Param]-instance (SqlType a, Insert b) => Insert (a :*: b) where-  params (a :*: b) = unsafePerformIO $ do-    res <- try $ return $! a-    return $ case res of-      Right a' ->-        Right (Param (mkLit a')) : params b-      Left DefaultValueException ->-        Left (Param (defaultValue :: Lit a)) : params b-instance {-# OVERLAPPABLE #-} SqlType a => Insert a where-  params a = unsafePerformIO $ do-    res <- try $ return $! a-    return $ case res of-      Right a' ->-        [Right $ Param (mkLit a')]-      Left DefaultValueException ->-        [Left $ Param (defaultValue :: Lit a)]
src/Database/Selda/SQL/Print.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE GADTs, OverloadedStrings #-}+{-# LANGUAGE GADTs, OverloadedStrings, CPP #-} -- | Pretty-printing for SQL queries. For some values of pretty. module Database.Selda.SQL.Print where import Database.Selda.Column@@ -9,7 +9,9 @@ import Database.Selda.Types import Control.Monad.State import Data.List+#if !MIN_VERSION_base(4, 11, 0) import Data.Monoid hiding (Product)+#endif import Data.Text (Text) import qualified Data.Text as Text @@ -216,11 +218,11 @@   pure $ Cfg.ppTypePK c t  ppCol :: Exp SQL a -> PP Text-ppCol (TblCol xs)    = error $ "compiler bug: ppCol saw TblCol: " ++ show xs ppCol (Col name)     = pure (fromColName name) ppCol (Lit l)        = ppLit l ppCol (BinOp op a b) = ppBinOp op a b ppCol (UnOp op a)    = ppUnOp op a+ppCol (NulOp a)      = ppNulOp a ppCol (Fun2 f a b)   = do   a' <- ppCol a   b' <- ppCol b@@ -243,6 +245,9 @@   x' <- ppCol x   q' <- ppSql q   pure $ mconcat [x', " IN (", q', ")"]++ppNulOp :: NulOp a -> PP Text+ppNulOp (Fun0 f) = pure $ f <> "()"  ppUnOp :: UnOp a b -> Exp SQL a -> PP Text ppUnOp op c = do
src/Database/Selda/SQL/Print/Config.hs view
@@ -41,6 +41,10 @@     --   has more than this many columns, you should really rethink     --   your database design.   , ppMaxInsertParams :: Maybe Int++    -- | @CREATE INDEX@ suffix to indicate that the index should use the given+    --   index method.+  , ppIndexMethodHook :: IndexMethod -> Text   }  -- | Default settings for pretty-printing.@@ -58,6 +62,7 @@     , ppColAttrsHook = \_ ats _ -> T.unwords $ map defColAttr ats     , ppAutoIncInsert = "NULL"     , ppMaxInsertParams = Nothing+    , ppIndexMethodHook = const ""     }  -- | Default compilation for SQL types.@@ -79,3 +84,4 @@ defColAttr Required      = "NOT NULL" defColAttr Optional      = "NULL" defColAttr Unique        = "UNIQUE"+defColAttr (Indexed _)   = ""
src/Database/Selda/Selectors.hs view
@@ -1,93 +1,116 @@ {-# LANGUAGE ScopedTypeVariables, TypeFamilies, MultiParamTypeClasses #-} {-# LANGUAGE TypeOperators, UndecidableInstances, FlexibleInstances #-} {-# LANGUAGE FlexibleContexts, RankNTypes, AllowAmbiguousTypes, GADTs #-}-module Database.Selda.Selectors where-import Database.Selda.Table+{-# LANGUAGE DeriveGeneric, CPP #-}+module Database.Selda.Selectors+  ( Assignment ((:=)), Selected, Selector, Source, Selectors, GSelectors+  , (!), with, ($=)+  , selectorsFor, selectorIndex+  ) where+import Control.Monad.State.Strict+import Database.Selda.SqlType import Database.Selda.Types import Database.Selda.Column-import Data.Dynamic import Data.List (foldl') import Data.Proxy+import GHC.Generics hiding (Selector, (:*:))+import qualified GHC.Generics as G import Unsafe.Coerce --- | Get the value at the given index from the given inductive tuple.-(!)  :: forall s t a. ToDyn (Cols () t) => Cols s t -> Selector t a -> Col s a-tup ! (Selector n) = unsafeCoerce (unsafeToList (toU tup) !! n)-  where toU = unsafeCoerce :: Cols s t -> Cols () t+-- | The result of a '(!)' operation.+--   If either the source row, the column to extract,+--   or both, is nullable, the result is also nullable.+type family Selected a b where+  Selected (Maybe a) (Maybe b) = Maybe b+  Selected (Maybe a) b         = Maybe b+  Selected a         b         = b -upd :: forall s t. (ToDyn (Cols () t))-     => Cols s t -> Assignment s t -> Cols s t-upd tup (Selector n := x) =-    fromU . unsafeFromList $ replace (unsafeToList $ toU tup) (unsafeCoerce x)+-- | The source type of a '(!)' operation.+type family Source a where+  Source (Maybe a) = a+  Source a         = a++-- | Extract the given column from the given row.+--   Extracting a value from a nullable column will yield a nullable value.+--   In other words, this operator is null-coalescing.+(!) :: SqlType b => Row s a -> Selector (Source a) b -> Col s (Selected a b)+(Many xs) ! (Selector i) = case xs !! i of Untyped x -> One (unsafeCoerce x)++upd :: Row s a -> Assignment s a -> Row s a+upd (Many xs) (Selector i := (One x')) =+  case splitAt i xs of+    (left, _:right) -> Many (left ++ Untyped x' : right)+    _               -> error "BUG: too few columns in row!"+upd (Many xs) (Modify (Selector i) f) =+  case splitAt i xs of+    (left, Untyped x:right) -> Many (left ++ f' (unsafeCoerce x) : right)+    _                       -> error "BUG: too few columns in row!"   where-    toU = unsafeCoerce :: Cols s t -> Cols () t-    fromU = unsafeCoerce :: Cols () t -> Cols s t-    replace xs x' =-      case splitAt n xs of-        (left, _:right) -> left ++ x' : right-        _               -> error "impossible"+    f' x = case f (One x) of+      One y -> Untyped y  -- | A selector-value assignment pair.-data Assignment s t where+data Assignment s a where+#if MIN_VERSION_base(4, 9, 0)+  -- | Set the given column to the given value.+#endif   (:=) :: Selector t a -> Col s a -> Assignment s t++#if MIN_VERSION_base(4, 9, 0)+  -- | Modify the given column by the given function.+#endif+  Modify :: Selector t a -> (Col s a -> Col s a) -> Assignment s t infixl 2 := +-- | Apply the given function to the given column.+($=) :: Selector t a -> (Col s a -> Col s a) -> Assignment s t+($=) = Modify+infixl 2 $=+ -- | For each selector-value pair in the given list, on the given tuple, --   update the field pointed out by the selector with the corresponding value.-with :: forall s t. (ToDyn (Cols () t))-     => Cols s t -> [Assignment s t] -> Cols s t+with :: Row s a -> [Assignment s a] -> Row s a with = foldl' upd  -- | A column selector. Column selectors can be used together with the '!' and---   'with' functions to get and set values on inductive tuples, or to indicate+--   'with' functions to get and set values on rows, or to specify --   foreign keys.-data Selector t a = Selector Int+newtype Selector t a = Selector {selectorIndex :: Int} --- | The inductive tuple of selectors for a table of type @a@.-type family Selectors t a where-  Selectors t (a :*: b) = (Selector t a :*: Selectors t b)-  Selectors t a         = Selector t a+-- | Generate selectors for the given type.+selectorsFor :: forall r. GSelectors r (Rep r) => Proxy r -> Selectors r+selectorsFor = flip evalState 0 . mkSel (Proxy :: Proxy (Rep r)) --- | Generate selector functions for the given table.---   Selectors can be used to access the fields of a query result tuple, avoiding---   the need to pattern match on the entire tuple.------ > tbl :: Table (Int :*: Text)--- > tbl = table "foo" $ required "bar" :*: required "baz"--- > (tblBar :*: tblBaz) = selectors tbl--- >--- > q :: Query s Text--- > q = tblBaz <$> select tbl-selectors :: forall a. HasSelectors a a => Table a -> Selectors a a-selectors _ = mkSel (Proxy :: Proxy a) 0 (Proxy :: Proxy a)+-- | An inductive tuple of selectors for the given relation.+type Selectors r = Sels r (Rep r) +type family Sels t f where+  Sels t ((a G.:*: b) G.:*: c) = Sels t (a G.:*: (b G.:*: c))+  Sels t (a G.:*: b)           = Sels t a :*: Sels t b+  Sels t (M1 x y f)            = Sels t f+  Sels t (K1 i a)              = Selector t a+ -- | Any table type that can have selectors generated.-class HasSelectors t a where-  mkSel :: Proxy t -> Int -> Proxy a -> Selectors t a+class GSelectors t (f :: * -> *) where+  mkSel :: Proxy f -> Proxy t -> State Int (Sels t f) -instance (Typeable a, HasSelectors t b) => HasSelectors t (a :*: b) where-  mkSel p n _ = (Selector n :*: mkSel p (n+1) (Proxy :: Proxy b))+instance SqlType a => GSelectors t (K1 i a) where+  mkSel _ _ = Selector <$> state (\n -> (n, n+1)) -instance {-# OVERLAPPABLE #-} (Selectors t a ~ Selector t a) =>-         HasSelectors t a where-  mkSel _ n _ = Selector n+instance (GSelectors t f, Sels t f ~ Sels t (M1 x y f)) =>+         GSelectors t (M1 x y f) where+  mkSel _ = mkSel (Proxy :: Proxy f) --- | A pair of the table with the given name and columns, and all its selectors.---   For example:------ > tbl :: Table (Int :*: Text)--- > (tbl, tblBar :*: tblBaz)--- >   =  tableWithSelectors "foo"--- >   $  required "bar"--- >   :*: required "baz"--- >--- > q :: Query s Text--- > q = tblBaz <$> select tbl-tableWithSelectors :: forall a. (TableSpec a, HasSelectors a a)-                   => TableName-                   -> ColSpecs a-                   -> (Table a, Selectors a a)-tableWithSelectors name cs = (t, s)-  where-    t = table name cs-    s = selectors t+instance GSelectors t (a G.:*: (b G.:*: c)) =>+         GSelectors t ((a G.:*: b) G.:*: c) where+  mkSel _ = mkSel (Proxy :: Proxy (a G.:*: (b G.:*: c)))++instance {-# OVERLAPPABLE #-}+  ( GSelectors t a+  , GSelectors t b+  , Sels t (a G.:*: b) ~ (Sels t a :*: Sels t b)+  ) => GSelectors t (a G.:*: b) where+    mkSel _ p = do+      x <- mkSel (Proxy :: Proxy a) p+      xs <- mkSel (Proxy :: Proxy b) p+      return (x :*: xs)
+ src/Database/Selda/SqlRow.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE UndecidableInstances, FlexibleInstances, FlexibleContexts #-}+{-# LANGUAGE TypeOperators, DefaultSignatures, ScopedTypeVariables, CPP #-}+#if MIN_VERSION_base(4, 10, 0)+{-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-}+#endif+module Database.Selda.SqlRow+  ( SqlRow (..), ResultReader+  , runResultReader, next+  ) where+import Control.Monad.State.Strict+import Database.Selda.SqlType+import Data.Typeable+import GHC.Generics++newtype ResultReader a = R (State [SqlValue] a)+  deriving (Functor, Applicative, Monad)++runResultReader :: ResultReader a -> [SqlValue] -> a+runResultReader (R m) = evalState m++next :: ResultReader SqlValue+next = R . state $ \s -> (head s, tail s)++class Typeable a => SqlRow a where+  -- | Read the next, potentially composite, result from a stream of columns.+  nextResult :: ResultReader a+  default nextResult :: (Generic a, GSqlRow (Rep a)) => ResultReader a+  nextResult = to <$> gNextResult++  -- | The number of nested columns contained in this type.+  nestedCols :: Proxy a -> Int+  default nestedCols :: (Generic a, GSqlRow (Rep a)) => Proxy a -> Int+  nestedCols _ = gNestedCols (Proxy :: Proxy (Rep a))+++-- * Generic derivation for SqlRow+class GSqlRow f where+  gNextResult :: ResultReader (f x)+  gNestedCols :: Proxy f -> Int++instance SqlType a => GSqlRow (K1 i a) where+  gNextResult = K1 <$> fromSql <$> next+  gNestedCols _ = 1++instance GSqlRow f => GSqlRow (M1 c i f) where+  gNextResult = M1 <$> gNextResult+  gNestedCols _ = gNestedCols (Proxy :: Proxy f)++instance (GSqlRow a, GSqlRow b) => GSqlRow (a :*: b) where+  gNextResult = liftM2 (:*:) gNextResult gNextResult+  gNestedCols _ = gNestedCols (Proxy :: Proxy a) + gNestedCols (Proxy :: Proxy b)+++-- * Various instances+instance SqlRow a => SqlRow (Maybe a) where+  nextResult = do+      xs <- R get+      if all isNull (take (nestedCols (Proxy :: Proxy a)) xs)+        then return Nothing+        else Just <$> nextResult+    where+      isNull SqlNull = True+      isNull _       = False+  nestedCols _ = nestedCols (Proxy :: Proxy a)++instance+  ( Typeable (a, b)+  , GSqlRow (Rep (a, b))+  ) => SqlRow (a, b)+instance+  ( Typeable (a, b, c)+  , GSqlRow (Rep (a, b, c))+  ) => SqlRow (a, b, c)+instance+  ( Typeable (a, b, c, d)+  , GSqlRow (Rep (a, b, c, d))+  ) => SqlRow (a, b, c, d)+instance+  ( Typeable (a, b, c, d, e)+  , GSqlRow (Rep (a, b, c, d, e))+  ) => SqlRow (a, b, c, d, e)+instance+  ( Typeable (a, b, c, d, e, f)+  , GSqlRow (Rep (a, b, c, d, e, f))+  ) => SqlRow (a, b, c, d, e, f)+instance+  ( Typeable (a, b, c, d, e, f, g)+  , GSqlRow (Rep (a, b, c, d, e, f, g))+  ) => SqlRow (a, b, c, d, e, f, g)
src/Database/Selda/SqlType.hs view
@@ -1,10 +1,11 @@ {-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables, FlexibleInstances #-}-{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE UndecidableInstances, DefaultSignatures #-} -- | Types representable in Selda's subset of SQL. module Database.Selda.SqlType   ( SqlType (..), SqlEnum (..)-  , Lit (..), RowID, SqlValue (..), SqlTypeRep (..)-  , invalidRowId, isInvalidRowId, unsafeRowId, fromRowId+  , Lit (..), RowID, ID (..), SqlValue (..), SqlTypeRep (..)+  , invalidRowId, isInvalidRowId, toRowId, fromRowId+  , toId, invalidId, isInvalidId   , compLit, litType   , sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat   ) where@@ -47,6 +48,8 @@ class Typeable a => SqlType a where   -- | Create a literal of this type.   mkLit :: a -> Lit a+  default mkLit :: (Typeable a, SqlEnum a) => a -> Lit a+  mkLit = LCustom . LText . toText    -- | The SQL representation for this type.   sqlType :: Proxy a -> SqlTypeRep@@ -54,9 +57,13 @@    -- | Convert an SqlValue into this type.   fromSql :: SqlValue -> a+  default fromSql :: (Typeable a, SqlEnum a) => SqlValue -> a+  fromSql = fromText . fromSql    -- | Default value when using 'def' at this type.   defaultValue :: Lit a+  default defaultValue :: (Typeable a, SqlEnum a) => Lit a+  defaultValue = LCustom $ mkLit (toText (minBound :: a))  -- | Any type that's bounded, enumerable and has a text representation, and --   thus representable as a Selda enumerable.@@ -187,16 +194,39 @@ isInvalidRowId (RowID n) = n < 0  -- | Create a row identifier from an integer.---   A row identifier created using this function is not guaranteed to be a---   valid row identifier.---   Do not use unless you are absolutely sure what you're doing.-unsafeRowId :: Int -> RowID-unsafeRowId = RowID+--   Use with caution, preferably only when reading user input.+toRowId :: Int -> RowID+toRowId = RowID  -- | Inspect a row identifier. fromRowId :: RowID -> Int fromRowId (RowID n) = n +-- | A typed row identifier.+--   Generic tables should use this instead of 'RowID'.+--   Use 'untyped' to erase the type of a row identifier, and @cast@ from the+--   "Database.Selda.Unsafe" module if you for some reason need to add a type+--   to a row identifier.+newtype ID a = ID {untyped :: RowID}+  deriving (Eq, Ord, Typeable)+instance Show (ID a) where+  show = show . untyped++-- | Create a typed row identifier from an integer.+--   Use with caution, preferably only when reading user input.+toId :: Int -> ID a+toId = ID . toRowId++-- | A typed row identifier which is guaranteed to not match any row in any+--   table.+invalidId :: ID a+invalidId = ID invalidRowId++-- | Is the given typed row identifier invalid? I.e. is it guaranteed to not+--   match any row in any table?+isInvalidId :: ID a -> Bool+isInvalidId = isInvalidRowId . untyped+ instance SqlType RowID where   mkLit (RowID n) = LCustom $ LInt n   sqlType _ = TRowID@@ -204,6 +234,12 @@   fromSql v          = error $ "fromSql: RowID column with non-int value: " ++ show v   defaultValue = mkLit invalidRowId +instance Typeable a => SqlType (ID a) where+  mkLit (ID n) = LCustom $ mkLit n+  sqlType _ = TRowID+  fromSql = ID . fromSql+  defaultValue = mkLit (ID invalidRowId)+ instance SqlType Int where   mkLit = LInt   sqlType _ = TInt@@ -286,8 +322,4 @@   fromSql x         = Just $ fromSql x   defaultValue = LNull -instance {-# OVERLAPPABLE #-} (Typeable a, SqlEnum a) => SqlType a where-  mkLit = LCustom . LText . toText-  sqlType _ = sqlType (Proxy :: Proxy Text)-  fromSql = fromText . fromSql-  defaultValue = LCustom $ mkLit (toText (minBound :: a))+instance SqlType Ordering
src/Database/Selda/Table.hs view
@@ -1,207 +1,194 @@-{-# LANGUAGE TypeOperators, TypeFamilies, OverloadedStrings #-}-{-# LANGUAGE UndecidableInstances, FlexibleInstances, ScopedTypeVariables #-}-{-# LANGUAGE MultiParamTypeClasses, FlexibleContexts #-}-{-# LANGUAGE CPP, DataKinds #-}--- | Selda table definition language.-module Database.Selda.Table where+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE TypeFamilies, TypeOperators, FlexibleInstances #-}+{-# LANGUAGE UndecidableInstances, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts, ScopedTypeVariables, ConstraintKinds #-}+{-# LANGUAGE GADTs, CPP, DeriveGeneric, DataKinds #-}+module Database.Selda.Table+  ( Attr (..), Table (..), Attribute+  , ColInfo (..), ColAttr (..), IndexMethod (..)+  , ForeignKey (..)+  , table, tableFieldMod, tableWithSelectors, selectors+  , primary, autoPrimary, untypedAutoPrimary, unique+  , index, indexUsing+  , tableExpr+  ) where+import Data.Text (Text)+#if MIN_VERSION_base(4, 10, 0)+import Data.Typeable+#else+import Data.Proxy+#endif import Database.Selda.Types+import Database.Selda.Selectors import Database.Selda.SqlType-import Control.Exception hiding (TypeError)-import GHC.Exts-import GHC.TypeLits-import Data.Dynamic-import Data.List (sort, group)-import Data.Monoid-import Data.Proxy-import Data.Text (unpack, intercalate, any)+import Database.Selda.Column (Row (..))+import Database.Selda.Generic+import Database.Selda.Table.Type+import Database.Selda.Table.Validation (snub) --- | An error occurred when validating a database table.---   If this error is thrown, there is a bug in your database schema, and the---   particular table that triggered the error is unusable.---   Since validation is deterministic, this error will be thrown on every---   consecutive operation over the offending table.------   Therefore, it is not meaningful to handle this exception in any way,---   just fix your bug instead.-data ValidationError = ValidationError String-  deriving (Show, Eq, Typeable)-instance Exception ValidationError+-- | A generic column attribute.+--   Essentially a pair or a record selector over the type @a@ and a column+--   attribute.+data Attr a where+  (:-) :: (a -> b) -> Attribute a b -> Attr a --- | A database table.---   Tables are parameterized over their column types. For instance, a table---   containing one string and one integer, in that order, would have the type---   @Table (Text :*: Int)@, and a table containing only a single string column---   would have the type @Table Text@.+-- | Generate a table from the given table name and list of column attributes.+--   All @Maybe@ fields in the table's type will be represented by nullable+--   columns, and all non-@Maybe@ fields fill be represented by required+--   columns.+--   For example: -----   Table and column names may contain any character except @\NUL@, and be---   non-empty. Column names must be unique per table.-data Table a = Table-  { -- | Name of the table. NOT guaranteed to be a valid SQL name.-    tableName :: TableName-    -- | All table columns.-    --   Invariant: the 'colAttrs' list of each column is sorted and contains-    --   no duplicates.-  , tableCols :: [ColInfo]-    -- | Does the given table have an auto-incrementing primary key?-  , tableHasAutoPK :: Bool-  }+-- > data Person = Person+-- >   { id   :: Int+-- >   , name :: Text+-- >   , age  :: Int+-- >   , pet  :: Maybe Text+-- >   }+-- >   deriving Generic+-- >+-- > people :: Table Person+-- > people = table "people" [name :- autoPrimary]+--+--   This example will create a table with the column types+--   @Int :*: Text :*: Int :*: Maybe Text@, where the first field is+--   an auto-incrementing primary key.+--+--   If the given type is not a record type, the column names will be+--   @col_1@, @col_2@, etc.+table :: forall a. Relational a+         => TableName+         -> [Attr a]+         -> Table a+table tn attrs = tableFieldMod tn attrs id -data ColInfo = ColInfo-  { colName  :: ColName-  , colType  :: SqlTypeRep-  , colAttrs :: [ColAttr]-  , colFKs   :: [(Table (), ColName)]+-- | Generate a table from the given table name,+--   a list of column attributes and a function+--   that maps from field names to column names.+--   Ex.:+--+-- > data Person = Person+-- >   { personId   :: Int+-- >   , personName :: Text+-- >   , personAge  :: Int+-- >   , personPet  :: Maybe Text+-- >   }+-- >   deriving Generic+-- >+-- > people :: Table Person+-- > people = tableFieldMod "people" [personName :- autoPrimaryGen] (stripPrefix "person")+--+--   This will create a table with the columns named+--   @Id@, @Name@, @Age@ and @Pet@.+tableFieldMod :: forall a. Relational a+                 => TableName+                 -> [Attr a]+                 -> (Text -> Text)+                 -> Table a+tableFieldMod tn attrs fieldMod = Table+  { tableName = tn+  , tableCols = map tidy cols+  , tableHasAutoPK = apk   }--newCol :: forall a. SqlType a => ColName -> ColSpec a-newCol name = ColSpec [ColInfo-  { colName  = name-  , colType  = sqlType (Proxy :: Proxy a)-  , colAttrs = []-  , colFKs   = []-  }]---- | A table column specification.-newtype ColSpec a = ColSpec {unCS :: [ColInfo]}---- | Any SQL type which is NOT nullable.-type family NonNull a :: Constraint where-#if MIN_VERSION_base(4, 9, 0)-  NonNull (Maybe a) = TypeError-    ( Text "Optional columns must not be nested, and" :<>:-      Text " required or primary key columns" :$$:-      Text "must not have option types."-    )-#else-  NonNull (Maybe a) = a ~ Maybe a-#endif-  NonNull a         = ()---- | Column attributes such as nullability, auto increment, etc.---   When adding elements, make sure that they are added in the order---   required by SQL syntax, as this list is only sorted before being---   pretty-printed.-data ColAttr = Primary | AutoIncrement | Required | Optional | Unique-  deriving (Show, Eq, Ord)+  where+    dummy = mkDummy+    cols = zipWith addAttrs [0..] (tblCols (Proxy :: Proxy a) fieldMod)+    apk = or [AutoIncrement `elem` as | _ :- Attribute as <- attrs]+    addAttrs n ci = ci+      { colAttrs = colAttrs ci ++ concat+          [ as+          | f :- Attribute as <- attrs+          , identify dummy f == n+          ]+      , colFKs = colFKs ci +++          [ thefk+          | f :- ForeignKey thefk <- attrs+          , identify dummy f == n+          ]+      } --- | A non-nullable column with the given name.-required :: (SqlType a, NonNull a) => ColName -> ColSpec a-required = addAttr Required . newCol+-- | Remove duplicate attributes.+tidy :: ColInfo -> ColInfo+tidy ci = ci {colAttrs = snub $ colAttrs ci} --- | A nullable column with the given name.-optional :: (SqlType a, NonNull a) => ColName -> ColSpec (Maybe a)-optional = addAttr Optional . newCol+-- | A pair of the table with the given name and columns, and all its selectors.+--   For example:+--+-- > tbl :: Table (Int :*: Text)+-- > (tbl, tblBar :*: tblBaz)+-- >   =  tableWithSelectors "foo"+-- >   $  required "bar"+-- >   :*: required "baz"+-- >+-- > q :: Query s Text+-- > q = tblBaz <$> select tbl+tableWithSelectors :: forall a. Relational a+                   => TableName+                   -> [Attr a]+                   -> (Table a, Selectors a)+tableWithSelectors name cs = (t, s)+  where+    t = table name cs+    s = selectors t --- | Marks the given column as the table's primary key.---   A table may only have one primary key; marking more than one key as---   primary will result in 'ValidationError' during validation.-primary :: (SqlType a, NonNull a) => ColName -> ColSpec a-primary = addAttr Primary . unique . required+-- | Generate selector functions for the given table.+--   Selectors can be used to access the fields of a query result tuple, avoiding+--   the need to pattern match on the entire tuple.+--+-- > tbl :: Table (Int :*: Text)+-- > tbl = table "foo" $ required "bar" :*: required "baz"+-- > (tblBar :*: tblBaz) = selectors tbl+-- >+-- > q :: Query s Text+-- > q = tblBaz <$> select tbl+selectors :: forall a. Relational a => Table a -> Selectors a+selectors _ = selectorsFor (Proxy :: Proxy a) --- | Automatically increment the given attribute if not specified during insert.---   Also adds the @PRIMARY KEY@ and @UNIQUE@ attributes on the column.-autoPrimary :: ColName -> ColSpec RowID-autoPrimary n = ColSpec [c {colAttrs = [Primary, AutoIncrement, Required, Unique]}]-  where ColSpec [c] = newCol n :: ColSpec RowID+-- | Some attribute that may be set on a column of type @c@, in a table of+--   type @t@.+data Attribute t c+  = Attribute [ColAttr]+  | ForeignKey (Table (), ColName) --- | Add a uniqueness constraint to the given column.---   Adding a uniqueness constraint to a column that is already implied to be---   unique, such as a primary key, is a no-op.-unique :: SqlType a => ColSpec a -> ColSpec a-unique = addAttr Unique+-- | A primary key which does not auto-increment.+primary :: Attribute t c+primary = Attribute [Primary, Required, Unique] --- | Add an attribute to a column. Not for public consumption.-addAttr :: SqlType a => ColAttr -> ColSpec a -> ColSpec a-addAttr attr (ColSpec [ci]) = ColSpec [ci {colAttrs = attr : colAttrs ci}]-addAttr _ _                 = error "impossible: ColSpec with several columns"+-- | Create an index on this column.+index :: Attribute t c+index = Attribute [Indexed Nothing] --- | An inductive tuple where each element is a column specification.-type family ColSpecs a where-  ColSpecs (a :*: b) = ColSpec a :*: ColSpecs b-  ColSpecs a         = ColSpec a+-- | Create an index using the given index method on this column.+indexUsing :: IndexMethod -> Attribute t c+indexUsing m = Attribute [Indexed (Just m)] --- | An inductive tuple forming a table specification.-class TableSpec a where-  mergeSpecs :: Proxy a -> ColSpecs a -> [ColInfo]-instance TableSpec b => TableSpec (a :*: b) where-  mergeSpecs _ (ColSpec a :*: b) = a ++ mergeSpecs (Proxy :: Proxy b) b-instance {-# OVERLAPPABLE #-} ColSpecs a ~ ColSpec a => TableSpec a where-  mergeSpecs _ (ColSpec a) = a+-- | An auto-incrementing primary key.+autoPrimary :: Attribute t (ID t)+autoPrimary = Attribute [Primary, AutoIncrement, Required, Unique] --- | A table with the given name and columns.-table :: forall a. TableSpec a => TableName -> ColSpecs a -> Table a-table name cs = Table-    { tableName = name-    , tableCols = tcs-    , tableHasAutoPK = Prelude.any ((AutoIncrement `elem`) . colAttrs) tcs-    }-  where-    tcs = map tidy $ mergeSpecs (Proxy :: Proxy a) cs+-- | An untyped auto-incrementing primary key.+--   You should really only use this for ad hoc tables, such as tuples.+untypedAutoPrimary :: Attribute t RowID+untypedAutoPrimary = Attribute [Primary, AutoIncrement, Required, Unique] --- | Remove duplicate attributes.-tidy :: ColInfo -> ColInfo-tidy ci = ci {colAttrs = snub $ colAttrs ci}+-- | A table-unique value.+unique :: Attribute t c+unique = Attribute [Unique] --- | Sort a list and remove all duplicates from it.-snub :: (Ord a, Eq a) => [a] -> [a]-snub = map head . soup+mkFK :: Table t -> Selector a b -> Attribute c d+mkFK (Table tn tcs tapk) sel =+  ForeignKey (Table tn tcs tapk, colName (tcs !! selectorIndex sel)) --- | Sort a list, then group all identical elements.-soup :: Ord a => [a] -> [[a]]-soup = group . sort+class ForeignKey a b where+  -- | A foreign key constraint referencing the given table and column.+  foreignKey :: Table t -> Selector t a -> Attribute self b --- | Ensure that there are no duplicate column names or primary keys.-validate :: TableName -> [ColInfo] -> [ColInfo]-validate name cis-  | null errs = cis-  | otherwise = throw $ ValidationError $ concat-      [ "validation of table ", unpack $ fromTableName name, " failed:"-      , "\n  "-      , unpack $ intercalate "\n  " errs-      ]-  where-    colIdents = map (fromColName . colName) cis-    allIdents = fromTableName name : colIdents-    errs = concat-      [ dupes-      , pkDupes-      , optionalRequiredMutex-      , nulIdents-      , emptyIdents-      , emptyTableName-      , nonPkFks-      ]-    emptyTableName-      | fromTableName name == "\"\"" = ["table name is empty"]-      | otherwise                    = []-    emptyIdents-      | Prelude.any (== "\"\"") colIdents =-        ["table has columns with empty names"]-      | otherwise =-        []-    nulIdents =-      [ "table or column name contains \\NUL: " <> n-      | n <- allIdents-      , Data.Text.any (== '\NUL') n-      ]-    dupes =-      ["duplicate column: " <> fromColName x | (x:_:_) <- soup $ map colName cis]-    pkDupes =-      ["multiple primary keys" | (Primary:_:_) <- soup $ concatMap colAttrs cis]-    nonPkFks =-      [ "column is used as a foreign key, but is not primary or unique: "-          <> fromTableName ftn <> "." <> fromColName fcn-      | ci <- cis-      , (Table ftn fcs _, fcn) <- colFKs ci-      , fc <- fcs-      , colName fc == fcn-      , not (Unique `elem` colAttrs fc)-      ]+instance ForeignKey a a where+  foreignKey = mkFK+instance ForeignKey (Maybe a) a where+  foreignKey = mkFK+instance ForeignKey a (Maybe a) where+  foreignKey = mkFK -    -- This should be impossible, but...-    optionalRequiredMutex =-      [ "BUG: column " <> fromColName (colName ci)-                       <> " is both optional and required"-      | ci <- cis-      , Optional `elem` colAttrs ci && Required `elem` colAttrs ci-      ]+-- | An expression representing the given table.+tableExpr :: Table a -> Row s a+tableExpr = Many . map colExpr . tableCols
src/Database/Selda/Table/Compile.hs view
@@ -1,12 +1,15 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings, CPP #-} -- | Generating SQL for creating and deleting tables. module Database.Selda.Table.Compile where import Database.Selda.Table+import Database.Selda.Table.Validation import Data.List ((\\), foldl')+#if !MIN_VERSION_base(4, 11, 0) import Data.Monoid+#endif import Data.Text (Text, intercalate, pack) import qualified Data.Text as Text-import Database.Selda.SQL hiding (params, param)+import Database.Selda.SQL hiding (param) import Database.Selda.SQL.Print.Config import Database.Selda.SqlType (SqlTypeRep(..)) import Database.Selda.Types@@ -14,23 +17,43 @@ data OnError = Fail | Ignore   deriving (Eq, Ord, Show) --- | Compile a @CREATE TABLE@ query from a table definition.-compileCreateTable :: PPConfig -> OnError -> Table a -> Text-compileCreateTable customColType ifex tbl = ensureValid `seq` mconcat-  [ "CREATE TABLE ", ifNotExists ifex, fromTableName (tableName tbl), "("-  , intercalate ", " (map (compileTableCol customColType) (tableCols tbl))-  , case allFKs of-      [] -> ""-      _  -> ", " <> intercalate ", " compFKs-  , ")"-  ]+-- | Compile a sequence of queries to create the given table, including indexes.+--   The first query in the sequence is always @CREATE TABLE@.+compileCreateTable :: PPConfig -> OnError -> Table a -> [Text]+compileCreateTable cfg ifex tbl =+    ensureValid `seq` (createTable : createIndexes)   where+    createTable = mconcat+      [ "CREATE TABLE ", ifNotExists ifex, fromTableName (tableName tbl), "("+      , intercalate ", " (map (compileTableCol cfg) (tableCols tbl))+      , case allFKs of+          [] -> ""+          _  -> ", " <> intercalate ", " compFKs+      , ")"+      ]+    createIndexes =+      [ compileCreateIndex cfg (tableName tbl) (colName col) mmethod+      | col <- tableCols tbl+      , Indexed mmethod <- colAttrs col+      ]     ifNotExists Fail   = ""     ifNotExists Ignore = "IF NOT EXISTS "     allFKs = [(colName ci, fk) | ci <- tableCols tbl, fk <- colFKs ci]     compFKs = zipWith (uncurry compileFK) allFKs [0..]-    ensureValid = validate (tableName tbl) (tableCols tbl)+    ensureValid = validateOrThrow (tableName tbl) (tableCols tbl) +-- | Compile a @CREATE INDEX@ query for the given index.+compileCreateIndex :: PPConfig -> TableName -> ColName -> Maybe IndexMethod -> Text+compileCreateIndex cfg tbl col mmethod = mconcat+  [ "CREATE INDEX "+  , fromColName $ addColPrefix col ("ix" <> rawTableName tbl <> "_")+  , " ON ", fromTableName tbl+  , case mmethod of+      Just method -> " " <> ppIndexMethodHook cfg method+      _           -> ""+  , " (", fromColName col, ")"+  ]+ -- | Compile a foreign key constraint. compileFK :: ColName -> (Table (), ColName) -> Int -> Text compileFK col (Table ftbl _ _, fcol) n = mconcat@@ -51,7 +74,7 @@     colAttrsHook = ppColAttrsHook cfg cty attrs (ppColAttrs cfg)     cty = colType ci     attrs = colAttrs ci-    ppType' +    ppType'       | cty == TRowID && [Primary, AutoIncrement] `areIn` attrs = ppTypePK       | otherwise = ppType     areIn x y = null (x \\ y)
− src/Database/Selda/Table/Foreign.hs
@@ -1,30 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Foreign key support.-module Database.Selda.Table.Foreign where-import Database.Selda.Selectors-import Database.Selda.Table-import Unsafe.Coerce---- | Add a foreign key constraint to the given column, referencing---   the column indicated by the given table and selector.---   If the referenced column is not a primary key or has a---   uniqueness constraint, a 'ValidationError' will be thrown---   during validation.-fk :: ColSpec a -> (Table t, Selector t a) -> ColSpec a-fk (ColSpec [c]) (tbl, Selector i) =-    ColSpec [c {colFKs = thefk : colFKs c}]-  where-    Table _ tcs _ = tbl-    thefk = (unsafeCoerce tbl, colName (tcs !! i))-fk _ _ =-  error "impossible: ColSpec with several columns"---- | Like 'fk', but for nullable foreign keys.-optFk :: ColSpec (Maybe a) -> (Table t, Selector t a) -> ColSpec (Maybe a)-optFk (ColSpec [c]) (tbl, Selector i) =-    ColSpec [c {colFKs = thefk : colFKs c}]-  where-    Table _ tcs _ = tbl-    thefk = (unsafeCoerce tbl, colName (tcs !! i))-optFk _ _ =-  error "impossible: ColSpec with several columns"
+ src/Database/Selda/Table/Type.hs view
@@ -0,0 +1,54 @@+module Database.Selda.Table.Type where+import Database.Selda.SqlType (SqlTypeRep)+import Database.Selda.SQL (SQL)+import Database.Selda.Types+import Database.Selda.Exp++-- | A database table, based on some Haskell data type.+--   Any single constructor type can form the basis of a table, as long as+--   it derives @Generic@ and all of its fields are instances of @SqlType@.+data Table a = Table+  { -- | Name of the table. NOT guaranteed to be a valid SQL name.+    tableName :: TableName++    -- | All table columns.+    --   Invariant: the 'colAttrs' list of each column is sorted and contains+    --   no duplicates.+  , tableCols :: [ColInfo]++    -- | Does the given table have an auto-incrementing primary key?+  , tableHasAutoPK :: Bool+  }++-- | A complete description of a database column.+data ColInfo = ColInfo+  { colName  :: ColName+  , colType  :: SqlTypeRep+  , colAttrs :: [ColAttr]+  , colFKs   :: [(Table (), ColName)]+  , colExpr  :: UntypedCol SQL+  }++-- | Column attributes such as nullability, auto increment, etc.+--   When adding elements, make sure that they are added in the order+--   required by SQL syntax, as this list is only sorted before being+--   pretty-printed.+data ColAttr+  = Primary+  | AutoIncrement+  | Required+  | Optional+  | Unique+  | Indexed (Maybe IndexMethod)+  deriving (Show, Eq, Ord)++-- | Method to use for indexing with 'indexedUsing'.+--   Index methods are ignored by the SQLite backend, as SQLite doesn't support+--   different index methods.+data IndexMethod+  = BTreeIndex+  | HashIndex+-- Omitted until the operator class business is sorted out+--  | GistIndex+--  | GinIndex+  deriving (Show, Eq, Ord)
+ src/Database/Selda/Table/Validation.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings, CPP #-}+module Database.Selda.Table.Validation where+import Control.Exception+import Data.List (group, sort)+import Data.Text (Text, any, intercalate, unpack)+import Data.Typeable+import Database.Selda.Table.Type+import Database.Selda.Types+#if !MIN_VERSION_base(4, 11, 0)+import Data.Monoid+#endif++-- | An error occurred when validating a database table.+--   If this error is thrown, there is a bug in your database schema, and the+--   particular table that triggered the error is unusable.+--   Since validation is deterministic, this error will be thrown on every+--   consecutive operation over the offending table.+--+--   Therefore, it is not meaningful to handle this exception in any way,+--   just fix your bug instead.+data ValidationError = ValidationError String+  deriving (Show, Eq, Typeable)+instance Exception ValidationError++-- | Ensure that there are no duplicate column names or primary keys.+--   Returns a list of validation errors encountered.+validate :: TableName -> [ColInfo] -> [Text]+validate name cis = errs+  where+    colIdents = map (fromColName . colName) cis+    allIdents = fromTableName name : colIdents+    errs = concat+      [ dupes+      , pkDupes+      , optionalRequiredMutex+      , nulIdents+      , emptyIdents+      , emptyTableName+      , nonPkFks+      ]+    emptyTableName+      | fromTableName name == "\"\"" = ["table name is empty"]+      | otherwise                    = []+    emptyIdents+      | Prelude.any (== "\"\"") colIdents =+        ["table has columns with empty names"]+      | otherwise =+        []+    nulIdents =+      [ "table or column name contains \\NUL: " <> n+      | n <- allIdents+      , Data.Text.any (== '\NUL') n+      ]+    dupes =+      ["duplicate column: " <> fromColName x | (x:_:_) <- soup $ map colName cis]+    pkDupes =+      ["multiple primary keys" | (Primary:_:_) <- soup $ concatMap colAttrs cis]+    nonPkFks =+      [ "column is used as a foreign key, but is not primary or unique: "+          <> fromTableName ftn <> "." <> fromColName fcn+      | ci <- cis+      , (Table ftn fcs _, fcn) <- colFKs ci+      , fc <- fcs+      , colName fc == fcn+      , not (Unique `elem` colAttrs fc)+      ]++    -- This should be impossible, but...+    optionalRequiredMutex =+      [ "BUG: column " <> fromColName (colName ci)+                       <> " is both optional and required"+      | ci <- cis+      , Optional `elem` colAttrs ci && Required `elem` colAttrs ci+      ]++-- | Return all columns of the given table if the table schema is valid,+--   otherwise throw a 'ValidationError'.+validateOrThrow :: TableName -> [ColInfo] -> [ColInfo]+validateOrThrow name cols =+  case validate name cols of+    []     -> cols+    errors -> throw $ ValidationError $ concat+      [ "validation of table `", unpack $ fromTableName name+      , "' failed:\n  "+      , unpack $ intercalate "\n  " errors+      ]++-- | Sort a list and remove all duplicates from it.+snub :: (Ord a, Eq a) => [a] -> [a]+snub = map head . soup++-- | Sort a list, then group all identical elements.+soup :: Ord a => [a] -> [[a]]+soup = group . sort
src/Database/Selda/Types.hs view
@@ -5,17 +5,16 @@ {-# LANGUAGE CPP #-} -- | Basic Selda types. module Database.Selda.Types-  ( (:*:)(..), Head, Append (..), (:++:), ToDyn (..), Tup (..)+  ( (:*:)(..), Head, Tup (..)   , first, second, third, fourth, fifth, sixth, seventh, eighth, ninth, tenth   , ColName, TableName-  , mkColName, mkTableName, addColSuffix, addColPrefix-  , fromColName, fromTableName+  , modColName, mkColName, mkTableName, addColSuffix, addColPrefix+  , fromColName, fromTableName, rawTableName   ) where import Data.Dynamic import Data.String import Data.Text (Text, replace, append) import GHC.Generics (Generic)-import Unsafe.Coerce  #ifndef NO_LOCALCACHE import Data.Hashable@@ -32,6 +31,10 @@ newtype TableName = TableName Text   deriving (Ord, Eq, Show, IsString) +-- | Modify the given column name using the given function.+modColName :: ColName -> (Text -> Text) -> ColName+modColName (ColName cn) f = ColName (f cn)+ -- | Add a prefix to a column name. addColPrefix :: ColName -> Text -> ColName addColPrefix (ColName cn) s = ColName $ Data.Text.append s cn@@ -48,6 +51,10 @@ fromTableName :: TableName -> Text fromTableName (TableName tn) = mconcat ["\"", escapeQuotes tn, "\""] +-- | Convert a table name into a string, without quotes.+rawTableName :: TableName -> Text+rawTableName (TableName tn) = escapeQuotes tn+ -- | Create a column name. mkColName :: Text -> ColName mkColName = ColName@@ -131,47 +138,3 @@ tenth :: Tup j => (a :*: b :*: c :*: d :*: e :*: f :*: g :*: h :*: i :*: j)       -> Head j tenth (_ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: j) = tupHead j---- | Normalized append of two inductive tuples.---   Note that this will flatten any nested inductive tuples.-type family a :++: b where-  (a :*: b) :++: c = a :*: (b :++: c)-  a         :++: b = a :*: b--class Append a b where-  app :: a -> b -> a :++: b-instance {-# OVERLAPPING #-} Append b c => Append (a :*: b) c where-  app (a :*: b) c = a :*: app b c-instance ((a :*: b) ~ (a :++: b)) => Append a b where-  app a b = a :*: b--data Unsafe = Unsafe Int--class Typeable a => ToDyn a where-  toDyns :: a -> [Dynamic]-  fromDyns :: [Dynamic] -> Maybe a-  -- | TODO: replace with safe coercions when that hits platform-1.-  unsafeToList :: a -> [Unsafe]-  -- | TODO: replace with safe coercions when that hits platform-1.-  unsafeFromList :: [Unsafe] -> a--instance (Typeable a, ToDyn b) => ToDyn (a :*: b) where-  toDyns (a :*: b) = toDyn a : toDyns b-  fromDyns (x:xs) = do-    x' <- fromDynamic x-    xs' <- fromDyns xs-    return (x' :*: xs')-  fromDyns _ = do-    Nothing-  unsafeToList (x :*: xs) = unsafeCoerce x : unsafeToList xs-  unsafeFromList (x : xs) = unsafeCoerce x :*: unsafeFromList xs-  unsafeFromList _        = error "too short list to unsafeFromList"--instance {-# OVERLAPPABLE #-} Typeable a => ToDyn a where-  toDyns a = [toDyn a]-  fromDyns [x] = fromDynamic x-  fromDyns _   = Nothing-  unsafeToList x = [unsafeCoerce x]-  unsafeFromList [x] = unsafeCoerce x-  unsafeFromList []  = error "too short list to unsafeFromList"-  unsafeFromList _   = error "too long list to unsafeFromList"
src/Database/Selda/Unsafe.hs view
@@ -2,13 +2,13 @@ -- | Unsafe operations giving the user unchecked low-level control over --   the generated SQL. module Database.Selda.Unsafe-  ( fun, fun2+  ( fun, fun2, fun0   , aggr   , cast-  , unsafeRowId+  , castAggr   ) where import Database.Selda.Column-import Database.Selda.Inner (aggr)+import Database.Selda.Inner (Aggr, aggr, liftAggr) import Database.Selda.SqlType import Data.Text (Text) import Data.Proxy@@ -18,6 +18,11 @@ cast :: forall s a b. SqlType b => Col s a -> Col s b cast = liftC $ Cast (sqlType (Proxy :: Proxy b)) +-- | Cast an aggregate to another type, using whichever coercion semantics+--   are used by the underlying SQL implementation.+castAggr :: forall s a b. SqlType b => Aggr s a -> Aggr s b+castAggr = liftAggr $ Cast (sqlType (Proxy :: Proxy b))+ -- | A unary operation. Note that the provided function name is spliced --   directly into the resulting SQL query. Thus, this function should ONLY --   be used to implement well-defined functions that are missing from Selda's@@ -28,3 +33,7 @@ -- | Like 'fun', but with two arguments. fun2 :: Text -> Col s a -> Col s b -> Col s c fun2 f = liftC2 (Fun2 f)++-- | Like 'fun', but with zero arguments.+fun0 :: Text -> Col s a+fun0 = One . NulOp . Fun0
+ src/Database/Selda/Validation.hs view
@@ -0,0 +1,202 @@+{-# LANGUAGE OverloadedStrings, TupleSections #-}+-- | Utilities for validating and inspecting Selda tables.+module Database.Selda.Validation+  ( TableDiff (..), ColumnDiff (..)+  , TableName, ColName, ColumnInfo, SqlTypeRep, columnInfo+  , showTableDiff, showColumnDiff+  , describeTable, diffTable, diffTables+  , validateTable, validateSchema+  ) where+import Control.Monad.Catch+import Data.List ((\\))+import Data.Maybe (catMaybes)+import Data.Text (pack, unpack)+import Database.Selda+import Database.Selda.Backend+import Database.Selda.Table.Type (tableName, tableCols)+import Database.Selda.Table.Validation (ValidationError (..), validateOrThrow)++-- | Are the given types compatible?+isCompatibleWith :: SqlTypeRep -> SqlTypeRep -> Bool+isCompatibleWith TRowID TInt = True+isCompatibleWith TInt TRowID = True+isCompatibleWith a b         = a == b++-- | Validate a table schema, and check it for consistency against the current+--   database.+--   Throws a 'ValidationError' if the schema does not validate, or if+--   inconsistencies were found.+validateTable :: MonadSelda m => Table a -> m ()+validateTable t = do+  validateSchema t+  diffs <- diffTable t+  case diffs of+    TableOK -> return ()+    errors  -> throwM $ ValidationError $ concat+      [ "error validating table ", unpack (fromTableName (tableName t)), ":\n"+      , show errors+      ]++-- | Ensure that the schema of the given table is valid.+--   Does not ensure consistency with the current database.+validateSchema :: MonadThrow m => Table a -> m ()+validateSchema t = validateOrThrow (tableName t) (tableCols t) `seq` return ()++-- | A description of the difference between a schema and its corresponding+--   database table.+data TableDiff+  = TableOK+  | TableMissing+  | InconsistentColumns [(ColName, [ColumnDiff])]+    deriving Eq+instance Show TableDiff where+  show = unpack . showTableDiff++-- | A description of the difference between a column in a Selda table and its+--   corresponding database column.+data ColumnDiff+  = ColumnMissing+  | ColumnPresent+  | NameMismatch ColName+  | UnknownType Text+  | TypeMismatch SqlTypeRep SqlTypeRep+  | PrimaryKeyMismatch Bool+  | AutoIncrementMismatch Bool+  | NullableMismatch Bool+  | UniqueMismatch Bool+  | ForeignKeyMissing TableName ColName+  | ForeignKeyPresent TableName ColName+  | IndexMissing+  | IndexPresent+    deriving Eq++instance Show ColumnDiff where+  show = unpack . showColumnDiff++-- | Pretty-print a table diff.+showTableDiff :: TableDiff -> Text+showTableDiff TableOK = "no inconsistencies detected"+showTableDiff TableMissing = "table does not exist"+showTableDiff (InconsistentColumns cols) = mconcat+  [ "table has inconsistent columns:\n"+  , mconcat (map showColDiffs cols)+  ]+  where+    showColDiffs (col, diffs) = mconcat+      [ "  ", fromColName col, ":\n"+      , mconcat (map showDiffs diffs)+      ]+    showDiffs diff = mconcat+      [ "    ", showColumnDiff diff, "\n"+      ]++-- | Pretty-print a column diff.+showColumnDiff :: ColumnDiff -> Text+showColumnDiff ColumnMissing =+  "column does not exist in database"+showColumnDiff ColumnPresent =+  "column exists in database even though it shouldn't"+showColumnDiff IndexMissing =+  "column does not have an index in the database, even though it should"+showColumnDiff IndexPresent =+  "column has an index in the database, even though it shouldn't"+showColumnDiff (NameMismatch n) =+  mconcat ["column is called ", fromColName n, " in database"]+showColumnDiff (UnknownType t) =+  mconcat ["column has incompatible type \"", t, "\" in database"]+showColumnDiff (TypeMismatch t1 t2) =+  mconcat [ "column should have type `", pack (show t1)+          , "', but actually has type `", pack (show t2)+          , "' in database"+          ]+showColumnDiff (ForeignKeyMissing tbl col) =+  mconcat [ "column should be a foreign key referencing column "+          , fromColName col, " of table ", fromTableName tbl+          , "', but isn't a foreign key in database"+          ]+showColumnDiff (ForeignKeyPresent tbl col) =+  mconcat [ "column is a foreign key referencing column "+          , fromColName col, " of table ", fromTableName tbl+          , ", in database, even though it shouldn't be"+          ]+showColumnDiff (PrimaryKeyMismatch dbval) =+  showBoolDiff dbval "primary key"+showColumnDiff (AutoIncrementMismatch dbval) =+  showBoolDiff dbval "auto-incrementing"+showColumnDiff (NullableMismatch dbval) =+  showBoolDiff dbval "nullable"+showColumnDiff (UniqueMismatch dbval) =+  showBoolDiff dbval "unique"++showBoolDiff :: Bool -> Text -> Text+showBoolDiff True what =+  mconcat ["column is ", what, " in database, even though it shouldn't be"]+showBoolDiff False what =+  mconcat ["column is not ", what, " in database, even though it should be"]++-- | Get a description of the table by the given name currently in the database.+describeTable :: MonadSelda m => TableName -> m [ColumnInfo]+describeTable tbl = do+  b <- seldaBackend+  liftIO $ getTableInfo b tbl++-- | Check the given table for consistency with the current database, returning+--   a description of all inconsistencies found.+--   The table schema itself is not validated beforehand.+diffTable :: MonadSelda m => Table a -> m TableDiff+diffTable tbl = do+  dbInfos <- describeTable (tableName tbl)+  return $ diffColumns (columnInfo tbl) dbInfos++-- | Compute the difference between the two given tables.+--   The first table is considered to be the schema, and the second the database.+diffTables :: Table a -> Table b -> TableDiff+diffTables schema db = diffColumns (columnInfo schema) (columnInfo db)++-- | Compute the difference between the columns of two tables.+--   The first table is considered to be the schema, and the second the database.+diffColumns :: [ColumnInfo] -> [ColumnInfo] -> TableDiff+diffColumns infos dbInfos =+    case ( zipWith diffColumn infos dbInfos+         , map colName infos \\ map colName dbInfos+         , map colName dbInfos \\ map colName infos) of+      ([], _, _) ->+        TableMissing+      (diffs, [], []) | all consistent diffs ->+        TableOK+      (diffs, missing, extras) ->+        InconsistentColumns $ concat+          [ filter (not . consistent) diffs+          , map (, [ColumnMissing]) missing+          , map (, [ColumnPresent]) extras+          ]+  where+    consistent (_, diffs) = null diffs+    diffColumn schema db = (colName schema, catMaybes+      ([ check colName NameMismatch+       , case colType db of+           Left typ ->+             Just (UnknownType typ)+           Right t | not (t `isCompatibleWith` schemaColType) ->+             Just (TypeMismatch schemaColType t)+           _ ->+             Nothing+       , check colIsPK PrimaryKeyMismatch+       , check colIsAutoIncrement AutoIncrementMismatch+       , check colIsNullable NullableMismatch+       , check colIsUnique UniqueMismatch+       ] ++ mconcat+       [ map (Just . uncurry ForeignKeyPresent)+             (colFKs schema \\ colFKs db)+       , map (Just . uncurry ForeignKeyMissing)+             (colFKs db \\ colFKs schema)+       ]))+      where+        Right schemaColType = colType schema+        check :: Eq a+              => (ColumnInfo -> a)+              -> (a -> ColumnDiff)+              -> Maybe ColumnDiff+        check f err+          | f schema == f db = Nothing+          | otherwise        = Just (err (f db))