Git Product home page Git Product logo

purescript-selda's Introduction

purescript-selda

CI

About

purescript-selda is an SQL library (eDSL) which allows a user to write type-safe queries.

  • Generated SQL is guaranteed to be correct by the type system.
  • It supports arbitrarily nested queries with capabilities of filtering, joins and aggregation.
  • We used standard monadic abstraction which supports writing queries in a linear, natural style.
  • Our main target is PostgreSQL though in the upcoming release (already on master) we add SQLite3 support (with ability to support other db backends).

Example Query

To declare a type for a SQL table (already created in the db) we write the following table definition:

people  Table (id  Int, name  String, age  Maybe Int)
people = Table { name: "people" }

Once we've defined the tables, we can write queries, e.g.

selectFrom people \{ id, name, age } → do
  { balance } ← leftJoin bankAccounts \b → id .== b.personId
  restrict $ id .> lit 1
  pure { id, balance }

Generated SQL for the above query:

SELECT people_0.id AS id, bank_accounts_1.balance AS balance
FROM people people_0
LEFT JOIN bank_accounts bank_accounts_1 ON ((people_0.id = bank_accounts_1.personId))
WHERE ((people_0.id > 1))

For a more gentle introduction and more examples please refer to the Step-by-Step Guide.

More Help

If you have any questions please don't hesitate to ask.
I'll be happy to help and provide any guidance if necessary.
Open an issue or hit me up directly (either on slack, forum or directly via email).

Install

Install postgresql-client's dependencies

npm install pg decimal.js

Info

  • Introductory Guide: End-to-End example: how to setup, write queries, use aggregation, deal with type errors and execute queries and inserts.
  • Advanced Guide: Custom Types and Expressions (litPG, Any, EForeign, custom PG functions), more flexible table definitions (Source, db schemas, set-returning functions) - unsafe escape hatches
  • Test Suite: For information about features, examples, usage, etc. refer to the test suite: Test.Common, Test.PG, Test.SQLite3. To run the tests, docker-compose up -d helps to prepare postgres database.
  • Documentation: Pursuit docs
  • My thesis

Credits

Supported by Lambda Terms

Inspired by selda

purescript-selda's People

Contributors

hjmtql avatar jordanmartinez avatar kamirus avatar paluh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

purescript-selda's Issues

Problematic writing generic queries with open rows

Writing such queries often requires providing type signatures including type class constraints.

The query below uses a combination of selectFrom and query1 that require complicated constraints. The table row is open thus these constraints need to persist - it is advised to write the 'top-level constraints' (FromTable in selectFrom and GenericQuery in query1) attached to those functions (in their signatures) and don't let the compiler unfold them.

To type-check the function below we need to provide the type signature ourself - or satisfy ourselves with concrete and closed table-row t and let the compiler solve these constraints immediately.

Please note that the query (returned by selectFrom) is defined separately in the where clause.
Somehow with the proper type signature, the function getUserByEmail does not type-check if we inline the query q.

This should be mentioned in the guide.

getUserByEmail
    t m i o
  . MonadSeldaPG m
   FromTable Unit t ( emailCol Unit String | i )
   GenericQuery BackendPGClass m 
      ( emailCol Unit String | i )
      ( emailString | o )
   Selda.Table t  String  m (Maybe { emailString | o })
getUserByEmail table email = query1 q
  where
    q = selectFrom table \r → do
      restrict $ r.email .== lit email
      pure r

CookBook and Advanced Guide

Add literate ps cookbook guide with a table of contents on the top of the file

Write about:

  • Custom Types (PG) - litPG - EForeign
  • ShowM for custom PG functions - Any
  • Overloaded binary expressions for Col and Aggr

Simplify PG query constraints

Change the internal FFI query function in pg-client to return an object instead of an array.
It would allow simpler parsing the output into the result record (e.g. use ReadForeign or codecs)

Auto field gets updated

Hi!
my userTable has

userTable :: Table ( bio  Maybe LongString , email  ShortString , id  Auto Int , image  Maybe LongString , password  LongString , username  ShortString )

