1 {-# LANGUAGE AllowAmbiguousTypes #-}
2 {-# LANGUAGE UndecidableInstances #-}
3 {-# OPTIONS_GHC -Wno-orphans #-}
5 module Worksheets.Utils.SQL (
6 module Worksheets.Utils.SQL,
7 module Database.SQLite.Simple,
8 module Database.SQLite.Simple.FromRow,
9 module Database.SQLite.Simple.FromField,
10 module Database.SQLite.Simple.ToField,
11 module Database.SQLite.Simple.Ok,
12 module Database.SQLite.Simple.Internal,
15 import Control.Exception (Exception, SomeException (..))
16 import Control.Monad.Trans.Class qualified as MT
17 import Control.Monad.Trans.Reader qualified as MT
18 import Control.Monad.Trans.State.Strict qualified as MT
19 import Data.Aeson qualified as JSON
20 import Data.List qualified as List
21 import Data.Text.Encoding qualified as Text
22 import Data.Text.Lazy qualified as LazyText
23 import Data.Text.Short qualified as ShortText
24 import Database.SQLite.Simple
25 import Database.SQLite.Simple.FromField
26 import Database.SQLite.Simple.FromRow
27 import Database.SQLite.Simple.Internal
28 import Database.SQLite.Simple.Ok (ManyErrors (..), Ok (..))
29 import Database.SQLite.Simple.ToField
30 import Database.SQLite.Simple.ToRow
31 import Database.SQLite3 qualified as SQL.Base
32 import Text.Show (showString)
34 import Worksheets.Utils.Generics qualified as Gen
35 import Worksheets.Utils.Prelude
37 -- DOC: https://highperformancesqlite.com/articles/sqlite-recommended-pragmas
39 -- PRAGMA foreign_keys = ON;
40 -- PRAGMA journal_mode = WAL;
41 -- PRAGMA mmap_size = 268435456; --256MB
42 -- PRAGMA page_size = 8192;
44 class GFromRowWithErrorContext f where gfromRowWithErrorContext :: RowParser (f a)
45 instance GFromRowWithErrorContext Gen.U1 where
46 gfromRowWithErrorContext = pure Gen.U1
47 instance FromField a => GFromRowWithErrorContext (Gen.K1 i a) where
48 gfromRowWithErrorContext = Gen.K1 <$> fromFieldWithErrorContext
49 instance GFromRowWithErrorContext a => GFromRowWithErrorContext (Gen.M1 i c a) where
50 gfromRowWithErrorContext = Gen.M1 <$> gfromRowWithErrorContext
51 instance (GFromRowWithErrorContext a, GFromRowWithErrorContext b) => GFromRowWithErrorContext (a Gen.:*: b) where
52 gfromRowWithErrorContext = (Gen.:*:) <$> gfromRowWithErrorContext <*> gfromRowWithErrorContext
54 fromFieldWithErrorContext :: FromField a => RowParser a
55 fromFieldWithErrorContext = fieldWithErrorContext fromField
56 {-# INLINE fromFieldWithErrorContext #-}
58 -- | Enables the use of:
60 -- deriving (`ToRow`, `FromRow`) via (`GenericallyWithOptions` MyDataType)
62 newtype GenericallyWithOptions a = GenericallyWithOptions a
64 instance (Generic a, GFromRowWithErrorContext (Gen.Rep a)) => FromRow (GenericallyWithOptions a) where
65 fromRow = gfromRowWithErrorContext <&> (Gen.to >>> GenericallyWithOptions)
66 instance (Generic a, GToRow (Gen.Rep a)) => ToRow (GenericallyWithOptions a) where
67 toRow (GenericallyWithOptions x) = Gen.from x & gtoRow
69 instance ToField ShortText where
70 toField = SQLText . ShortText.toText
71 instance FromField ShortText where
72 -- fromField = fromField >>> fmap ShortText.fromText
73 fromField (Field (SQLText txt) _) = Ok $ txt & ShortText.fromText
74 fromField f@(Field d col) = returnError ConversionFailed f $ "need an SQLText at column=" <> show col <> " but got: " <> show d
76 data FieldErrorContext = FieldErrorContext
77 { fieldErrorContextRemainingColumns :: [SQL.Base.SQLData]
79 instance Show FieldErrorContext where
80 showsPrec _p FieldErrorContext{..} = showString (pShow fieldErrorContextRemainingColumns & LazyText.unpack)
81 instance Exception FieldErrorContext
83 fieldWithErrorContext :: FieldParser a -> RowParser a
84 fieldWithErrorContext fieldP = RP $ do
85 ncols <- MT.asks nColumns
86 (column, remaining) <- MT.lift MT.get
87 MT.lift (MT.put (column + 1, List.tail remaining))
89 then MT.lift (MT.lift (Errors [SomeException (ColumnOutOfBounds (column + 1))]))
91 let r = List.head remaining
92 let fld = Field r column
98 let err = FieldErrorContext remaining
99 Errors (SomeException err : errs)
102 instance FromField JSON.Value where
103 fromField = fromJSONField JSON.Null
104 instance ToField JSON.Value where
105 toField = JSON.encode >>> toField
107 instance FromField JSON.Array where
108 fromField = fromJSONField mempty
109 instance ToField JSON.Array where
110 toField = toJSONField
112 instance JSON.ToJSON a => ToField [a] where
113 toField = toJSONField
114 instance (JSON.FromJSON a, Typeable a) => FromField [a] where
115 fromField = fromJSONField []
117 -- | Uses `JSON.eitherDecodeStrict` and expect the JSON
118 -- to be stored in an `BLOB` column.
120 -- Beware that SQlite does not enforces `BLOB` columns
121 -- to contain only `BLOB` data, they could be `TEXT`,
122 -- so you have to check that the way you `INSERT`
123 -- is indeed inserting a `BLOB`,
124 -- eg. in Python `bytes()` must be used:
126 -- bytes(json.dumps(value, ensure_ascii=False),'utf-8')
128 fromJSONField :: JSON.FromJSON a => Typeable a => a -> Field -> Ok a
129 fromJSONField empt fld =
130 fromField fld & \case
132 fromField fld & \case
133 Errors err -> Ok empt
134 -- returnError Incompatible fld $ show err
135 Ok txt -> txt & Text.encodeUtf8 & decodeBS
136 Ok bs -> bs & decodeBS
139 case JSON.eitherDecodeStrict bs of
140 Left err -> returnError ConversionFailed fld err
142 case JSON.fromJSON val of
143 JSON.Error err -> returnError ConversionFailed fld err
144 JSON.Success a -> Ok a
146 toJSONField :: JSON.ToJSON a => a -> SQLData
147 toJSONField = JSON.toJSON >>> toField
149 selectors :: forall a. Gen.DataSelectorNames (Gen.Rep a ()) => Query
151 Gen.dataSelectorNames @(Gen.Rep a ())
152 <&> Gen.dropUntilUnderscore
153 & List.intercalate ","