bitemyapp / esqueleto

New home of Esqueleto, please file issues so we can get things caught up!
BSD 3-Clause "New" or "Revised" License
370 stars 107 forks source link

`SqlSelect` instance for non-`Entity` records #311

Open parsonsmatt opened 2 years ago

parsonsmatt commented 2 years ago

It kinda sucks that we get nice syntax support for Entity record but for anything else we're stuck with tuples of values.

Some ideas:

Higher Kinded Data

I'm not a fan of HKD, but it does work for this alright.

data FooF f = FooF { fooName :: f (Value String), fooAge :: f (Value Int) }

instance SqlSelect (FooF SqlExpr) (FooF Identity) where ...

With a bit of TemplateHaskell the instances are relatively easy to generate.

I recall having some super bad type error messages with this, particularly if f got inferred to be two different things in a record.

Separate Records

We could have something like:

data Foo = Foo { fooName :: String, fooAge :: Int }

mkSqlSelectRecord ''Foo

-- ======>

data FooSql = FooSql { fooName :: SqlExpr (Value String), fooAge :: SqlExpr (Value Int) }

instance SqlSelect FooSql Foo where ...

But there's some question about making constructors/types match without conflict, and how to deal with the record labels - keep them the same? suffix/prefix somehow?

Anonymous Records

Possibly large-anon could be used to return something sorta record-like, which we can SqlSelect easily enough.

I've certainly seen anon records with syntax like:

myRec :: Key "foo" Int :& Key "bar" Int :& Key "baz" Integer
myRec = #foo := 1 :& #bar := 2 :& #baz := 3

which may be pretty easy to map onto a SqlSelect instance.

prairie ?

Possibly we could leverage prairie. It is inspired by EntityField.

-- for constructing,
buildSql :: (forall a. Field rec a -> SqlExpr (Value a)) -> SqlExpr (Columns a)

-- for access, using `OverloadedRecordDot` or `getFIeld` or generalizing `(^.)`
instance (SymbolToField sym rec typ, Record rec) => HasField sym (Columns a) (SqlExpr (Value a))

instance (FieldDict PersistField a) => SqlSelect (SqlExpr (Columns a)) a

We'd be able to write, like,

do
  from ...
  where_ ...
  pure $ buildSql $ \case
    FooName -> user ^. #name  
    FooAge -> dog ^. #age

Hmm. The problem is basically that we often need to write something like:

data MyRow = MyRow { lots of fields }

myAction :: SqlPersistT IO [MyRow]
myAction = fmap (fmap convert) $ do
  select $ do
    from ...
    where_ ...
    pure (lots, of, rows, wow, so, many)
  where
    convert :: (Value Lots, Value Of, Value Rows, Value Wow, Value So, Value Many) -> MyRow
    convert = ...

Tuples suck are are tedious and error prone. It'd be much nicer to use record syntax to construct things in a name-directed way.

myAction :: SqlPersistT IO [MyRow]
myAction = do
  select $ do
    from ...
    where_ ...
    pure MyRow
        { myRowLots = lots
        , myRowOf = of
        , myRowColums = columns -- whoops lmao
        , myRowsWow = -- etc
        , ..
        }

The problem is that myRowLots :: Lots, and lots in the sql is SqlExpr (Value Lots). Type-error.

For Entity, the only way to actually get a hold of an SqlExpr (Entity a) is to use from and pull it out of a table. So that's how we avoid needing to construct an Entity for this.