I have tried to do an update

update :: Pool -> Raw -> UserId -> Aff (Either InputError User)
update pool raw id =
  withConnection pool
    ( \conn ->
        runSelda conn do
          S.update userTable
            (\r -> r.id .== litPG id)
            ( \r ->
                r
                  { bio = litPG raw.bio
                  , email = litPG raw.email
                  , image = litPG raw.image
                  , password = litPG raw.password
                  , username = litPG raw.username
                  }
            )
          S.query $ selectById id
    )
    >>= validate

And I get

Error: (ProgrammingError { code: "428C9", column: "", constraint: "", dataType: "", detail: "Column \"id\" is an identity column defined as GENERATED ALWAYS.", file: "rewriteHandler.c", hint: "", internalPosition: "", internalQuery: "", line: "827", message: "column \"id\" can only be updated to DEFAULT", position: "", routine: "rewriteTargetListIU", schema: "", severity: "ERROR", table: "", where_: "" })

logUpdate shows:

UPDATE "user" SET username = $2, password = $3, image = $4, id = id, email = $5, bio = $6 WHERE (id = $1)
[3,"jim","$2a$06$cfxV0SUgZU60hPdu9sSptOHDkG7VT2VjCxSKnhM0ivRb6abHepnDe",null,"[email protected]","I work at statefarm"]
logUpdate 
  forall t s r m.
  TableToColsWithoutAlias s t r =>
  GetCols r =>
  MonadEffect m =>
  Table t  ({ | r }  Col s Boolean)  ({ | r }  { | r })  m Unit
logUpdate table pred up = do
  let
    { strQuery, params } = showPG $ showUpdate table pred up
  log strQuery
  log $ unsafeStringify params
  log ""

I don't know the full functionality. Please correct me if I am doing things awfully complicated. It would be nice to have:

  • Easier generic logging of the update, insert ..., not me replicating every param for every command.
  • Update returning like insert returning.

Thanks.

Type error message in nested aggregate query with missing `aggregate` call

selectFrom_
  (selectFrom table \{ id } → do -- missing `aggregate $` before selectFrom
      pid ← groupBy id
      pure { pid })
  pure

results in an unclear error message

The inferred type

∀ t39 t62 t78
. Mapping OuterCols (Aggr (Inner t39) Int) t62
⇒ FoldingWithIndex ExtractCols (SProxy "pid") (Array (Tuple String ...)) t62 (Array (Tuple String ...))
⇒ MappingWithIndex SubQueryResult (SProxy "pid") t62 t78
⇒ FullQuery t39 { pid ∷ t78 }

has type variables which are not mentioned in the body of the type.
Consider adding a type annotation.

in value declaration ...
PureScript(AmbiguousTypeVariables)

Generated SQL in test cases

It would be nice to have generated SQL next to the test cases or pretty-printing it during test execution.
It needs discussion.

Consider using issue tracker + milestones + tags for project management

I think that current development model doesn't scale well and can be confusing for new contributors. I think that selda is starting to be useful in its current shape and we can start to release it.
I understand that there is still a lot of moving parts, but we have to decide on some core and plan all design experiments according to it.
I propose turning TODOs into issues and tagging them and also introducing milestones with clear goals for the lib.

SELECT values without FROM clause

Create a function selectValues that takes a record of Col s a expressions and creates FullQuery s ... such that:

For a record { col1 :: Col s a1, ..., coln :: Col s an }

Would generate to
SELECT <col1_expr> AS col1, ..., <coln_expr> as coln

Better type inference for insert

insert (as well as insert_) has a meaningless type for a user, thus it is hard to start using it.
motivation: #40 has more background on this issue.

insert_ 
   m t r.
  GenericInsert BackendPGClass m t r 
  MonadSeldaPG m 
  Table t  Array { | r }  m Unit

Say we have the following table definition:

employees 
  Table
    ( id  Auto Int
    , name  String
    , salary  Default Int
    )
employees = Table { name: "employees" }

