bitemyapp / esqueleto

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

`deriveEsqueletoRecord` should "see through" `Maybe` for nested records #347

Open parsonsmatt opened 1 year ago

parsonsmatt commented 1 year ago

I've got a record I'm deriveEsqueletoRecording:

data X = X { x :: Entity Foo }

deriveEsqueletoRecord ''X

getX :: SqlQuery X

and I'm subSelecting that in another record:

data Y = Y { y :: Maybe X }

deriveEsqueletoRecord ''Y

getY = do
    pure Y
        { y = subSelect getX
        }

Unfortunately this fails for a few reasons:

  1. subSelect only works on a SqlExpr (Value a) - fixable with a subSelectRecord :: SqlQuery a -> SqlExpr (Maybe a).
  2. The codegen for SqlY creates the field y :: SqlExpr (Value (Maybe X)). The SqlExpr (Value (Maybe _)) should be "folded in" to the Maybe _ bit.

Here's the code that determines the field type:

-- | Transforms a record field type into a corresponding `SqlExpr` type.
--
-- * @'Entity' x@ is transformed into @'SqlExpr' ('Entity' x)@.
-- * @'Maybe' ('Entity' x)@ is transformed into @'SqlExpr' ('Maybe' ('Entity' x))@.
-- * @x@ is transformed into @'SqlExpr' ('Value' x)@.
-- * If there exists an instance @'SqlSelect' sql x@, then @x@ is transformed into @sql@.
--
-- This function should match `sqlSelectProcessRowPat`.
sqlFieldType :: Type -> Q Type
sqlFieldType fieldType = do
  maybeSqlType <- reifySqlSelectType fieldType

  pure $
    flip fromMaybe maybeSqlType $
      case fieldType of
        -- Entity x -> SqlExpr (Entity x)
        AppT (ConT ((==) ''Entity -> True)) _innerType -> AppT (ConT ''SqlExpr) fieldType

        -- Maybe (Entity x) -> SqlExpr (Maybe (Entity x))
        (ConT ((==) ''Maybe -> True))
          `AppT` ((ConT ((==) ''Entity -> True))
                  `AppT` _innerType) -> AppT (ConT ''SqlExpr) fieldType

        -- x -> SqlExpr (Value x)
        _ -> (ConT ''SqlExpr)
                `AppT` ((ConT ''Value)
                        `AppT` fieldType)

And reifySqlSelectType:

-- Given a type, find the corresponding SQL type.
--
-- If there exists an instance `SqlSelect sql ty`, then the SQL type for `ty`
-- is `sql`.
--
-- This function definitely works for records and instances generated by this
-- module, and might work for instances outside of it.
reifySqlSelectType :: Type -> Q (Maybe Type)
reifySqlSelectType originalType = do
  -- Here we query the compiler for Instances of `SqlSelect a $(originalType)`;
  -- the API for this is super weird, it interprets a list of types as being
  -- applied as successive arguments to the typeclass name.
  --
  -- See: https://gitlab.haskell.org/ghc/ghc/-/issues/21825
  --
  -- >>> reifyInstances ''SqlSelect [VarT (mkName "a"), ConT ''MyRecord]
  -- [ InstanceD Nothing
  --             []
  --             (AppT (AppT (ConT Database.Esqueleto.Internal.Internal.SqlSelect)
  --                         (ConT Ghci3.SqlMyRecord))
  --                   (ConT Ghci3.MyRecord))
  --             []
  -- ]
  tyVarName <- newName "a"
  instances <- reifyInstances ''SqlSelect [VarT tyVarName, originalType]

  -- Given the original type (`originalType`) and an instance type for a
  -- `SqlSelect` instance, get the SQL type which corresponds to the original
  -- type.
  let extractSqlRecord :: Type -> Type -> Maybe Type
      extractSqlRecord originalTy instanceTy =
        case instanceTy of
          (ConT ((==) ''SqlSelect -> True))
            `AppT` sqlTy
            `AppT` ((==) originalTy -> True) -> Just sqlTy
          _ -> Nothing

      -- Filter `instances` to the instances which match `originalType`.
      filteredInstances :: [Type]
      filteredInstances =
        flip mapMaybe instances
          (\case InstanceD _overlap
                           _constraints
                           (extractSqlRecord originalType -> Just sqlRecord)
                           _decs ->
                             Just sqlRecord
                 _ -> Nothing)

  pure $ listToMaybe filteredInstances

Need to experiment a bit with this

parsonsmatt commented 1 year ago

I think we can fix this just by generating the Maybe record instances of SqlSelect, ToAlias, ToAliasReference, etc.

parsonsmatt commented 1 year ago

I think we can reuse that info for the #344 stuff too!