From e2eb753317a3293df08aa4a295f22c634ea5bf19 Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Tue, 26 Mar 2024 21:59:28 -0500 Subject: [PATCH] feat: init, basic serde --- .tool-versions | 5 +- bun.lockb | Bin 53117 -> 56385 bytes package.json | 3 +- spago.lock | 877 ++++++++++++++++++++++++++++++++ spago.yaml | 33 +- src/Data.Postgres.Geometry.purs | 23 + src/Data.Postgres.Range.js | 71 +++ src/Data.Postgres.Range.purs | 122 +++++ src/Data.Postgres.Raw.js | 30 ++ src/Data.Postgres.Raw.purs | 22 + src/Data.Postgres.js | 37 ++ src/Data.Postgres.purs | 167 ++++++ src/Effect.Postgres.purs | 4 + src/Main.purs | 7 - test/Test.Data.Postgres.purs | 74 +++ test/Test.Main.purs | 13 + 16 files changed, 1464 insertions(+), 24 deletions(-) create mode 100644 spago.lock create mode 100644 src/Data.Postgres.Geometry.purs create mode 100644 src/Data.Postgres.Range.js create mode 100644 src/Data.Postgres.Range.purs create mode 100644 src/Data.Postgres.Raw.js create mode 100644 src/Data.Postgres.Raw.purs create mode 100644 src/Data.Postgres.js create mode 100644 src/Data.Postgres.purs create mode 100644 src/Effect.Postgres.purs delete mode 100644 src/Main.purs create mode 100644 test/Test.Data.Postgres.purs create mode 100644 test/Test.Main.purs diff --git a/.tool-versions b/.tool-versions index 598836b..11affa2 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,2 @@ -bun 1.0.11 -purescript 0.15.12 -nodejs 20.9.0 +bun 1.0.35 +purescript 0.15.15 diff --git a/bun.lockb b/bun.lockb index c42d05ceb16556e06bd80e7e360bf13128ae05e8..8179e335b409e2e7eb6b791248bdb8b0f84b11c3 100755 GIT binary patch delta 11549 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: Range.Range) => RangeRawRecord} */ +export const rangeRawToRecord = r => { + if (r.hasMask(Range.RANGE_EMPTY)) { + return { + upper: undefined, + lower: undefined, + lowerIncl: false, + upperIncl: false, + } + } else { + const upper = r.upper === null ? undefined : r.upper + const lower = r.lower === null ? undefined : r.lower + return { + upper: r.hasMask(Range.RANGE_UB_INF) ? undefined : upper, + lower: r.hasMask(Range.RANGE_LB_INF) ? undefined : lower, + lowerIncl: r.hasMask(Range.RANGE_LB_INC), + upperIncl: r.hasMask(Range.RANGE_UB_INC), + } + } +} + +/** @type {(_: RangeRawRecord) => Range.Range} */ +export const rangeRawFromRecord = r => { + const upper = r.upper === undefined ? null : r.upper + const lower = r.lower === undefined ? null : r.lower + if (upper === null && lower === null) { + // @ts-ignore + return new Range.Range(lower, upper, Range.RANGE_EMPTY) + } + + let mask = 0 + if (upper === null) { + mask |= Range.RANGE_UB_INF + } else if (r.upperIncl) { + mask |= Range.RANGE_UB_INC + } + + if (lower === null) { + mask |= Range.RANGE_LB_INF + } else if (r.lowerIncl) { + mask |= Range.RANGE_LB_INC + } + + return new Range.Range(lower, upper, mask) +} + +/** @type {(r: Range.Range) => () => string} */ +export const rangeRawSerialize = r => () => { + return Range.serialize(r) +} + +/** @type {(r: string) => (f: (s: string) => () => T) => () => Range.Range} */ +export const rangeRawParse = r => f => () => { + return Range.parse(r, s => f(s)()) +} + +/** @type {(r: unknown) => () => Range.Range} */ +export const readRangeRaw = r => () => { + if (r instanceof Range.Range) { + return r + } else { + throw new TypeError(`expected instance of Range, found ${r}`) + } +} diff --git a/src/Data.Postgres.Range.purs b/src/Data.Postgres.Range.purs new file mode 100644 index 0000000..64d5c93 --- /dev/null +++ b/src/Data.Postgres.Range.purs @@ -0,0 +1,122 @@ +module Data.Postgres.Range where + +import Prelude + +import Control.Alt ((<|>)) +import Control.Monad.Trans.Class (lift) +import Data.Generic.Rep (class Generic) +import Data.Maybe (Maybe(..), fromMaybe) +import Data.Newtype (class Newtype, unwrap) +import Data.Postgres (class Deserialize, class Rep, class Serialize, RepT, deserialize, serialize, smash) +import Data.Postgres.Raw (Raw) +import Data.Postgres.Raw as Raw +import Data.Show.Generic (genericShow) +import Effect (Effect) +import Foreign (unsafeToForeign) + +type RangeRawRecord = { upper :: Raw, lower :: Raw, lowerIncl :: Boolean, upperIncl :: Boolean } + +foreign import data RangeRaw :: Type +foreign import readRangeRaw :: Raw -> Effect RangeRaw +foreign import rangeRawToRecord :: RangeRaw -> RangeRawRecord +foreign import rangeRawFromRecord :: RangeRawRecord -> RangeRaw +foreign import rangeRawParse :: String -> (String -> Effect Raw) -> Effect RangeRaw +foreign import rangeRawSerialize :: RangeRaw -> Effect String + +rangeFromRaw :: forall a. Deserialize a => RangeRawRecord -> RepT (Range a) +rangeFromRaw raw = do + upper' :: Maybe a <- deserialize raw.upper + lower' :: Maybe a <- deserialize raw.lower + pure $ Range { upper: makeBound raw.upperIncl <$> upper', lower: makeBound raw.lowerIncl <$> lower' } + +rangeToRaw :: forall a. Serialize a => Range a -> RepT RangeRawRecord +rangeToRaw r = do + upper' <- serialize $ boundValue <$> upper r + lower' <- serialize $ boundValue <$> lower r + pure $ { upper: upper', lower: lower', upperIncl: fromMaybe false $ boundIsInclusive <$> upper r, lowerIncl: fromMaybe false $ boundIsInclusive <$> lower r } + +data Bound a = BoundIncl a | BoundExcl a + +derive instance Generic (Bound a) _ +derive instance Eq a => Eq (Bound a) +instance Show a => Show (Bound a) where + show = genericShow + +boundValue :: forall a. Bound a -> a +boundValue (BoundIncl a) = a +boundValue (BoundExcl a) = a + +boundIsInclusive :: forall a. Bound a -> Boolean +boundIsInclusive (BoundIncl _) = true +boundIsInclusive (BoundExcl _) = false + +upper :: forall a. Range a -> Maybe (Bound a) +upper = _.upper <<< unwrap + +lower :: forall a. Range a -> Maybe (Bound a) +lower = _.lower <<< unwrap + +makeBound :: forall a. Boolean -> a -> Bound a +makeBound i a + | i = BoundIncl a + | otherwise = BoundExcl a + +newtype Range a = Range { upper :: Maybe (Bound a), lower :: Maybe (Bound a) } + +derive instance Generic (Range a) _ +derive instance Newtype (Range a) _ +derive instance Eq a => Eq (Range a) +instance Show a => Show (Range a) where + show = genericShow + +instance (Ord a, Rep a) => Serialize (Range a) where + serialize a = do + raw <- rangeToRaw a + pure $ Raw.unsafeFromForeign $ unsafeToForeign $ rangeRawFromRecord raw + +instance (Ord a, Rep a) => Deserialize (Range a) where + deserialize raw = do + range :: RangeRaw <- lift $ readRangeRaw raw + rangeFromRaw $ rangeRawToRecord range + +instance Monoid (Range a) where + mempty = Range { upper: Nothing, lower: Nothing } + +instance Semigroup (Range a) where + append (Range { upper: au, lower: al }) (Range { upper: bu, lower: bl }) = Range ({ upper: bu <|> au, lower: bl <|> al }) + +parseSQL :: forall a. Rep a => (String -> RepT a) -> String -> RepT (Range a) +parseSQL fromString sql = do + range <- lift $ rangeRawParse sql $ smash <<< (serialize <=< fromString) + rangeFromRaw $ rangeRawToRecord range + +printSQL :: forall a. Rep a => Range a -> RepT String +printSQL range = do + record <- rangeToRaw range + lift $ rangeRawSerialize $ rangeRawFromRecord record + +contains :: forall a. Ord a => a -> Range a -> Boolean +contains a r = + let + upperOk + | Just (BoundIncl u) <- upper r = u >= a + | Just (BoundExcl u) <- upper r = u > a + | otherwise = true + lowerOk + | Just (BoundIncl u) <- lower r = u <= a + | Just (BoundExcl u) <- lower r = u < a + | otherwise = true + in + upperOk && lowerOk + +gte :: forall a. Ord a => a -> Range a +gte a = Range { upper: Just $ BoundIncl a, lower: Nothing } + +gt :: forall a. Ord a => a -> Range a +gt a = Range { upper: Just $ BoundExcl a, lower: Nothing } + +lt :: forall a. Ord a => a -> Range a +lt a = Range { upper: Nothing, lower: Just $ BoundExcl a } + +lte :: forall a. Ord a => a -> Range a +lte a = Range { upper: Nothing, lower: Just $ BoundIncl a } diff --git a/src/Data.Postgres.Raw.js b/src/Data.Postgres.Raw.js new file mode 100644 index 0000000..7acd4fe --- /dev/null +++ b/src/Data.Postgres.Raw.js @@ -0,0 +1,30 @@ +/** @type {(raw: unknown) => string} */ +export const rawToString = raw => + typeof raw === 'undefined' + ? 'undefined' + : typeof raw === 'string' + ? raw + : typeof raw === 'number' || + typeof raw === 'boolean' || + typeof raw === 'symbol' + ? raw.toString() + : typeof raw === 'object' + ? raw === null + ? 'null' + : `[${raw.constructor.name}]` + : 'unknown' + +/** @type {(a: unknown) => (b: unknown) => boolean} */ +export const rawEq = a => b => + typeof a === 'undefined' && typeof b === 'undefined' + ? true + : typeof a === typeof b && + ['number', 'boolean', 'symbol', 'string'].includes(typeof a) + ? a === b + : typeof a === 'object' && typeof b === 'object' + ? a === null && b === null + ? true + : a instanceof Array && b instanceof Array + ? a.every((a_, ix) => rawEq(a_)(b[ix])) + : false + : false diff --git a/src/Data.Postgres.Raw.purs b/src/Data.Postgres.Raw.purs new file mode 100644 index 0000000..990649a --- /dev/null +++ b/src/Data.Postgres.Raw.purs @@ -0,0 +1,22 @@ +module Data.Postgres.Raw where + +import Prelude + +import Foreign (Foreign) +import Unsafe.Coerce (unsafeCoerce) + +foreign import data Raw :: Type +foreign import rawToString :: Raw -> String +foreign import rawEq :: Raw -> Raw -> Boolean + +instance Show Raw where + show = rawToString + +instance Eq Raw where + eq = rawEq + +unsafeFromForeign :: Foreign -> Raw +unsafeFromForeign = unsafeCoerce + +unsafeToForeign :: Raw -> Foreign +unsafeToForeign = unsafeCoerce diff --git a/src/Data.Postgres.js b/src/Data.Postgres.js new file mode 100644 index 0000000..a833d11 --- /dev/null +++ b/src/Data.Postgres.js @@ -0,0 +1,37 @@ +import { getTypeParser, setTypeParser } from 'pg-types' +import * as Range from 'postgres-range' + +export const null_ = null + +export const modifyPgTypes = () => { + const oid = { + 'text[]': 1009, + json: 114, + jsonb: 3802, + 'json[]': 199, + 'jsonb[]': 3807, + timestamp: 1114, + timestamptz: 1184, + 'timestamp[]': 1115, + 'timestamptz[]': 1185, + tsrange: 3908, + tstzrange: 3910, + } + + // @ts-ignore + const asString = a => a + const asStringArray = getTypeParser(oid['text[]']) + const asStringRange = Range.parse + + setTypeParser(oid['json'], asString) + setTypeParser(oid['jsonb'], asString) + setTypeParser(oid['json[]'], asStringArray) + setTypeParser(oid['jsonb[]'], asStringArray) + + setTypeParser(oid['timestamp'], asString) + setTypeParser(oid['timestamptz'], asString) + setTypeParser(oid['timestamp[]'], asStringArray) + setTypeParser(oid['timestamptz[]'], asStringArray) + setTypeParser(oid['tsrange'], asStringRange) + setTypeParser(oid['tstzrange'], asStringRange) +} diff --git a/src/Data.Postgres.purs b/src/Data.Postgres.purs new file mode 100644 index 0000000..75e9fbc --- /dev/null +++ b/src/Data.Postgres.purs @@ -0,0 +1,167 @@ +module Data.Postgres where + +import Prelude + +import Control.Alt ((<|>)) +import Control.Monad.Error.Class (liftEither, liftMaybe) +import Control.Monad.Except (Except, ExceptT, except, runExcept, runExceptT, withExceptT) +import Data.Bifunctor (lmap) +import Data.DateTime (DateTime) +import Data.Generic.Rep (class Generic) +import Data.List.NonEmpty (NonEmptyList) +import Data.Maybe (Maybe(..)) +import Data.Newtype (unwrap, wrap) +import Data.Postgres.Raw (Raw) +import Data.Postgres.Raw (unsafeFromForeign, unsafeToForeign) as Raw +import Data.RFC3339String as DateTime.ISO +import Data.Show.Generic (genericShow) +import Data.Traversable (traverse) +import Effect (Effect) +import Effect.Exception (error) +import Foreign (ForeignError) +import Foreign as F +import Node.Buffer (Buffer) + +newtype JSON = JSON String + +foreign import null_ :: Raw + +-- | Important! This effect MUST be evaluated to guarantee +-- | that (de)serialization will work for timestamp and JSON types. +-- | +-- | This mutates the `pg-types`, overriding the default deserialization +-- | behavior for JSON and timestamp types. +foreign import modifyPgTypes :: Effect Unit + +-- | The SQL value NULL +data Null = Null + +derive instance Generic Null _ +derive instance Eq Null +derive instance Ord Null +instance Show Null where + show = genericShow + +-- | The serialization & deserialization monad. +type RepT a = ExceptT RepError Effect a + +-- | Errors encounterable while serializing & deserializing. +data RepError + = RepErrorTypeMismatch { expected :: String, found :: String } + | RepErrorInvalid String + | RepErrorForeign ForeignError + | RepErrorOther String + | RepErrorMultiple (NonEmptyList RepError) + +derive instance Generic RepError _ +derive instance Eq RepError +instance Show RepError where + show a = genericShow a + +instance Semigroup RepError where + append (RepErrorMultiple as) (RepErrorMultiple bs) = RepErrorMultiple (as <> bs) + append (RepErrorMultiple as) b = RepErrorMultiple (as <> pure b) + append a (RepErrorMultiple bs) = RepErrorMultiple (pure a <> bs) + append a b = RepErrorMultiple (pure a <> pure b) + +-- | Flatten to an Effect, rendering any `RepError`s to `String` using `Show`. +smash :: forall a. RepT a -> Effect a +smash = liftEither <=< map (lmap (error <<< show)) <<< runExceptT + +-- | Lift an `Except` returned by functions in the `Foreign` module to `RepT` +liftForeign :: forall a. Except (NonEmptyList ForeignError) a -> RepT a +liftForeign = except <<< runExcept <<< withExceptT (RepErrorMultiple <<< map RepErrorForeign) + +-- | Serialize data of type `a` to a `Raw` SQL value. +class Serialize a where + serialize :: a -> RepT Raw + +-- | Deserialize data of type `a` from a `Raw` SQL value. +class Deserialize a where + deserialize :: Raw -> RepT a + +-- | A type which is `Rep`resentable as a SQL value. +class (Serialize a, Deserialize a) <= Rep a + +instance (Serialize a, Deserialize a) => Rep a + +-- | Coerces the value to `Raw` +unsafeSerializeCoerce :: forall m a. Monad m => a -> m Raw +unsafeSerializeCoerce = pure <<< Raw.unsafeFromForeign <<< F.unsafeToForeign + +instance Serialize Raw where + serialize = pure + +instance Serialize Null where + serialize _ = unsafeSerializeCoerce null_ + +-- | `bytea` +instance Serialize Buffer where + serialize = unsafeSerializeCoerce + +instance Serialize Int where + serialize = unsafeSerializeCoerce + +instance Serialize Boolean where + serialize = unsafeSerializeCoerce + +instance Serialize String where + serialize = unsafeSerializeCoerce + +instance Serialize Number where + serialize = unsafeSerializeCoerce + +instance Serialize Char where + serialize = unsafeSerializeCoerce + +instance Serialize DateTime where + serialize = serialize <<< unwrap <<< DateTime.ISO.fromDateTime + +instance Serialize a => Serialize (Maybe a) where + serialize (Just a) = serialize a + serialize Nothing = unsafeSerializeCoerce null_ + +instance Serialize a => Serialize (Array a) where + serialize = unsafeSerializeCoerce <=< traverse serialize + +instance Deserialize Raw where + deserialize = pure + +-- | `bytea` +instance Deserialize Buffer where + deserialize = liftForeign <<< (F.unsafeReadTagged "Buffer") <<< Raw.unsafeToForeign + +instance Deserialize Null where + deserialize = map (const Null) <<< liftForeign <<< F.readNullOrUndefined <<< Raw.unsafeToForeign + +instance Deserialize Int where + deserialize = liftForeign <<< F.readInt <<< Raw.unsafeToForeign + +instance Deserialize Boolean where + deserialize = liftForeign <<< F.readBoolean <<< Raw.unsafeToForeign + +instance Deserialize String where + deserialize = liftForeign <<< F.readString <<< Raw.unsafeToForeign + +instance Deserialize Number where + deserialize = liftForeign <<< F.readNumber <<< Raw.unsafeToForeign + +instance Deserialize Char where + deserialize = liftForeign <<< F.readChar <<< Raw.unsafeToForeign + +instance Deserialize DateTime where + deserialize raw = do + s :: String <- deserialize raw + let invalid = RepErrorInvalid $ "Not a valid ISO8601 string: `" <> s <> "`" + liftMaybe invalid $ DateTime.ISO.toDateTime $ wrap s + +instance Deserialize a => Deserialize (Array a) where + deserialize = traverse (deserialize <<< Raw.unsafeFromForeign) <=< liftForeign <<< F.readArray <<< Raw.unsafeToForeign + +instance Deserialize a => Deserialize (Maybe a) where + deserialize raw = + let + nothing = const Nothing <$> deserialize @Null raw + just = Just <$> deserialize raw + in + just <|> nothing diff --git a/src/Effect.Postgres.purs b/src/Effect.Postgres.purs new file mode 100644 index 0000000..81762a7 --- /dev/null +++ b/src/Effect.Postgres.purs @@ -0,0 +1,4 @@ +module Effect.Pg where + +import Prelude + diff --git a/src/Main.purs b/src/Main.purs deleted file mode 100644 index ee561ac..0000000 --- a/src/Main.purs +++ /dev/null @@ -1,7 +0,0 @@ -module Main where - -import Prelude -import Effect (Effect) - -main :: Effect Unit -main = pure unit diff --git a/test/Test.Data.Postgres.purs b/test/Test.Data.Postgres.purs new file mode 100644 index 0000000..2a1858f --- /dev/null +++ b/test/Test.Data.Postgres.purs @@ -0,0 +1,74 @@ +module Test.Data.Postgres where + +import Prelude + +import Control.Monad.Error.Class (liftEither) +import Control.Monad.Except (runExceptT) +import Data.DateTime (DateTime(..)) +import Data.DateTime.Instant as Instant +import Data.Int as Int +import Data.Maybe (Maybe, fromJust, fromMaybe, maybe) +import Data.Newtype (wrap) +import Data.Postgres (class Rep, Null(..), deserialize, null_, serialize, smash) +import Data.Postgres.Range as Range +import Data.Postgres.Raw (Raw) +import Data.Postgres.Raw as Raw +import Data.RFC3339String as DateTime.ISO +import Data.Tuple.Nested (type (/\), (/\)) +import Effect.Class (liftEffect) +import Effect.Console (log) +import Effect.Unsafe (unsafePerformEffect) +import Foreign (unsafeToForeign) +import Partial.Unsafe (unsafePartial) +import Test.QuickCheck (class Arbitrary, (==?)) +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) +import Test.Spec.QuickCheck (quickCheck) + +asRaw :: forall a. a -> Raw +asRaw = Raw.unsafeFromForeign <<< unsafeToForeign + +spec :: Spec Unit +spec = + let + check :: forall @a @x. Eq a => Show a => Arbitrary x => Rep a => String -> (x -> a) -> (a -> Raw) -> Spec Unit + check s xa asRaw_ = + describe s do + it "serialize" $ quickCheck \(x :: x) -> (unsafePerformEffect $ runExceptT $ serialize $ xa x) ==? pure (asRaw_ $ xa x) + it "deserialize" $ quickCheck \(x :: x) -> (unsafePerformEffect $ runExceptT $ deserialize $ asRaw_ $ xa x) ==? pure (xa x) + + check_ :: forall @a. Eq a => Show a => Arbitrary a => Rep a => String -> Spec Unit + check_ s = check @a @a s identity asRaw + + dateTimeFromArbitrary :: Int -> DateTime + dateTimeFromArbitrary = Instant.toDateTime <<< unsafePartial fromJust <<< Instant.instant <<< wrap <<< Int.toNumber + in + describe "Data.Postgres" do + check_ @Int "Int" + check_ @String "String" + check_ @Boolean "Boolean" + check_ @Number "Number" + check_ @Char "Char" + + check @(Maybe String) "Maybe String" identity (maybe null_ asRaw) + check @(Array String) "Array String" identity asRaw + check @DateTime "DateTime" dateTimeFromArbitrary (asRaw <<< DateTime.ISO.fromDateTime) + + describe "Null" do + it "serialize" $ liftEffect $ shouldEqual null_ =<< (smash $ serialize Null) + it "deserialize" $ liftEffect $ shouldEqual Null =<< (smash $ deserialize null_) + + describe "Range" do + it "deserialize" do + quickCheck \(up /\ lo /\ uinc /\ linc :: Int /\ Int /\ Boolean /\ Boolean) -> unsafePerformEffect do + let + record = + { upper: unsafePerformEffect $ smash $ serialize up + , lower: unsafePerformEffect $ smash $ serialize lo + , upperIncl: uinc + , lowerIncl: linc + } + raw = asRaw $ Range.rangeRawFromRecord record + exp :: Range.Range Int <- smash $ Range.rangeFromRaw record + act :: Range.Range Int <- smash $ deserialize raw + pure $ exp ==? act diff --git a/test/Test.Main.purs b/test/Test.Main.purs new file mode 100644 index 0000000..5f5fd3d --- /dev/null +++ b/test/Test.Main.purs @@ -0,0 +1,13 @@ +module Test.Main where + +import Prelude + +import Effect (Effect) +import Effect.Aff (launchAff_) +import Test.Data.Postgres as Test.Data.Postgres +import Test.Spec.Reporter (consoleReporter) +import Test.Spec.Runner (runSpec) + +main :: Effect Unit +main = launchAff_ $ runSpec [ consoleReporter ] do + Test.Data.Postgres.spec