We should be able to apply insert_ to employees and let type inference figure out the rest.
But this is not currently the case as salary is a default column, thus we can assign two different types to insert_ employees

insertEmployees1
    m
  . MonadSeldaPG m
   Array
      { nameString   -- required
      , salaryInt    -- optional
      -- , id ∷ Int     -- illegal to insert
      }
   m Unit
insertEmployees1 = insert_ employees

insertEmployees2
    m
  . MonadSeldaPG m
   Array
      { nameString
      }
   m Unit
insertEmployees2 = insert_ employees

Potential Solution

Change (or provide an alternative) insert to allow only one type once a table is provided (type parameter t should uniquely determine r from the type of insert_).
Types of optional columns should get wrapped with Maybe.

Then definition

insertEmployees = insert_ employees

Should be inferred to

insertEmployees
    m
  . MonadSeldaPG m
   Array
      { nameString
      , salaryMaybe Int -- wrapped with additional Maybe
      }
   m Unit

Columns named after SQL keywords causes problems

Columns SQL keyword names like drop or backend-specific end (for PG) may not work properly.

PG should handle these names for queries without problems but insert/update/delete won't work properly.

There is a workaround - one can define the second table with explicitly escaped column names as follows and use the second table for insert/update/delete only

table  Table ( end  Int )
table = Table { name: "table" }

table_quote  Table ( "\"end\""  Int )
table_quote = Table { name: "table" }

Handle more SQL types

In SQL1992 we have:

SQL defines distinct data types named by the following <key word>s:
CHARACTER, CHARACTER VARYING, BIT, BIT VARYING, NUMERIC, DECIMAL,
INTEGER, SMALLINT, FLOAT, REAL, DOUBLE PRECISION, DATE, TIME,
TIMESTAMP, and INTERVAL.

For sure we can easily extend and add support for DOUBLE PRECISION (Number). We can also explore datetime related types and check if all interesting db backends handle these values consistently.

Nested structures/ JSON

Hi! Oftentimes the object structure is nested while the database result is flat, for example

{
  "_id": "string",
  "email": "string",
  "name": {
    "first": "string",
    "middle": "string",
    "last": "string"
  },
  "picture": "string",
  "role": "none",
  "userStatus": true,
  "dateOfBirth": "2021-02-14",
  "address": {
    "line1": "string",
    "line2": "string",
    "city": "string",
    "state": "string",
    "zip": "string"
  },
  "phones": [
    {
      "type": "none",
      "digits": "string"
    }
  ]
}

If I don't save the nested structures as JSON, I find myself creating a flat and a nested structure and doing some laborious mapping

mkUser :: Entity -> User
mkUser r =
  { _id: r._id
  , email: r.email
  , picture: r.picture
  , role: r.role
  , dateOfBirth: r.dateOfBirth
  , userStatus: r.userStatus
  , address:
      { line1: r.line1
      , line2: r.line2
      , city: r.city
      , state: r.state
      , zip: r.zip
      }
  , name:
      { first: r.first
      , middle: r.middle
      , last: r.last
      }
  ...
  }

If I do save the nested structure as JSON, I loose the ability to type-safe search, right? For instance search for users with city xy. Or is there a better solution to that?

Problem with "pg": "^7.6.0" in package.json

Hi! My OS is Debian GNU/Linux 10.
I have

  • created the db and user
  • cloned the project
npm install
npm run-script lit
spago run -m Guide.SimpleE2E
[info] Build succeeded

No error message, nothing.
Updating the pg dependency seems to solve the problem.

`groupBy` used on non-column value results in a runtime error

Example part of a query:

groupBy $ lit "<some_arbitrary_string>"

results in a runtime error.

groupBy should be used on column values only.
Its type is groupBy ∷ ∀ s a. Col s a → Query s (Aggr s a),
where Col s a is probably too general.
We want to allow only Column a values wrapped with Col s a

Column a values are created from table definitions in functions like selectFrom and are accessible in the record r as shown in the example below:

selectFrom tableDescription $ \r -> do ... -- query description follows here

