-
Notifications
You must be signed in to change notification settings - Fork 108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add new Postgresql.JSON.Experimental #283
base: master
Are you sure you want to change the base?
Changes from 4 commits
67ab9c6
59ab459
82f484a
ab00454
1d2fc6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -560,20 +560,17 @@ subSelectForeign expr foreignKey k = | |
-- 'subSelectMaybe'. For the most common safe use of this, see 'subSelectCount'. | ||
-- | ||
-- @since 3.2.0 | ||
subSelectUnsafe :: PersistField a => SqlQuery (SqlExpr (Value a)) -> SqlExpr (Value a) | ||
subSelectUnsafe :: SqlSelect (SqlExpr a) r => SqlQuery (SqlExpr a) -> SqlExpr a | ||
subSelectUnsafe = sub SELECT | ||
|
||
-- | Project a field of an entity. | ||
(^.) :: forall typ val . (PersistEntity val, PersistField typ) | ||
=> SqlExpr (Entity val) | ||
-> EntityField val typ | ||
-> SqlExpr (Value typ) | ||
ERaw m f ^. field | ||
ent ^. field | ||
| isIdField field = idFieldValue | ||
| Just alias <- sqlExprMetaAlias m = | ||
ERaw noMeta $ \_ info -> | ||
f Never info <> ("." <> useIdent info (aliasedEntityColumnIdent alias fieldDef), []) | ||
| otherwise = ERaw noMeta $ \_ info -> (dot info $ persistFieldDef field, []) | ||
| otherwise = ERaw noMeta $ \_ info -> (viewFieldBuilder ent info fieldDef, []) | ||
where | ||
fieldDef = | ||
if isIdField field then | ||
|
@@ -583,25 +580,27 @@ ERaw m f ^. field | |
persistFieldDef field | ||
idFieldValue = | ||
case getEntityKeyFields ed of | ||
idField :| [] -> | ||
ERaw noMeta $ \_ info -> (dot info idField, []) | ||
idField :| [] -> ERaw noMeta $ \_ info -> (viewFieldBuilder ent info idField, []) | ||
|
||
idFields -> | ||
let renderedFields info = dot info <$> NEL.toList idFields | ||
let renderedFields info = viewFieldBuilder ent info <$> NEL.toList idFields | ||
in ERaw noMeta{ sqlExprMetaCompositeFields = Just renderedFields} $ | ||
\p info -> (parensM p $ uncommas $ renderedFields info, []) | ||
|
||
ed = entityDef $ getEntityVal (Proxy :: Proxy (SqlExpr (Entity val))) | ||
|
||
dot info fieldDef' = | ||
sourceIdent info <> "." <> fieldIdent | ||
where | ||
sourceIdent = fmap fst $ f Never | ||
fieldIdent | ||
| Just baseI <- sqlExprMetaAlias m = | ||
useIdent info $ aliasedEntityColumnIdent baseI fieldDef' | ||
| otherwise = | ||
fromDBName info (coerce $ fieldDB fieldDef') | ||
viewFieldBuilder :: SqlExpr (Entity val) -> IdentInfo -> FieldDef -> TLB.Builder | ||
viewFieldBuilder (ERaw m f) info fieldDef | ||
| Just alias <- sqlExprMetaAlias m = | ||
fst (f Never info) <> ("." <> useIdent info (aliasedEntityColumnIdent alias fieldDef)) | ||
| otherwise = sourceIdent info <> "." <> fieldIdent | ||
where | ||
sourceIdent = fst <$> f Never | ||
fieldIdent | ||
| Just baseI <- sqlExprMetaAlias m = | ||
useIdent info $ aliasedEntityColumnIdent baseI fieldDef | ||
| otherwise = | ||
fromDBName info (coerce $ fieldDB fieldDef) | ||
belevy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
-- | Project an SqlExpression that may be null, guarding against null cases. | ||
withNonNull | ||
|
@@ -2374,7 +2373,7 @@ setAux field value = \ent -> ERaw noMeta $ \_ info -> | |
(valueToSet, valueVals) = valueF Parens info | ||
in (fieldName info field <> " = " <> valueToSet, valueVals) | ||
|
||
sub :: PersistField a => Mode -> SqlQuery (SqlExpr (Value a)) -> SqlExpr (Value a) | ||
sub :: (SqlSelect (SqlExpr a) r) => Mode -> SqlQuery (SqlExpr a) -> SqlExpr a | ||
sub mode query = ERaw noMeta $ \_ info -> first parens $ toRawSql mode info query | ||
|
||
fromDBName :: IdentInfo -> DBName -> TLB.Builder | ||
|
@@ -2521,7 +2520,7 @@ valueToFunctionArg info (ERaw _ f) = f Never info | |
-- from 'unsafeSqlBinOp' applies to this function as well. | ||
unsafeSqlFunction | ||
:: UnsafeSqlFunctionArgument a | ||
=> TLB.Builder -> a -> SqlExpr (Value b) | ||
=> TLB.Builder -> a -> SqlExpr b | ||
unsafeSqlFunction name arg = | ||
ERaw noMeta $ \_ info -> | ||
let (argsTLB, argsVals) = | ||
|
@@ -2557,7 +2556,7 @@ unsafeSqlFunctionParens name arg = | |
|
||
-- | (Internal) An explicit SQL type cast using CAST(value as type). | ||
-- See 'unsafeSqlBinOp' for warnings. | ||
unsafeSqlCastAs :: T.Text -> SqlExpr (Value a) -> SqlExpr (Value b) | ||
unsafeSqlCastAs :: T.Text -> SqlExpr a -> SqlExpr b | ||
unsafeSqlCastAs t (ERaw _ f) = ERaw noMeta $ \_ -> ((first (\value -> "CAST" <> parens (value <> " AS " <> TLB.fromText t))) . f Never) | ||
|
||
-- | (Internal) This class allows 'unsafeSqlFunction' to work with different | ||
|
@@ -2576,8 +2575,8 @@ class UnsafeSqlFunctionArgument a where | |
instance UnsafeSqlFunctionArgument () where | ||
toArgList _ = [] | ||
|
||
instance (a ~ Value b) => UnsafeSqlFunctionArgument (SqlExpr a) where | ||
toArgList = (:[]) . veryUnsafeCoerceSqlExprValue | ||
instance UnsafeSqlFunctionArgument (SqlExpr a) where | ||
toArgList = (:[]) . veryVeryUnsafeCoerceSqlExpr | ||
|
||
instance UnsafeSqlFunctionArgument a => UnsafeSqlFunctionArgument [a] where | ||
toArgList = concatMap toArgList | ||
|
@@ -2684,18 +2683,22 @@ instance ( UnsafeSqlFunctionArgument a | |
) => UnsafeSqlFunctionArgument (a, b, c, d, e, f, g, h, i, j) where | ||
toArgList = toArgList . from10 | ||
|
||
-- | (Internal) Coerce a SqlExpr from any arbitrary a to any arbitrary b | ||
-- You should /not/ use this function unless you know what you're doing! | ||
veryVeryUnsafeCoerceSqlExpr :: SqlExpr a -> SqlExpr b | ||
veryVeryUnsafeCoerceSqlExpr = coerce | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wait we have a |
||
|
||
-- | (Internal) Coerce a value's type from 'SqlExpr (Value a)' to | ||
-- 'SqlExpr (Value b)'. You should /not/ use this function | ||
-- unless you know what you're doing! | ||
veryUnsafeCoerceSqlExprValue :: SqlExpr (Value a) -> SqlExpr (Value b) | ||
veryUnsafeCoerceSqlExprValue = coerce | ||
veryUnsafeCoerceSqlExprValue = veryVeryUnsafeCoerceSqlExpr | ||
|
||
|
||
-- | (Internal) Coerce a value's type from 'SqlExpr (ValueList | ||
-- a)' to 'SqlExpr (Value a)'. Does not work with empty lists. | ||
veryUnsafeCoerceSqlExprValueList :: SqlExpr (ValueList a) -> SqlExpr (Value a) | ||
veryUnsafeCoerceSqlExprValueList = coerce | ||
veryUnsafeCoerceSqlExprValueList = veryVeryUnsafeCoerceSqlExpr | ||
|
||
|
||
---------------------------------------------------------------------- | ||
|
@@ -3381,7 +3384,7 @@ instance PersistField a => SqlSelect (SqlExpr (Value a)) (Value a) where | |
sqlSelectProcessRow pvs = Value <$> fromPersistValue (PersistList pvs) | ||
|
||
-- | Materialize a @SqlExpr (Value a)@. | ||
materializeExpr :: IdentInfo -> SqlExpr (Value a) -> (TLB.Builder, [PersistValue]) | ||
materializeExpr :: IdentInfo -> SqlExpr a -> (TLB.Builder, [PersistValue]) | ||
materializeExpr info (ERaw m f) | ||
| Just fields <- sqlExprMetaCompositeFields m = (uncommas $ fmap parens $ fields info, []) | ||
| Just alias <- sqlExprMetaAlias m | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
{-# LANGUAGE FlexibleContexts #-} | ||
{-# LANGUAGE FlexibleInstances #-} | ||
{-# LANGUAGE FunctionalDependencies #-} | ||
{-# LANGUAGE OverloadedStrings #-} | ||
{-# LANGUAGE ScopedTypeVariables #-} | ||
{-# LANGUAGE TypeApplications #-} | ||
{-# LANGUAGE TypeFamilies #-} | ||
{-# LANGUAGE UndecidableInstances #-} | ||
module Database.Esqueleto.Internal.JSON | ||
where | ||
|
||
import qualified Data.Aeson as Aeson | ||
import Data.Bifunctor (first) | ||
import qualified Data.ByteString.Lazy as LBS | ||
import Data.Proxy (Proxy(..)) | ||
import Data.Text (Text) | ||
import qualified Data.Text as Text | ||
import Data.Text.Encoding (encodeUtf8) | ||
import qualified Data.Text.Lazy.Builder as TLB | ||
import Database.Esqueleto.Internal.Internal | ||
import Database.Persist | ||
|
||
class SqlToJson jsonValue a b | jsonValue a -> b where | ||
toJson :: a -> SqlExpr (jsonValue b) | ||
|
||
class JsonAgg jsonValue where | ||
jsonAgg :: SqlExpr (jsonValue a) -> SqlExpr (jsonValue [a]) | ||
|
||
class JsonBuildArray jsonValue where | ||
unsafeJsonbBuildArray :: UnsafeSqlFunctionArgument a => a -> SqlExpr (jsonValue b) | ||
|
||
class JsonBuildObject jsonValue where | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MySQL supports JSON as well. The intention is to add support in a db agnostic manner with as much independent of db specifics as possible |
||
unsafeJsonbBuildObject :: [(SqlExpr (Value Text), SqlExpr SomeValue)] -> SqlExpr (jsonValue a) | ||
|
||
multiset :: forall jsonValue a b r. | ||
( Aeson.FromJSON b | ||
, SqlToJson jsonValue a b | ||
, JsonAgg jsonValue | ||
, SqlSelect (SqlExpr (jsonValue [b])) r | ||
) | ||
=> SqlQuery a -> SqlExpr (jsonValue [b]) | ||
multiset q = | ||
subSelectUnsafe $ jsonAgg . toJson <$> q | ||
|
||
instance SqlToJson jsonValue (SqlExpr a) b | ||
=> SqlToJson jsonValue (SqlExpr (Maybe a)) (Maybe b) where | ||
toJson = | ||
let unMaybe :: SqlExpr (Maybe a) -> SqlExpr a | ||
unMaybe = veryVeryUnsafeCoerceSqlExpr | ||
in veryVeryUnsafeCoerceSqlExpr . toJson @jsonValue . unMaybe | ||
|
||
|
||
instance forall a jsonValue . (PersistEntity a, JsonBuildObject jsonValue) | ||
=> SqlToJson jsonValue (SqlExpr (Entity a)) (Entity a) where | ||
toJson ent = | ||
unsafeJsonbBuildObject fields | ||
where | ||
ed = entityDef $ Proxy @a | ||
baseFields = fmap (\fieldDef -> | ||
( unsafeSqlValue $ TLB.fromText $ "'" <> unFieldNameHS (fieldHaskell fieldDef) <> "'" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't seem right. We're using the Haskell names for the database representation of the This is one of the big problems I ran into with trying to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does persistent support a way to read the to/from json field name given a field? We can sidestep the issue of to/from by using a custom decoder. As long as we are consistent at the boundaries we could even avoid the need to specify JSON instances for the types. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Am I right when I say this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct, the goal being to make something that automatically just works with the default JSON parser that gets generated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@parsonsmatt I am not actually clear on what you mean by this? We aren't letting postgres do its default conversion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think I'm unsure that this is the right approach for converting something to a JSON representation. I think I'd rather see something like: newtype JsonEntity a = JsonEntity (Entity a)
instance PersistEntity a => FromJSON (JsonEntity a) where
parseJSON = withObject "JsonEntity" $ \o -> do
let edef = entityDef (Proxy @a)
... which definitely parses how a database would convert things to JSON. Then we aren't having to worry about custom encoders or decoders.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could work sure, if we used a more general There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So there isn't really a great way to do this in general without that Is there an example of someone using an exotic JSON parser? The thing that I need the default parser for is the underlying record not the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be noted that using the QQ's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it even has to be an exotic parse, just a different set of options. Like, mkPersist sqlSettings [persistLowerCase|
User
name Text
age Int
|]
deriveJSON defaultOptions ''User This would expect the JSON record to have the type name prefixes. I don't think the So I think we do need a |
||
, ERaw noMeta $ \_ info -> (viewFieldBuilder ent info fieldDef, []) | ||
)) (getEntityFields ed) | ||
idField = fmap (\fieldDef -> | ||
( unsafeSqlValue "'id'" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't necessarily proper, as you can specify the primary column name:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The actual field name in the SQL is irrelevant, it's the representation that FromJSON expects that matters. Entity assumes a key of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it not possible to use the database names? 🤔 To me that sounds more robust/portable. Though I guess if it only happens within one query, it doesn't really matter. EDIT: Oh, that might also make it possible to get back easily using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed the db names could be used and a custom JSON decoder could be used to extract the fields. |
||
, ERaw noMeta $ \_ info -> (viewFieldBuilder ent info fieldDef, []) | ||
)) (getEntityIdField ed) | ||
|
||
fields = maybe baseFields (:baseFields) idField | ||
|
||
|
||
instance ( SqlToJson jsonValue a a' | ||
, SqlToJson jsonValue b b' | ||
, JsonBuildArray jsonValue | ||
) | ||
=> SqlToJson jsonValue (a, b) (a', b') where | ||
toJson (a, b) = | ||
unsafeJsonbBuildArray | ||
( toJson @jsonValue a | ||
, toJson @jsonValue b | ||
) | ||
|
||
instance ( SqlToJson jsonValue a a' | ||
, SqlToJson jsonValue b b' | ||
, SqlToJson jsonValue c c' | ||
, JsonBuildArray jsonValue | ||
) | ||
=> SqlToJson jsonValue (a, b, c) (a', b', c') where | ||
toJson (a, b, c) = | ||
unsafeJsonbBuildArray | ||
( toJson @jsonValue a | ||
, toJson @jsonValue b | ||
, toJson @jsonValue c | ||
) | ||
|
||
instance ( SqlToJson jsonValue a a' | ||
, SqlToJson jsonValue b b' | ||
, SqlToJson jsonValue c c' | ||
, SqlToJson jsonValue d d' | ||
, JsonBuildArray jsonValue | ||
) | ||
=> SqlToJson jsonValue (a, b, c, d) (a', b', c', d') where | ||
toJson (a, b, c, d) = | ||
unsafeJsonbBuildArray | ||
( toJson @jsonValue a | ||
, toJson @jsonValue b | ||
, toJson @jsonValue c | ||
, toJson @jsonValue d | ||
) | ||
instance ( SqlToJson jsonValue a a' | ||
, SqlToJson jsonValue b b' | ||
, SqlToJson jsonValue c c' | ||
, SqlToJson jsonValue d d' | ||
, SqlToJson jsonValue e e' | ||
, JsonBuildArray jsonValue | ||
) | ||
=> SqlToJson jsonValue (a, b, c, d, e) (a', b', c', d', e') where | ||
toJson (a, b, c, d, e) = | ||
unsafeJsonbBuildArray | ||
( toJson @jsonValue a | ||
, toJson @jsonValue b | ||
, toJson @jsonValue c | ||
, toJson @jsonValue d | ||
, toJson @jsonValue e | ||
) | ||
instance ( SqlToJson jsonValue a a' | ||
, SqlToJson jsonValue b b' | ||
, SqlToJson jsonValue c c' | ||
, SqlToJson jsonValue d d' | ||
, SqlToJson jsonValue e e' | ||
, SqlToJson jsonValue f f' | ||
, JsonBuildArray jsonValue | ||
) | ||
=> SqlToJson jsonValue (a, b, c, d, e, f) (a', b', c', d', e', f') where | ||
toJson (a, b, c, d, e, f) = | ||
unsafeJsonbBuildArray | ||
( toJson @jsonValue a | ||
, toJson @jsonValue b | ||
, toJson @jsonValue c | ||
, toJson @jsonValue d | ||
, toJson @jsonValue e | ||
, toJson @jsonValue f | ||
) | ||
instance ( SqlToJson jsonValue a a' | ||
, SqlToJson jsonValue b b' | ||
, SqlToJson jsonValue c c' | ||
, SqlToJson jsonValue d d' | ||
, SqlToJson jsonValue e e' | ||
, SqlToJson jsonValue f f' | ||
, SqlToJson jsonValue g g' | ||
, JsonBuildArray jsonValue | ||
) | ||
=> SqlToJson jsonValue (a, b, c, d, e, f, g) (a', b', c', d', e', f', g') where | ||
toJson (a, b, c, d, e, f, g) = | ||
unsafeJsonbBuildArray | ||
( toJson @jsonValue a | ||
, toJson @jsonValue b | ||
, toJson @jsonValue c | ||
, toJson @jsonValue d | ||
, toJson @jsonValue e | ||
, toJson @jsonValue f | ||
, toJson @jsonValue g | ||
) | ||
instance ( SqlToJson jsonValue a a' | ||
, SqlToJson jsonValue b b' | ||
, SqlToJson jsonValue c c' | ||
, SqlToJson jsonValue d d' | ||
, SqlToJson jsonValue e e' | ||
, SqlToJson jsonValue f f' | ||
, SqlToJson jsonValue g g' | ||
, SqlToJson jsonValue h h' | ||
, JsonBuildArray jsonValue | ||
) | ||
=> SqlToJson jsonValue (a, b, c, d, e, f, g, h) (a', b', c', d', e', f', g', h') where | ||
toJson (a, b, c, d, e, f, g, h) = | ||
unsafeJsonbBuildArray | ||
( toJson @jsonValue a | ||
, toJson @jsonValue b | ||
, toJson @jsonValue c | ||
, toJson @jsonValue d | ||
, toJson @jsonValue e | ||
, toJson @jsonValue f | ||
, toJson @jsonValue g | ||
, toJson @jsonValue h | ||
) | ||
|
||
sqlSelectProcessRowJSON :: (Applicative f, Aeson.FromJSON r) | ||
=> [PersistValue] -> Either Text (f r) | ||
sqlSelectProcessRowJSON [PersistByteString bs] = | ||
case Aeson.eitherDecode $ LBS.fromStrict bs of | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to import either Text.pack pure $
Aeson.eitherDecodeStrict bs |
||
Right r -> Right $ pure r | ||
Left e -> Left $ Text.pack e | ||
sqlSelectProcessRowJSON [PersistText t] = | ||
first (<> (" " <> t)) $ sqlSelectProcessRowJSON [PersistByteString (encodeUtf8 t)] | ||
|
||
sqlSelectProcessRowJSON _ = Left "Expected ByteString but database returned unexpected type" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't
fst (f Never info)
literallysourceIdent info
?Maybe define
sourceIdent
asfst (f Never info)
and use in both places?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a better question, does this branch ever differ from the otherwise branch? both branches appear to produce identical results for the
Just alias
case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, very true.
fieldIdent
'sJust
case will never get triggered 🙃 (And is indeed the same)