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 c42d05c..8179e33 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index e446309..c21954b 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,12 @@ }, "devDependencies": { "bun-types": "1.0.11", + "postgres-range": "^1.1.4", "purs-tidy": "^0.10.0", "spago": "next" }, "peerDependencies": { "typescript": "^5.0.0" }, - "dependencies": {} + "dependencies": { "pg-types": "^4.0.2" } } diff --git a/spago.lock b/spago.lock new file mode 100644 index 0000000..1b9e047 --- /dev/null +++ b/spago.lock @@ -0,0 +1,877 @@ +workspace: + packages: + pg: + path: ./ + dependencies: + - bifunctors + - control + - datetime + - effect + - exceptions + - foldable-traversable + - foreign + - lists + - maybe + - newtype + - node-buffer + - precise-datetime + - prelude + - transformers + - unsafe-coerce + test_dependencies: + - quickcheck + - spec + - spec-quickcheck + build_plan: + - aff + - ansi + - arraybuffer-types + - arrays + - avar + - bifunctors + - catenable-lists + - console + - const + - contravariant + - control + - datetime + - decimals + - distributive + - effect + - either + - enums + - exceptions + - exists + - fixed-points + - foldable-traversable + - foreign + - fork + - formatters + - free + - functions + - functors + - gen + - identity + - integers + - invariant + - js-date + - lazy + - lcg + - lists + - maybe + - mmorph + - newtype + - node-buffer + - nonempty + - now + - nullable + - numbers + - ordered-collections + - orders + - parallel + - parsing + - partial + - pipes + - precise-datetime + - prelude + - profunctor + - quickcheck + - random + - record + - refs + - safe-coerce + - spec + - spec-quickcheck + - st + - strings + - tailrec + - transformers + - tuples + - type-equality + - unfoldable + - unicode + - unsafe-coerce + extra_packages: {} +packages: + aff: + type: registry + version: 7.1.0 + integrity: sha256-7hOC6uQO9XBAI5FD8F33ChLjFAiZVfd4BJMqlMh7TNU= + dependencies: + - arrays + - bifunctors + - control + - datetime + - effect + - either + - exceptions + - foldable-traversable + - functions + - maybe + - newtype + - parallel + - prelude + - refs + - tailrec + - transformers + - unsafe-coerce + ansi: + type: registry + version: 7.0.0 + integrity: sha256-ZMB6HD+q9CXvn9fRCmJ8dvuDrOVHcjombL3oNOerVnE= + dependencies: + - foldable-traversable + - lists + - strings + arraybuffer-types: + type: registry + version: 3.0.2 + integrity: sha256-mQKokysYVkooS4uXbO+yovmV/s8b138Ws3zQvOwIHRA= + dependencies: [] + arrays: + type: registry + version: 7.3.0 + integrity: sha256-tmcklBlc/muUtUfr9RapdCPwnlQeB3aSrC4dK85gQlc= + dependencies: + - bifunctors + - control + - foldable-traversable + - functions + - maybe + - nonempty + - partial + - prelude + - safe-coerce + - st + - tailrec + - tuples + - unfoldable + - unsafe-coerce + avar: + type: registry + version: 5.0.0 + integrity: sha256-e7hf0x4hEpcygXP0LtvfvAQ49Bbj2aWtZT3gqM///0A= + dependencies: + - aff + - effect + - either + - exceptions + - functions + - maybe + bifunctors: + type: registry + version: 6.0.0 + integrity: sha256-/gZwC9YhNxZNQpnHa5BIYerCGM2jeX9ukZiEvYxm5Nw= + dependencies: + - const + - either + - newtype + - prelude + - tuples + catenable-lists: + type: registry + version: 7.0.0 + integrity: sha256-76vYENhwF4BWTBsjeLuErCH2jqVT4M3R1HX+4RwSftA= + dependencies: + - control + - foldable-traversable + - lists + - maybe + - prelude + - tuples + - unfoldable + console: + type: registry + version: 6.1.0 + integrity: sha256-CxmAzjgyuGDmt9FZW51VhV6rBPwR6o0YeKUzA9rSzcM= + dependencies: + - effect + - prelude + const: + type: registry + version: 6.0.0 + integrity: sha256-tNrxDW8D8H4jdHE2HiPzpLy08zkzJMmGHdRqt5BQuTc= + dependencies: + - invariant + - newtype + - prelude + contravariant: + type: registry + version: 6.0.0 + integrity: sha256-TP+ooAp3vvmdjfQsQJSichF5B4BPDHp3wAJoWchip6c= + dependencies: + - const + - either + - newtype + - prelude + - tuples + control: + type: registry + version: 6.0.0 + integrity: sha256-sH7Pg9E96JCPF9PIA6oQ8+BjTyO/BH1ZuE/bOcyj4Jk= + dependencies: + - newtype + - prelude + datetime: + type: registry + version: 6.1.0 + integrity: sha256-g/5X5BBegQWLpI9IWD+sY6mcaYpzzlW5lz5NBzaMtyI= + dependencies: + - bifunctors + - control + - either + - enums + - foldable-traversable + - functions + - gen + - integers + - lists + - maybe + - newtype + - numbers + - ordered-collections + - partial + - prelude + - tuples + decimals: + type: registry + version: 7.1.0 + integrity: sha256-DriR6lPEfFpjVv7e4JAQkr3ZLf0h17Qg2cAIrhxWV7w= + dependencies: + - maybe + distributive: + type: registry + version: 6.0.0 + integrity: sha256-HTDdmEnzigMl+02SJB88j+gAXDx9VKsbvR4MJGDPbOQ= + dependencies: + - identity + - newtype + - prelude + - tuples + - type-equality + effect: + type: registry + version: 4.0.0 + integrity: sha256-eBtZu+HZcMa5HilvI6kaDyVX3ji8p0W9MGKy2K4T6+M= + dependencies: + - prelude + either: + type: registry + version: 6.1.0 + integrity: sha256-6hgTPisnMWVwQivOu2PKYcH8uqjEOOqDyaDQVUchTpY= + dependencies: + - control + - invariant + - maybe + - prelude + enums: + type: registry + version: 6.0.1 + integrity: sha256-HWaD73JFLorc4A6trKIRUeDMdzE+GpkJaEOM1nTNkC8= + dependencies: + - control + - either + - gen + - maybe + - newtype + - nonempty + - partial + - prelude + - tuples + - unfoldable + exceptions: + type: registry + version: 6.0.0 + integrity: sha256-y/xTAEIZIARCE+50/u1di0ncebJ+CIwNOLswyOWzMTw= + dependencies: + - effect + - either + - maybe + - prelude + exists: + type: registry + version: 6.0.0 + integrity: sha256-A0JQHpTfo1dNOj9U5/Fd3xndlRSE0g2IQWOGor2yXn8= + dependencies: + - unsafe-coerce + fixed-points: + type: registry + version: 7.0.0 + integrity: sha256-hTl5fzeG4mzAOFzEzAeNH7kJvJgYCH7x3v2NdX9pOE4= + dependencies: + - exists + - newtype + - prelude + - transformers + foldable-traversable: + type: registry + version: 6.0.0 + integrity: sha256-fLeqRYM4jUrZD5H4WqcwUgzU7XfYkzO4zhgtNc3jcWM= + dependencies: + - bifunctors + - const + - control + - either + - functors + - identity + - maybe + - newtype + - orders + - prelude + - tuples + foreign: + type: registry + version: 7.0.0 + integrity: sha256-1ORiqoS3HW+qfwSZAppHPWy4/6AQysxZ2t29jcdUMNA= + dependencies: + - either + - functions + - identity + - integers + - lists + - maybe + - prelude + - strings + - transformers + fork: + type: registry + version: 6.0.0 + integrity: sha256-X7u0SuCvFbLbzuNEKLBNuWjmcroqMqit4xEzpQwAP7E= + dependencies: + - aff + formatters: + type: registry + version: 7.0.0 + integrity: sha256-5JaC9d2p0xoqJWjWxlHH19R4iJwFTBr4j7SlYcLgicE= + dependencies: + - datetime + - fixed-points + - lists + - numbers + - parsing + - prelude + - transformers + free: + type: registry + version: 7.1.0 + integrity: sha256-JAumgEsGSzJCNLD8AaFvuX7CpqS5yruCngi6yI7+V5k= + dependencies: + - catenable-lists + - control + - distributive + - either + - exists + - foldable-traversable + - invariant + - lazy + - maybe + - prelude + - tailrec + - transformers + - tuples + - unsafe-coerce + functions: + type: registry + version: 6.0.0 + integrity: sha256-adMyJNEnhGde2unHHAP79gPtlNjNqzgLB8arEOn9hLI= + dependencies: + - prelude + functors: + type: registry + version: 5.0.0 + integrity: sha256-zfPWWYisbD84MqwpJSZFlvM6v86McM68ob8p9s27ywU= + dependencies: + - bifunctors + - const + - contravariant + - control + - distributive + - either + - invariant + - maybe + - newtype + - prelude + - profunctor + - tuples + - unsafe-coerce + gen: + type: registry + version: 4.0.0 + integrity: sha256-f7yzAXWwr+xnaqEOcvyO3ezKdoes8+WXWdXIHDBCAPI= + dependencies: + - either + - foldable-traversable + - identity + - maybe + - newtype + - nonempty + - prelude + - tailrec + - tuples + - unfoldable + identity: + type: registry + version: 6.0.0 + integrity: sha256-4wY0XZbAksjY6UAg99WkuKyJlQlWAfTi2ssadH0wVMY= + dependencies: + - control + - invariant + - newtype + - prelude + integers: + type: registry + version: 6.0.0 + integrity: sha256-sf+sK26R1hzwl3NhXR7WAu9zCDjQnfoXwcyGoseX158= + dependencies: + - maybe + - numbers + - prelude + invariant: + type: registry + version: 6.0.0 + integrity: sha256-RGWWyYrz0Hs1KjPDA+87Kia67ZFBhfJ5lMGOMCEFoLo= + dependencies: + - control + - prelude + js-date: + type: registry + version: 8.0.0 + integrity: sha256-6TVF4DWg5JL+jRAsoMssYw8rgOVALMUHT1CuNZt8NRo= + dependencies: + - datetime + - effect + - exceptions + - foreign + - integers + - now + lazy: + type: registry + version: 6.0.0 + integrity: sha256-lMsfFOnlqfe4KzRRiW8ot5ge6HtcU3Eyh2XkXcP5IgU= + dependencies: + - control + - foldable-traversable + - invariant + - prelude + lcg: + type: registry + version: 4.0.0 + integrity: sha256-h7ME5cthLfbgJOJdsZcSfFpwXsx4rf8YmhebU+3iSYg= + dependencies: + - effect + - integers + - maybe + - partial + - prelude + - random + lists: + type: registry + version: 7.0.0 + integrity: sha256-EKF15qYqucuXP2lT/xPxhqy58f0FFT6KHdIB/yBOayI= + dependencies: + - bifunctors + - control + - foldable-traversable + - lazy + - maybe + - newtype + - nonempty + - partial + - prelude + - tailrec + - tuples + - unfoldable + maybe: + type: registry + version: 6.0.0 + integrity: sha256-5cCIb0wPwbat2PRkQhUeZO0jcAmf8jCt2qE0wbC3v2Q= + dependencies: + - control + - invariant + - newtype + - prelude + mmorph: + type: registry + version: 7.0.0 + integrity: sha256-urZlZNNqGeQFe5D/ClHlR8QgGBNHTMFPtJ5S5IpflTQ= + dependencies: + - free + - functors + - transformers + newtype: + type: registry + version: 5.0.0 + integrity: sha256-gdrQu8oGe9eZE6L3wOI8ql/igOg+zEGB5ITh2g+uttw= + dependencies: + - prelude + - safe-coerce + node-buffer: + type: registry + version: 9.0.0 + integrity: sha256-PWE2DJ5ruBLCmeA/fUiuySEFmUJ/VuRfyrnCuVZBlu4= + dependencies: + - arraybuffer-types + - effect + - maybe + - nullable + - st + - unsafe-coerce + nonempty: + type: registry + version: 7.0.0 + integrity: sha256-54ablJZUHGvvlTJzi3oXyPCuvY6zsrWJuH/dMJ/MFLs= + dependencies: + - control + - foldable-traversable + - maybe + - prelude + - tuples + - unfoldable + now: + type: registry + version: 6.0.0 + integrity: sha256-xZ7x37ZMREfs6GCDw/h+FaKHV/3sPWmtqBZRGTxybQY= + dependencies: + - datetime + - effect + nullable: + type: registry + version: 6.0.0 + integrity: sha256-yiGBVl3AD+Guy4kNWWeN+zl1gCiJK+oeIFtZtPCw4+o= + dependencies: + - effect + - functions + - maybe + numbers: + type: registry + version: 9.0.1 + integrity: sha256-/9M6aeMDBdB4cwYDeJvLFprAHZ49EbtKQLIJsneXLIk= + dependencies: + - functions + - maybe + ordered-collections: + type: registry + version: 3.2.0 + integrity: sha256-o9jqsj5rpJmMdoe/zyufWHFjYYFTTsJpgcuCnqCO6PM= + dependencies: + - arrays + - foldable-traversable + - gen + - lists + - maybe + - partial + - prelude + - st + - tailrec + - tuples + - unfoldable + orders: + type: registry + version: 6.0.0 + integrity: sha256-nBA0g3/ai0euH8q9pSbGqk53W2q6agm/dECZTHcoink= + dependencies: + - newtype + - prelude + parallel: + type: registry + version: 6.0.0 + integrity: sha256-VJbkGD0rAKX+NUEeBJbYJ78bEKaZbgow+QwQEfPB6ko= + dependencies: + - control + - effect + - either + - foldable-traversable + - functors + - maybe + - newtype + - prelude + - profunctor + - refs + - transformers + parsing: + type: registry + version: 10.2.0 + integrity: sha256-ZDIdMFAKkst57x6BVa1aUWJnS8smoZnXsZ339Aq1mPA= + dependencies: + - arrays + - control + - effect + - either + - enums + - foldable-traversable + - functions + - identity + - integers + - lazy + - lists + - maybe + - newtype + - nullable + - numbers + - partial + - prelude + - st + - strings + - tailrec + - transformers + - tuples + - unfoldable + - unicode + - unsafe-coerce + partial: + type: registry + version: 4.0.0 + integrity: sha256-fwXerld6Xw1VkReh8yeQsdtLVrjfGiVuC5bA1Wyo/J4= + dependencies: [] + pipes: + type: registry + version: 8.0.0 + integrity: sha256-kvfqGM4cPA/wCcBHbp5psouFw5dZGvku2462x7ZBwSY= + dependencies: + - aff + - lists + - mmorph + - prelude + - tailrec + - transformers + - tuples + precise-datetime: + type: registry + version: 7.0.0 + integrity: sha256-F7tzZ7++Ihtg3xjumzwaHQvGQg61UtEAe5MWeOlTzRY= + dependencies: + - arrays + - datetime + - decimals + - either + - enums + - foldable-traversable + - formatters + - integers + - js-date + - lists + - maybe + - newtype + - numbers + - prelude + - strings + - tuples + - unicode + prelude: + type: registry + version: 6.0.1 + integrity: sha256-o8p6SLYmVPqzXZhQFd2hGAWEwBoXl1swxLG/scpJ0V0= + dependencies: [] + profunctor: + type: registry + version: 6.0.1 + integrity: sha256-E58hSYdJvF2Qjf9dnWLPlJKh2Z2fLfFLkQoYi16vsFk= + dependencies: + - control + - distributive + - either + - exists + - invariant + - newtype + - prelude + - tuples + quickcheck: + type: registry + version: 8.0.1 + integrity: sha256-ZvpccKQCvgslTXZCNmpYW4bUsFzhZd/kQUr2WmxFTGY= + dependencies: + - arrays + - console + - control + - effect + - either + - enums + - exceptions + - foldable-traversable + - gen + - identity + - integers + - lazy + - lcg + - lists + - maybe + - newtype + - nonempty + - numbers + - partial + - prelude + - record + - st + - strings + - tailrec + - transformers + - tuples + - unfoldable + random: + type: registry + version: 6.0.0 + integrity: sha256-CJ611a35MPCE7XQMp0rdC6MCn76znlhisiCRgboAG+Q= + dependencies: + - effect + - integers + record: + type: registry + version: 4.0.0 + integrity: sha256-Za5U85bTRJEfGK5Sk4hM41oXy84YQI0I8TL3WUn1Qzg= + dependencies: + - functions + - prelude + - unsafe-coerce + refs: + type: registry + version: 6.0.0 + integrity: sha256-Vgwne7jIbD3ZMoLNNETLT8Litw6lIYo3MfYNdtYWj9s= + dependencies: + - effect + - prelude + safe-coerce: + type: registry + version: 2.0.0 + integrity: sha256-a1ibQkiUcbODbLE/WAq7Ttbbh9ex+x33VCQ7GngKudU= + dependencies: + - unsafe-coerce + spec: + type: registry + version: 7.6.0 + integrity: sha256-+merGdQbL9zWONbnt8S8J9afGJ59MQqGtS0qSd3yu4I= + dependencies: + - aff + - ansi + - arrays + - avar + - bifunctors + - control + - datetime + - effect + - either + - exceptions + - foldable-traversable + - fork + - identity + - integers + - lists + - maybe + - newtype + - now + - ordered-collections + - parallel + - pipes + - prelude + - refs + - strings + - tailrec + - transformers + - tuples + spec-quickcheck: + type: registry + version: 5.0.0 + integrity: sha256-iE0iThqZCuDGe3pwg5RvqcL8E5cRQ4txDuloCclOsCs= + dependencies: + - aff + - prelude + - quickcheck + - random + - spec + st: + type: registry + version: 6.2.0 + integrity: sha256-z9X0WsOUlPwNx9GlCC+YccCyz8MejC8Wb0C4+9fiBRY= + dependencies: + - partial + - prelude + - tailrec + - unsafe-coerce + strings: + type: registry + version: 6.0.1 + integrity: sha256-WssD3DbX4OPzxSdjvRMX0yvc9+pS7n5gyPv5I2Trb7k= + dependencies: + - arrays + - control + - either + - enums + - foldable-traversable + - gen + - integers + - maybe + - newtype + - nonempty + - partial + - prelude + - tailrec + - tuples + - unfoldable + - unsafe-coerce + tailrec: + type: registry + version: 6.1.0 + integrity: sha256-Xx19ECVDRrDWpz9D2GxQHHV89vd61dnXxQm0IcYQHGk= + dependencies: + - bifunctors + - effect + - either + - identity + - maybe + - partial + - prelude + - refs + transformers: + type: registry + version: 6.0.0 + integrity: sha256-Pzw40HjthX77tdPAYzjx43LK3X5Bb7ZspYAp27wksFA= + dependencies: + - control + - distributive + - effect + - either + - exceptions + - foldable-traversable + - identity + - lazy + - maybe + - newtype + - prelude + - tailrec + - tuples + - unfoldable + tuples: + type: registry + version: 7.0.0 + integrity: sha256-1rXgTomes9105BjgXqIw0FL6Fz1lqqUTLWOumhWec1M= + dependencies: + - control + - invariant + - prelude + type-equality: + type: registry + version: 4.0.1 + integrity: sha256-Hs9D6Y71zFi/b+qu5NSbuadUQXe5iv5iWx0226vOHUw= + dependencies: [] + unfoldable: + type: registry + version: 6.0.0 + integrity: sha256-JtikvJdktRap7vr/K4ITlxUX1QexpnqBq0G/InLr6eg= + dependencies: + - foldable-traversable + - maybe + - partial + - prelude + - tuples + unicode: + type: registry + version: 6.0.0 + integrity: sha256-QJnTVZpmihEAUiMeYrfkusVoziJWp2hJsgi9bMQLue8= + dependencies: + - foldable-traversable + - maybe + - strings + unsafe-coerce: + type: registry + version: 6.0.0 + integrity: sha256-IqIYW4Vkevn8sI+6aUwRGvd87tVL36BBeOr0cGAE7t0= + dependencies: [] diff --git a/spago.yaml b/spago.yaml index c5aa839..c7cd20c 100644 --- a/spago.yaml +++ b/spago.yaml @@ -1,24 +1,31 @@ package: + name: pg build: strict: true pedantic_packages: true dependencies: - - prelude - - aff + - bifunctors + - control + - datetime - effect - - either - - maybe + - exceptions - foldable-traversable - - console + - foreign + - lists + - maybe - newtype - - strings - - stringutils + - node-buffer + - precise-datetime + - prelude - transformers - - tuples - - typelevel-prelude - name: project + - unsafe-coerce + test: + main: Test.Main + dependencies: + - quickcheck + - spec + - spec-quickcheck workspace: extra_packages: {} - package_set: - url: https://raw.githubusercontent.com/purescript/package-sets/psc-0.15.10-20230930/packages.json - hash: sha256-nTsd44o7/hrTdk0c6dh0wyBqhFFDJJIeKdQU6L1zv/A= + packageSet: + registry: 50.5.0 diff --git a/src/Data.Postgres.Geometry.purs b/src/Data.Postgres.Geometry.purs new file mode 100644 index 0000000..f37de52 --- /dev/null +++ b/src/Data.Postgres.Geometry.purs @@ -0,0 +1,23 @@ +module Data.Postgres.Geometry where + +import Prelude + +import Data.Generic.Rep (class Generic) +import Data.Newtype (class Newtype) +import Data.Show.Generic (genericShow) + +newtype Point = Point { x :: Number, y :: Number } + +derive instance Newtype Point _ +derive instance Generic Point _ +derive instance Eq Point +instance Show Point where + show = genericShow + +newtype Circle = Circle { center :: Point, radius :: Number } + +derive instance Newtype Circle _ +derive instance Generic Circle _ +derive instance Eq Circle +instance Show Circle where + show = genericShow diff --git a/src/Data.Postgres.Range.js b/src/Data.Postgres.Range.js new file mode 100644 index 0000000..31140be --- /dev/null +++ b/src/Data.Postgres.Range.js @@ -0,0 +1,71 @@ +import * as Range from 'postgres-range' + +/** + * @template T + * @typedef {{upper: T | undefined, lower: T | undefined, lowerIncl: boolean, upperIncl: boolean}} RangeRawRecord + */ + +/** @type {(_: 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