Add primitive queries that don't use type classes

Copied from here:

  • I see this as an opportunity to simplify how queries are executed (or any SQL statements) - making the whole process more abstract is one thing and simplifying some type classes is the other.
  • type classes like GenericQuery are there for two reasons:
    • to hide complicated constraints from the user, e.q. so that the type of the query consists of one constraint without any intermediate type parameters
    • overloading convenience
  • but the user should not be forced to use them
  • we should provide common query variations that could be used by current type classes and users directly. I've included examples that should illustrate my point
primGenericQuery 
   s a i.
  GetCols i 
  String   -- query parameter placeholder, e.g. '$' for pg, and '?' for sqlite
  Int      -- first query parameter index, usually `1`
            -- both of these parameters determine how we generate query
            -- parameter names, e.g. '$1', '$2', ... for pg
  (String  Array Foreign  a) 
  FullQuery s { | i } 
  a
primGenericQuery ph i exec q = do
  -- transform Query AST `q` into a query string and query parameters
  let { strQuery, params } = showM ph i $ showQuery q

  -- execute the query with the parameters
  exec strQuery params

pgPrimGenericQuery 
   s a i m.
  GetCols i              -- needed by query string generation
  MonadSeldaPG m         -- PG specific monad constraints
  (Array Foreign  m a)  -- generic decoder, errors are handled by monad `m`
  FullQuery s { | i }    -- query AST
  m (Array a)             -- result rows of output records, record type `{ | o }` is determined by `i` via the type-function
pgPrimGenericQuery decodeRow = primGenericQuery "$" 1 exec
  where
  exec strQuery params = do
    conn ← ask
    errOrResult ← liftAff $ PostgreSQL.unsafeQuery conn strQuery params
    result ← either throwError pure errOrResult
    traverse decodeRow result.rows

pgGenericQuery 
   s o i m.
  GetCols i 
  MapR UnCol_ i o              -- type-level function that maps over `{ | i }`
                                -- and removes `Col s` from each record member
                                -- thus `o` is now determined by the input `i`
  MonadSeldaPG m 
  (Array Foreign  m { | o }) 
  FullQuery s { | i } 
  m (Array { | o })             -- the additional constraint forces the
                                -- appropriate result type
pgGenericQuery = pgPrimGenericQuery

IS NOT NULL as SQL expression not query needed

Hi!
I would like to translate

      ( Query
          """
SELECT
  followee.*,
  follower_id IS NOT NULL AS FOLLOWING
FROM
  "user" AS followee
  LEFT JOIN FOLLOWING ON (followee.id = followee_id) AND (follower_id = $1)
WHERE
  followee.username = $2"""
      )
      [ p_ followerId, p_ followeeUsername ]

into

type ProfileCols s
  = { bio :: Col s (Maybe LongString)
    , following :: Col s Boolean
    , image :: Col s (Maybe LongString)
    , username :: Col s ShortString
    }

selectFollowee :: forall s. Maybe FollowerId -> FolloweeUsername -> FullQuery s (ProfileCols s)
selectFollowee followerId followeeUsername =
  selectFrom userTable \fee -> do
    following <- leftJoin followingTable \f -> f.followeeId .== fee.id && f.followerId .== (litPG (fromMaybe 0 followerId))
    restrict $ fee.username .== (litPG followeeUsername)
    pure
      { bio: fee.bio
      , following: isJust <$> following.followerId -- error
      , image: fee.image
      , username: fee.username
      }

No type class instance was found for Data.Functor.Functor (Col s3)

Provide a way to specify required, optional and forbidden columns for `insert` and `update`

As @JordanMartinez pointed out on the slack channel there is a problem with INSERT operation. Currently insert requires you to provide values for all columns of a given table. This defeats the purpose of defaults or auto columns on the db side. Short example and current "solution" described by Jordan can be found here:

https://gist.github.com/JordanMartinez/c5ad7334281f21863cf22cbfb70f0372#file-seldaexample-purs-L42-L65

I propose adding something like this (modulo naming) for this purpose:

https://github.com/paluh/purescript-little-selda/blob/master/src/Database/Selda/Little.purs#L499

This allows user to specify three options for a given column:

• DB takes care entirely for a given column and it is forbidden to specify its value in insert or update,
• DB provides default value but allows you to override it during insert. In such a case you have to wrap value in a Maybe in insert or update statements,
• DB doesn't provide anything for given column so insert / update requires you to provide a value.

I hope that we add something even better to the lib finally.

Unwrapped column type in a table declaration should be translated to NoDbDefault.

Select Count (*) error

Hi!
I want something like

select :: forall s. UserId -> S.FullQuery s (ArticleCols s)
select followerId =
  selectFrom articleTable \a -> do
    ...
    pure
      { ...
      , favoritesCount: countFavorites a.id
      ...
      }

-- SELECT COUNT(*) FROM favorited WHERE favorited.article_id = a.id) AS INTEGER) AS favorites_count,
countFavorites :: forall s. Id -> S.FullQuery s { c  Col s Int }
countFavorites id =
  aggregate
    $ selectFrom_ -- <-error
        ( aggregate
            $ selectFrom favoritedTable \f -> do
                restrict $ f.article_id .== litPG id
                pure { i: f.article_id }
        ) \{ i } -> pure { c: count i }

-- with
type FavoritedTable
  = Table
      ( user_id :: UserId
      , article_id :: ArticleId
      )

favoritedTable :: FavoritedTable
favoritedTable = Table { name: "favorited" }

The error message says I need to annotate, but where? Also, why do I need aggregate two times?

No type class instance was found for Heterogeneous.Mapping.MappingWithIndex OuterCols (SProxy "i") t4 t5
The instance head contains unknown type variables. Consider adding a type annotation.

while applying a function selectFrom_
of type FromSubQuery t0 t1 t2 => FullQuery (Inner t0) (Record t1) -> (Record t2 -> Query t0 ...) -> FullQuery t0 (Record t3)
to argument (apply aggregate) ((selectFrom favoritedTable) (\f ->
(...) (...)
)
)
while inferring the type of selectFrom_ ((apply aggregate) ((selectFrom favoritedTable) (\f ->
...
)
)
)
in value declaration countFavorites

where t3 is an unknown type
t2 is an unknown type
t0 is an unknown type
t1 is an unknown type
t4 is an unknown type
t5 is an unknown type

Array distinct unnest ::TEXT[]

Hi!
I want all tags (array) in every article.

SELECT ARRAY(SELECT DISTINCT unnest(tag_list) FROM article ORDER BY 1)::TEXT[] AS tags

I have tried a bunch of stuff which always results in double FROM and ::TEXT[] not in the right place. This non-selda-solution is the best I came up with.

CREATE VIEW TAG AS
SELECT
  ARRAY ( SELECT DISTINCT
      unnest(tag_list)
    FROM
      article
    ORDER BY
      1)::text[];
type TagTable
  = Table
      ( array :: Array Tag
      )

tagTable :: TagTable
tagTable = Table { name: "tag" }

findTags :: Pool -> Aff (Array Tag)
findTags pool =
  withConnection pool
    ( \conn ->
        runSelda conn
          $ do
              logQuery $ selectFrom tagTable pure
              query $ selectFrom tagTable pure
    )
    >>= validateTags

If you know something better, let me know.

Delete returning

What applies to #44 also applies to delete. Otherwise, it is hard to tell if a particular delete statement has actually matched anything, right?

Allow newtype-wrapped columns

Allowing newtype wrapped columns would be especially useful for (foreign) keys.

For example, it's easy to mistype and join on the wrong fields if they're all ints, but if we add a newtype, then this should be a lot safer. For example, I would love if this worked:

newtype PersonId = PersonId Int
derive instance personIdNewtype :: Newtype PersonId _
derive instance personEq :: Eq PersonId

people  Table
  ( id  PersonId
  , name  String
  , age  Maybe Int
  )
people = Table { name: "people" }

bankAccounts  Table
  ( id  Auto Int
  , personId  PersonId
  , balance  Default Int
  )
bankAccounts = Table { name: "bank_accounts" }

qNamesWithBalance
    s. FullQuery s { nameCol s String , balanceCol s (Maybe Int) }
qNamesWithBalance =
  selectFrom people \{ id, name, age } → do      -- FROM people
    { balance } ← leftJoin bankAccounts          -- LEFT JOIN bank_accounts
                    \acc → id .== acc.personId   -- ON people.id = bank_accounts.personId
    restrict $ id .> lit 1                       -- WHERE people.id > 1
    pure { name, balance }                       -- SELECT people.name, bank_accounts.balance

So we're wrapping the person id in PersonId Int. Right now this gives an error on selectFrom people since it's looking for an Int but finding a PersonId. Ideally, it would work on any Newtype t Int by wrapping/unwrapping the values.

Would this be possible to implement? I might have a go, given some direction.

`insert` should return full table row

I think that the main purpose of RETURING in INSERT clause in postgresql is to access data which were not inserted by the query because we have on hand inserted subset :-)

Is it possible to extend resulting row values to full table column set?

Allow querying from arbitrary sources not only tables

Currently

Right now we allow querying from Table definitions like

fooTable :: Table (c1 :: Int)
fooTable = Table { name: "foo_table" }

Which when used to generate the SQL is used like

SELECT foo_table_0.c1
FROM foo_table foo_table_0

The name of the table foo_table is aliased using this name and a unique number

New feature

We would like to write a query using PG function generate_series

SELECT tablealias.columnalias
FROM generate_series(1,11) tablealias (columnalias);

Proposal

Since datatype Table is used as a black box - meaning we only:

  • put the name supplied in the Table constructor after FROM (or JOIN ...)
  • generate an alias from the given name
  • use alias-namespaced column names given in the type row (here: (c1 :: Int))

Then we could provide a Table-like datatype with more general alias-generation

generateSeries :: Source ( col :: Int )
generateSeries = Source
  { source: \aliasSufix -> "generate_series(1,11) " <> "gs" <> aliasSufix <> " (col)"
  , alias: \aliasSufix -> "gs" <> aliasSufix
  }

source function would be used in FROM/JOIN clause
alias function would be used in other places to refer to this source
These depend on the aliasSufix supplied during SQL generation to ensure that aliases are unique

Add guide to the README

Write an introductory guide explaining what selda can do, how to write some queries and how to use it in a real-life application.

Different Expr type for different backends - Count & Max return type

pg returns the result of the COUT/MAX as String, because of potential precision loss.
While other backends can return Int there (and losing some digits if the result does not fit the Int range)

It is useful to have the expression of type Col s Int rather than Col s String e.g. comparison on strings is different than on ints.

Potential solution:

  • drop the appropriate type in the Expr AST
  • add CAST expression (extend Expr)
  • implement count, max_ and other related functions per backend

Easier logging of the update, insert ...

I guess everybody who wants to log his or her queries/commands is doing the same,

logUpdate 
  forall t s r m.
  TableToColsWithoutAlias s t r =>
  GetCols r =>
  MonadEffect m =>
  Table t  ({ | r }  Col s Boolean)  ({ | r }  { | r })  m Unit
logUpdate table pred up = do
  let
    { strQuery, params } = showPG $ showUpdate table pred up
  log strQuery
  log $ unsafeStringify params
  log ""

logQuery :: forall s i m. GetCols i => MonadEffect m => FullQuery s { | i } -> m Unit
logQuery q = do
  let
    { strQuery, params } = showPG $ showQuery q
  log strQuery
  log $ unsafeStringify params
  log ""
...

I don't know how to do it, but it would be nice to have some sort of tee or errorTee function, which lets me express the statement "I would like to see the generated output".

Enable value-level codecs to be used instead of type-level codecs

Right now, the WriteForeign/ReadForeign and ToSQLValue/FromSQLValue type classes are used to encode/decode values to/from the underlying database.

Could this type-level codec requirement be lessened, so that value-level codecs could be used instead (e.g. purescript-codec-argonaut)?

If so, I believe the Table type could take a record as an additional argument whose fields' values correspond to the codecs to use when encoding/decoding values for a given column.

Provide `insert1` sugar

It would be nice if we could provide insert1 for single insertion which returns exactly one row in as a result.

Insert should execute one query

Right now insert (insert_) executes a query for each given record, then it concatenates the result.
It should insert all of them with just one query.

Query with placeholders and parameters

Motivation: Allow some values created using lit function to be passed as a parameter to a query (right now every value created with lit ends up as a string)

TODO: Allow creating and executing queries with placeholders and corresponding (array of) parameters, which should be passed to the backend-specific query execution function.

Steps:

  • extend Expr with Foreign
  • functions showX should return a result inside a monad that accumulates parameters and assigns placeholders

Question about data intermediate representations

I have coded a findByCredentials query with selda. Email, password goes in, User record comes out. The User record has restricted newtypes inside of it (LongString, ShortString).

The same information User(bio,...) is now represented 4 times in the code (see below) + database table definition in sql. Can you help me getting the number down?

Thanks.

findByCredentials :: PG.Pool -> Credentials -> Aff (Either InputError User)
findByCredentials pool { email, password } =
  PG.withConnection pool case _ of
    Left pgError -> throwError $ error $ ("PostgreSQL connection error: " <> show pgError)
    Right conn -> do
      runSelda conn (app email password) >>= validate

validate :: forall a. Either PGError (Array a) -> Aff (Either InputError a)
validate result = do
  case result of
    Left e -> case e of
      IntegrityError detail -> case detail.constraint of
        "email_unique" -> pure $ Left EMAIL_EXISTS
        "username_unique" -> pure $ Left USERNAME_EXISTS
        otherwise -> throwError $ error $ show e
      otherwise -> throwError $ error $ show e
    Right rows -> case head rows of
      Nothing -> pure $ Left NOT_FOUND
      Just row -> pure $ Right row

user 
  Table
    ( bio :: Maybe String -- 1. representation
    , email :: String
    , id :: Auto Int
    , image :: Maybe String
    , password :: String
    , username :: String
    )
user =
  Source "user"
    $ case _ of
        Nothing"\"user\""
        Just alias → "\"user\"" <> " " <> alias

type InterUser 
  = { bio :: Maybe String -- 2. representation
    , email :: String
    , id :: Int
    , image :: Maybe String
    , password :: String
    , username :: String
    }

mkUser :: InterUser -> User 
mkUser i =
  unsafePartial
    { bio: LongString.unsafeFromString <$> i.bio -- 3. representation
    , email: ShortString.unsafeFromString (i.email)
    , id: i.id
    , image: LongString.unsafeFromString <$> i.image
    , password: ShortString.unsafeFromString i.password
    , username: ShortString.unsafeFromString i.username
    }

selectUser 
   s.
  Email ->
  Password ->
  FullQuery s
    { bio :: Col s (Maybe String)  -- 4. representation
    , email  Col s String
    , id  Col s Int
    , image :: Col s (Maybe String)
    , password :: Col s String
    , username :: Col s String
    }
selectUser email password =
  selectFrom user \r → do
    restrict $ r.email .== lit (ShortString.toString email) && r.password .== lit (ShortString.toString password)
    pure r

queryUser 
   m.
  MonadSeldaPG m =>
  Email -> Password -> m (Array InterUser)
queryUser email password = query (selectUser email password)

app   m. MonadSeldaPG m => Email -> Password -> m (Array User)
app email password = do
  logQuery $ selectUser email password
  rows <- queryUser email password
  pure $ mkUser <$> rows

logQuery   s i m. GetCols i  MonadEffect m  FullQuery s { | i }  m Unit
logQuery q = do
  let
    { strQuery, params } = showPG $ showQuery q
  log strQuery
  log $ unsafeStringify params
  log ""

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.