git.iko.soy/rowtype-yoga/purescript-yoga-scylladb
on main help sign in
36 files changed+2847−17
since splitsnapshots
unifiedsplit
README.md+1 bun.lock+22 compile-fail/AllowFilteringOnInsert.purs+16 compile-fail/AllowFilteringWithoutSelect.purs+15 compile-fail/DeleteThenSet.purs+15 compile-fail/DoubleAllowFiltering.purs+15 compile-fail/DoubleLimit.purs+15 compile-fail/DoubleOrderBy.purs+15 compile-fail/DoubleSelect.purs+15 compile-fail/DoubleWhere.purs+15 compile-fail/IfExistsWithoutWhere.purs+15 compile-fail/IfNotExistsOnDelete.purs+15 compile-fail/IfNotExistsWithoutInsert.purs+15 compile-fail/InsertAfterSelect.purs+16 compile-fail/LimitWithoutSelect.purs+15 compile-fail/OrderByAfterLimit.purs+15 compile-fail/OrderByWithoutSelect.purs+15 compile-fail/SelectAfterDelete.purs+15 compile-fail/SelectNonexistentColumn.purs+15 compile-fail/SetNonexistentColumn.purs+15 compile-fail/UsingTTLWithoutInsert.purs+15 compile-fail/WhereAfterInsert.purs+16 compile-fail/WhereAfterLimit.purs+15 compile-fail/WhereAfterOrderBy.purs+15 compile-fail/WhereBeforeDML.purs+15 compile-fail/WhereNonexistentParam.purs+15 compile-fail/WithTTLAfterSelect.purs+15 compile-fail/run.sh+45 package.json+3 spago.lock+1055 spago.yaml+23−10 src/Yoga/ScyllaDB/Schema.purs+1089 src/Yoga/ScyllaDB/ScyllaDB.purs+11−2 test/Main.purs+6−5 test/Yoga/ScyllaDB/Schema.test.js+2 test/Yoga/ScyllaDB/Schema.test.purs+212
README.mdnew file+1
@@ -0,0 +1 @@
1+
bun.locknew file+22
@@ -0,0 +1,22 @@
1+{
2+ "lockfileVersion": 1,
3+ "workspaces": {
4+ "": {
5+ "name": "purescript-yoga-scylladb",
6+ "dependencies": {
7+ "cassandra-driver": "^4.8.0",
8+ },
9+ },
10+ },
11+ "packages": {
12+ "@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
13+
14+ "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
15+
16+ "cassandra-driver": ["cassandra-driver@4.8.0", "", { "dependencies": { "@types/node": "^18.11.18", "adm-zip": "~0.5.10", "long": "~5.2.3" } }, "sha512-HritfMGq9V7SuESeSodHvArs0mLuMk7uh+7hQK2lqdvXrvm50aWxb4RPxkK3mPDdsgHjJ427xNRFITMH2ei+Sw=="],
17+
18+ "long": ["long@5.2.5", "", {}, "sha512-e0r9YBBgNCq1D1o5Dp8FMH0N5hsFtXDBiVa0qoJPHpakvZkmDKPRoGffZJII/XsHvj9An9blm+cRJ01yQqU+Dw=="],
19+
20+ "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
21+ }
22+}
compile-fail/AllowFilteringOnInsert.pursnew file+16
@@ -0,0 +1,16 @@
1+-- expect: HasClause "select"
2+module CompileFail.AllowFilteringOnInsert where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+import Unsafe.Coerce (unsafeCoerce)
10+
11+type UsersTable = S.Table "users"
12+ ( id :: Scylla.UUID # S.PartitionKey
13+ , name :: String
14+ )
15+
16+bad = S.from (Proxy :: Proxy UsersTable) # S.insert { id: (unsafeCoerce "" :: Scylla.UUID), name: "x" } # S.allowFiltering
compile-fail/AllowFilteringWithoutSelect.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: HasClause "select"
2+module CompileFail.AllowFilteringWithoutSelect where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.delete # S.allowFiltering
compile-fail/DeleteThenSet.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "delete"
2+module CompileFail.DeleteThenSet where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.delete # S.set { name: "x" }
compile-fail/DoubleAllowFiltering.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "allowFiltering"
2+module CompileFail.DoubleAllowFiltering where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.where_ @"name = $name" # S.allowFiltering # S.allowFiltering
compile-fail/DoubleLimit.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "limit"
2+module CompileFail.DoubleLimit where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.limit @"10" # S.limit @"5"
compile-fail/DoubleOrderBy.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "orderBy"
2+module CompileFail.DoubleOrderBy where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.orderBy @"name" # S.orderBy @"id"
compile-fail/DoubleSelect.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "select"
2+module CompileFail.DoubleSelect where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.selectAll
compile-fail/DoubleWhere.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "where"
2+module CompileFail.DoubleWhere where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.where_ @"id = $id" # S.where_ @"name = $name"
compile-fail/IfExistsWithoutWhere.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: HasClause "where"
2+module CompileFail.IfExistsWithoutWhere where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.delete # S.ifExists
compile-fail/IfNotExistsOnDelete.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: HasClause "insert"
2+module CompileFail.IfNotExistsOnDelete where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.delete # S.where_ @"id = $id" # S.ifNotExists
compile-fail/IfNotExistsWithoutInsert.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: HasClause "insert"
2+module CompileFail.IfNotExistsWithoutInsert where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.ifNotExists
compile-fail/InsertAfterSelect.pursnew file+16
@@ -0,0 +1,16 @@
1+-- expect: Lacks @Type "select"
2+module CompileFail.InsertAfterSelect where
3+
4+import Prelude
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+import Unsafe.Coerce (unsafeCoerce)
10+
11+type UsersTable = S.Table "users"
12+ ( id :: Scylla.UUID # S.PartitionKey
13+ , name :: String
14+ )
15+
16+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.insert { id: (unsafeCoerce "" :: Scylla.UUID), name: "x" }
compile-fail/LimitWithoutSelect.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: HasClause "select"
2+module CompileFail.LimitWithoutSelect where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.delete # S.limit @"10"
compile-fail/OrderByAfterLimit.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "limit"
2+module CompileFail.OrderByAfterLimit where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.limit @"10" # S.orderBy @"name"
compile-fail/OrderByWithoutSelect.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: HasClause "select"
2+module CompileFail.OrderByWithoutSelect where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.orderBy @"id DESC"
compile-fail/SelectAfterDelete.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "delete"
2+module CompileFail.SelectAfterDelete where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.delete # S.selectAll
compile-fail/SelectNonexistentColumn.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Column not found in table
2+module CompileFail.SelectNonexistentColumn where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.select @"nonexistent"
compile-fail/SetNonexistentColumn.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Could not match type
2+module CompileFail.SetNonexistentColumn where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.set { bogus: "x" }
compile-fail/UsingTTLWithoutInsert.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: HasClause "insert"
2+module CompileFail.UsingTTLWithoutInsert where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.usingTTL @"3600"
compile-fail/WhereAfterInsert.pursnew file+16
@@ -0,0 +1,16 @@
1+-- expect: WHERE requires a preceding SELECT, UPDATE (set), or DELETE
2+module CompileFail.WhereAfterInsert where
3+
4+import Prelude
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+import Unsafe.Coerce (unsafeCoerce)
10+
11+type UsersTable = S.Table "users"
12+ ( id :: Scylla.UUID # S.PartitionKey
13+ , name :: String
14+ )
15+
16+bad = S.from (Proxy :: Proxy UsersTable) # S.insert { id: (unsafeCoerce "" :: Scylla.UUID), name: "x" } # S.where_ @"id = $id"
compile-fail/WhereAfterLimit.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "limit"
2+module CompileFail.WhereAfterLimit where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.limit @"10" # S.where_ @"id = $id"
compile-fail/WhereAfterOrderBy.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "orderBy"
2+module CompileFail.WhereAfterOrderBy where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.orderBy @"name" # S.where_ @"id = $id"
compile-fail/WhereBeforeDML.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: WHERE requires a preceding SELECT, UPDATE (set), or DELETE
2+module CompileFail.WhereBeforeDML where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.where_ @"id = $id"
compile-fail/WhereNonexistentParam.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Column not found in table
2+module CompileFail.WhereNonexistentParam where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.where_ @"bogus = $bogus"
compile-fail/WithTTLAfterSelect.pursnew file+15
@@ -0,0 +1,15 @@
1+-- expect: Lacks "select"
2+module CompileFail.WithTTLAfterSelect where
3+import Prelude
4+
5+import Yoga.ScyllaDB.Schema as S
6+import Yoga.ScyllaDB.ScyllaDB as Scylla
7+import Type.Function (type (#))
8+import Type.Proxy (Proxy(..))
9+
10+type UsersTable = S.Table "users"
11+ ( id :: Scylla.UUID # S.PartitionKey
12+ , name :: String
13+ )
14+
15+bad = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.withTTL @"3600"
compile-fail/run.shnew file+45
@@ -0,0 +1,45 @@
1+#!/usr/bin/env bash
2+set -euo pipefail
3+
4+cd "$(dirname "$0")/.."
5+
6+PASS=0
7+FAIL=0
8+ERRORS=()
9+
10+for f in compile-fail/*.purs; do
11+ name=$(basename "$f" .purs)
12+ # Extract expected error pattern from first line comment: -- expect: <pattern>
13+ expected=$(head -1 "$f" | sed -n 's/^-- expect: //p')
14+
15+ output=$(eval "purs compile 'src/**/*.purs' '.spago/p/*/src/**/*.purs' '$f' --output output-compile-fail" 2>&1 || true)
16+
17+ if echo "$output" | grep -q "^Error found:"; then
18+ if [ -n "$expected" ]; then
19+ if echo "$output" | grep -q "$expected"; then
20+ printf " \033[32m✓\033[0m %s \033[2m(%s)\033[0m\n" "$name" "$expected"
21+ ((PASS++))
22+ else
23+ ERRORS+=("WRONG ERROR: $name — expected '$expected' but got different error")
24+ ((FAIL++))
25+ fi
26+ else
27+ printf " \033[32m✓\033[0m %s correctly rejected\n" "$name"
28+ ((PASS++))
29+ fi
30+ else
31+ ERRORS+=("UNEXPECTED PASS: $name compiled but should have failed")
32+ ((FAIL++))
33+ fi
34+done
35+
36+rm -rf output-compile-fail
37+
38+echo ""
39+echo "Compile-fail: $PASS passed, $FAIL failed"
40+
41+for err in "${ERRORS[@]+"${ERRORS[@]}"}"; do
42+ printf " \033[31m✗ %s\033[0m\n" "$err"
43+done
44+
45+[ "$FAIL" -eq 0 ]
package.json+3
@@ -18,5 +18,8 @@
1818 "test": "spago test",
1919 "test:bash": "./test-with-docker.sh",
2020 "test:ts": "bun run test-with-docker.ts"
21+ },
22+ "dependencies": {
23+ "cassandra-driver": "^4.8.0"
2124 }
2225}
spago.locknew file+1055
@@ -0,0 +1,1055 @@
1+{
2+ "workspace": {
3+ "packages": {
4+ "yoga-scylladb": {
5+ "path": "./",
6+ "core": {
7+ "dependencies": [
8+ {
9+ "aff": ">=7.1.0 <8.0.0"
10+ },
11+ {
12+ "arrays": ">=7.3.0 <8.0.0"
13+ },
14+ {
15+ "datetime": ">=6.1.0 <7.0.0"
16+ },
17+ {
18+ "effect": ">=4.0.0 <5.0.0"
19+ },
20+ {
21+ "either": ">=6.1.0 <7.0.0"
22+ },
23+ {
24+ "exceptions": ">=6.1.0 <7.0.0"
25+ },
26+ {
27+ "foldable-traversable": ">=6.0.0 <7.0.0"
28+ },
29+ {
30+ "foreign": ">=7.0.0 <8.0.0"
31+ },
32+ {
33+ "foreign-object": ">=4.1.0 <5.0.0"
34+ },
35+ {
36+ "heterogeneous": ">=0.7.0 <0.8.0"
37+ },
38+ {
39+ "js-promise": ">=1.0.0 <2.0.0"
40+ },
41+ {
42+ "js-promise-aff": ">=1.0.0 <2.0.0"
43+ },
44+ {
45+ "maybe": ">=6.0.0 <7.0.0"
46+ },
47+ {
48+ "newtype": ">=5.0.0 <6.0.0"
49+ },
50+ {
51+ "nullable": ">=6.0.0 <7.0.0"
52+ },
53+ {
54+ "ordered-collections": ">=3.2.0 <4.0.0"
55+ },
56+ {
57+ "prelude": ">=6.0.2 <7.0.0"
58+ },
59+ {
60+ "record": ">=4.0.0 <5.0.0"
61+ },
62+ {
63+ "strings": ">=6.0.1 <7.0.0"
64+ },
65+ {
66+ "transformers": ">=6.1.0 <7.0.0"
67+ },
68+ {
69+ "typelevel-prelude": ">=7.0.0 <8.0.0"
70+ },
71+ {
72+ "unsafe-coerce": ">=6.0.0 <7.0.0"
73+ },
74+ {
75+ "yoga-json": ">=5.2.1 <6.0.0"
76+ },
77+ {
78+ "yoga-sql-types": ">=0.2.0 <0.3.0"
79+ }
80+ ]
81+ },
82+ "test": {
83+ "dependencies": [
84+ {
85+ "console": ">=6.0.0 <7.0.0"
86+ },
87+ {
88+ "exceptions": ">=6.0.0 <7.0.0"
89+ },
90+ {
91+ "foreign": ">=7.0.0 <8.0.0"
92+ },
93+ {
94+ "maybe": ">=6.0.0 <7.0.0"
95+ },
96+ {
97+ "newtype": ">=5.0.0 <6.0.0"
98+ },
99+ {
100+ "spec": ">=7.0.0 <8.0.0"
101+ },
102+ "yoga-test-docker"
103+ ]
104+ }
105+ }
106+ },
107+ "extra_packages": {}
108+ },
109+ "packages": {
110+ "aff": {
111+ "type": "registry",
112+ "version": "7.1.0",
113+ "integrity": "sha256-/nOpDQVnKIoP1pQ1ZZBYpadK0TuMlG4WwufGzJpsfD0=",
114+ "dependencies": [
115+ "control",
116+ "datetime",
117+ "effect",
118+ "either",
119+ "exceptions",
120+ "functions",
121+ "newtype",
122+ "parallel",
123+ "partial",
124+ "prelude",
125+ "st",
126+ "tailrec",
127+ "transformers",
128+ "unsafe-coerce"
129+ ]
130+ },
131+ "ansi": {
132+ "type": "registry",
133+ "version": "7.0.0",
134+ "integrity": "sha256-tOUaXJFzPSfBVAqLI8hc+Tz2M1rg16cXqyHaK/3SmFg=",
135+ "dependencies": [
136+ "foldable-traversable",
137+ "lists",
138+ "prelude"
139+ ]
140+ },
141+ "arrays": {
142+ "type": "registry",
143+ "version": "7.3.0",
144+ "integrity": "sha256-GD4z7LCi9wzi7FCQqbWhvs8paoUK9NO+HwgXZ+KP8Rk=",
145+ "dependencies": [
146+ "bifunctors",
147+ "control",
148+ "foldable-traversable",
149+ "functions",
150+ "maybe",
151+ "nonempty",
152+ "partial",
153+ "prelude",
154+ "safe-coerce",
155+ "st",
156+ "tailrec",
157+ "tuples",
158+ "unfoldable",
159+ "unsafe-coerce"
160+ ]
161+ },
162+ "avar": {
163+ "type": "registry",
164+ "version": "5.0.1",
165+ "integrity": "sha256-8V4SxF4TIWZ9Ik1P4lPzIRUAZeFBe8w5WA2VcCEKsIk=",
166+ "dependencies": [
167+ "aff",
168+ "effect",
169+ "either",
170+ "exceptions",
171+ "functions",
172+ "maybe",
173+ "prelude"
174+ ]
175+ },
176+ "bifunctors": {
177+ "type": "registry",
178+ "version": "6.1.0",
179+ "integrity": "sha256-CvZRb8uI75eKIqiYntR+k05Wk6tKlMS5w8sCA2AFna0=",
180+ "dependencies": [
181+ "const",
182+ "either",
183+ "newtype",
184+ "prelude",
185+ "tuples"
186+ ]
187+ },
188+ "catenable-lists": {
189+ "type": "registry",
190+ "version": "7.0.0",
191+ "integrity": "sha256-7+i/MHBLIRh604iCNT4ENWRWE/wuHIotER4kpdX9Ve0=",
192+ "dependencies": [
193+ "control",
194+ "foldable-traversable",
195+ "lists",
196+ "maybe",
197+ "prelude",
198+ "tuples",
199+ "unfoldable"
200+ ]
201+ },
202+ "console": {
203+ "type": "registry",
204+ "version": "6.1.0",
205+ "integrity": "sha256-3T5PPz1mATZmV0C4GzkCkpLlCa5xG7u8ZNgnLrrrI1Q=",
206+ "dependencies": [
207+ "effect",
208+ "prelude"
209+ ]
210+ },
211+ "const": {
212+ "type": "registry",
213+ "version": "6.0.0",
214+ "integrity": "sha256-S+m0lQsxnji0z1XRUmkn+3RFuWJ5CXWuIr1u3i8Bfoc=",
215+ "dependencies": [
216+ "invariant",
217+ "newtype",
218+ "prelude"
219+ ]
220+ },
221+ "contravariant": {
222+ "type": "registry",
223+ "version": "6.0.0",
224+ "integrity": "sha256-nuckEWqL80NaPmR/l9AUyrfycNh18XcvlZV9PAamwqc=",
225+ "dependencies": [
226+ "const",
227+ "either",
228+ "newtype",
229+ "prelude",
230+ "tuples"
231+ ]
232+ },
233+ "control": {
234+ "type": "registry",
235+ "version": "6.0.0",
236+ "integrity": "sha256-0vxyYh5FL+ilxgLo954w1LqCUC0RSIivNNlb9CaNUQg=",
237+ "dependencies": [
238+ "newtype",
239+ "prelude"
240+ ]
241+ },
242+ "datetime": {
243+ "type": "registry",
244+ "version": "6.1.0",
245+ "integrity": "sha256-tbv6eDXy6QOD7YSknxYSbsDF32OC2JrNbSBpzMs7Doc=",
246+ "dependencies": [
247+ "bifunctors",
248+ "control",
249+ "either",
250+ "enums",
251+ "foldable-traversable",
252+ "functions",
253+ "gen",
254+ "integers",
255+ "lists",
256+ "maybe",
257+ "newtype",
258+ "numbers",
259+ "ordered-collections",
260+ "partial",
261+ "prelude",
262+ "tuples"
263+ ]
264+ },
265+ "distributive": {
266+ "type": "registry",
267+ "version": "6.0.0",
268+ "integrity": "sha256-netyJK4B32PDYHD0tF2AenKgo3urzbI770ycM0pArAo=",
269+ "dependencies": [
270+ "identity",
271+ "newtype",
272+ "prelude",
273+ "tuples",
274+ "type-equality"
275+ ]
276+ },
277+ "effect": {
278+ "type": "registry",
279+ "version": "4.0.0",
280+ "integrity": "sha256-DdqU3bncRTjgMVPPRlAuGHVeajSPEcRaRhNuio7if6s=",
281+ "dependencies": [
282+ "prelude"
283+ ]
284+ },
285+ "either": {
286+ "type": "registry",
287+ "version": "6.1.0",
288+ "integrity": "sha256-tBHx3PgtH4GZOApZCDkcM7szRTYJinrUrVGWazQCUUg=",
289+ "dependencies": [
290+ "control",
291+ "invariant",
292+ "maybe",
293+ "prelude"
294+ ]
295+ },
296+ "enums": {
297+ "type": "registry",
298+ "version": "6.0.1",
299+ "integrity": "sha256-sdZOmLX5+5pASGpvVyT0vSD5BBYQjZiayjV5XiPutso=",
300+ "dependencies": [
301+ "control",
302+ "either",
303+ "gen",
304+ "maybe",
305+ "newtype",
306+ "nonempty",
307+ "partial",
308+ "prelude",
309+ "tuples",
310+ "unfoldable"
311+ ]
312+ },
313+ "exceptions": {
314+ "type": "registry",
315+ "version": "6.1.0",
316+ "integrity": "sha256-nRrr+N0wk0TUFOWLR7e1n1oxcXo/X345bQUtoY9lW6A=",
317+ "dependencies": [
318+ "effect",
319+ "either",
320+ "maybe",
321+ "prelude"
322+ ]
323+ },
324+ "exists": {
325+ "type": "registry",
326+ "version": "6.0.0",
327+ "integrity": "sha256-vtLbrWNaI+pzx/2fw2AQnCovoLtcosClIhjO26QKkh8=",
328+ "dependencies": [
329+ "unsafe-coerce"
330+ ]
331+ },
332+ "foldable-traversable": {
333+ "type": "registry",
334+ "version": "6.0.0",
335+ "integrity": "sha256-NGGWyCio/xjce6fPP7y3wwg7CheqllID6aJudDxbWhA=",
336+ "dependencies": [
337+ "bifunctors",
338+ "const",
339+ "control",
340+ "either",
341+ "functors",
342+ "identity",
343+ "maybe",
344+ "newtype",
345+ "orders",
346+ "prelude",
347+ "tuples"
348+ ]
349+ },
350+ "foreign": {
351+ "type": "registry",
352+ "version": "7.0.0",
353+ "integrity": "sha256-jiDRMVQfovGsbbA/FzbzYf6iWk9i2b5gNrIjz+gFfsI=",
354+ "dependencies": [
355+ "either",
356+ "functions",
357+ "integers",
358+ "lists",
359+ "maybe",
360+ "prelude",
361+ "strings",
362+ "transformers",
363+ "unsafe-coerce"
364+ ]
365+ },
366+ "foreign-object": {
367+ "type": "registry",
368+ "version": "4.1.0",
369+ "integrity": "sha256-x/Q7r80z/vmHRKvPwhYmHP/DnJciPtsyGfRTMzayKIU=",
370+ "dependencies": [
371+ "arrays",
372+ "foldable-traversable",
373+ "functions",
374+ "gen",
375+ "lists",
376+ "maybe",
377+ "prelude",
378+ "st",
379+ "tailrec",
380+ "tuples",
381+ "typelevel-prelude",
382+ "unfoldable",
383+ "unsafe-coerce"
384+ ]
385+ },
386+ "fork": {
387+ "type": "registry",
388+ "version": "6.0.0",
389+ "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=",
390+ "dependencies": [
391+ "aff",
392+ "prelude",
393+ "transformers"
394+ ]
395+ },
396+ "free": {
397+ "type": "registry",
398+ "version": "7.1.0",
399+ "integrity": "sha256-tUBInfUpRQEw4u94GWcYW7EYdmSdDHSRn88a+C0eltU=",
400+ "dependencies": [
401+ "catenable-lists",
402+ "control",
403+ "distributive",
404+ "either",
405+ "exists",
406+ "foldable-traversable",
407+ "invariant",
408+ "lazy",
409+ "maybe",
410+ "prelude",
411+ "tailrec",
412+ "transformers",
413+ "tuples",
414+ "unsafe-coerce"
415+ ]
416+ },
417+ "functions": {
418+ "type": "registry",
419+ "version": "6.0.0",
420+ "integrity": "sha256-0jgtOr+SFO4UScKc40oA/9Vdcf2rLn3h3vZlY0KfqVo=",
421+ "dependencies": [
422+ "prelude"
423+ ]
424+ },
425+ "functors": {
426+ "type": "registry",
427+ "version": "5.0.0",
428+ "integrity": "sha256-W1o/4wcp6S3chodYL8uixlXK2mfr1L+SzM1R+whqUco=",
429+ "dependencies": [
430+ "bifunctors",
431+ "const",
432+ "contravariant",
433+ "control",
434+ "distributive",
435+ "either",
436+ "invariant",
437+ "maybe",
438+ "newtype",
439+ "prelude",
440+ "profunctor",
441+ "tuples",
442+ "unsafe-coerce"
443+ ]
444+ },
445+ "gen": {
446+ "type": "registry",
447+ "version": "4.0.0",
448+ "integrity": "sha256-kyOateeOtQa07vfTMYeSuCKdelgKE1R65fwKkZ10wsY=",
449+ "dependencies": [
450+ "either",
451+ "foldable-traversable",
452+ "identity",
453+ "maybe",
454+ "newtype",
455+ "nonempty",
456+ "prelude",
457+ "tailrec",
458+ "tuples",
459+ "unfoldable"
460+ ]
461+ },
462+ "heterogeneous": {
463+ "type": "registry",
464+ "version": "0.7.0",
465+ "integrity": "sha256-pQwcFvhWL8noktxaoD7FlLs2F2lYf3QHoyO0VOWO3I0=",
466+ "dependencies": [
467+ "either",
468+ "foldable-traversable",
469+ "functors",
470+ "prelude",
471+ "record",
472+ "tuples",
473+ "variant"
474+ ]
475+ },
476+ "identity": {
477+ "type": "registry",
478+ "version": "6.0.0",
479+ "integrity": "sha256-RY/iXPpxpqvKHYfJeFVNI1J06kohB9rbtWYuha8t0LE=",
480+ "dependencies": [
481+ "control",
482+ "invariant",
483+ "newtype",
484+ "prelude"
485+ ]
486+ },
487+ "integers": {
488+ "type": "registry",
489+ "version": "6.0.0",
490+ "integrity": "sha256-Y8yozC1uHRLcruobEyv5/bMKzE1BoKJ/olHv6o+bgm8=",
491+ "dependencies": [
492+ "maybe",
493+ "numbers",
494+ "prelude"
495+ ]
496+ },
497+ "invariant": {
498+ "type": "registry",
499+ "version": "6.0.0",
500+ "integrity": "sha256-HyoT1I5qIHRq8RSZji6zZDvqnpVjLw748utrKwZtqsw=",
501+ "dependencies": [
502+ "control",
503+ "prelude"
504+ ]
505+ },
506+ "js-bigints": {
507+ "type": "registry",
508+ "version": "2.2.1",
509+ "integrity": "sha256-r1rE6Olv5b3JrP32lY2uKJ+eBRTsrVnC983K8VWwqe4=",
510+ "dependencies": [
511+ "integers",
512+ "maybe",
513+ "prelude"
514+ ]
515+ },
516+ "js-date": {
517+ "type": "registry",
518+ "version": "8.0.0",
519+ "integrity": "sha256-DNQrp4xYc8o80jtxo0aOkUeXGi6h0Xidkk2ZQ8Y4l5Y=",
520+ "dependencies": [
521+ "datetime",
522+ "effect",
523+ "enums",
524+ "foreign",
525+ "functions",
526+ "integers",
527+ "maybe",
528+ "prelude"
529+ ]
530+ },
531+ "js-promise": {
532+ "type": "registry",
533+ "version": "1.0.0",
534+ "integrity": "sha256-u+6+ofIwT5XhT2fbMu4dShwkyWGfPRt9642Cnzw/Wvk=",
535+ "dependencies": [
536+ "effect",
537+ "exceptions",
538+ "foldable-traversable",
539+ "functions",
540+ "maybe",
541+ "newtype",
542+ "prelude"
543+ ]
544+ },
545+ "js-promise-aff": {
546+ "type": "registry",
547+ "version": "1.0.0",
548+ "integrity": "sha256-gEKAdNnTWjXS+tTp/AhYl/ezloKfuoQFcBKrdOx2USo=",
549+ "dependencies": [
550+ "aff",
551+ "control",
552+ "effect",
553+ "either",
554+ "exceptions",
555+ "foreign",
556+ "js-promise",
557+ "maybe",
558+ "prelude",
559+ "transformers"
560+ ]
561+ },
562+ "lazy": {
563+ "type": "registry",
564+ "version": "6.0.0",
565+ "integrity": "sha256-MHRC+1Pc+AjortBp4b/e1e6KpEpsDwaJBNBB7TOL+1I=",
566+ "dependencies": [
567+ "control",
568+ "foldable-traversable",
569+ "invariant",
570+ "prelude"
571+ ]
572+ },
573+ "lists": {
574+ "type": "registry",
575+ "version": "7.0.0",
576+ "integrity": "sha256-/mGd7wLoU+wsTcqii7SSUByxsvwvjnu2PjwAD47yYb4=",
577+ "dependencies": [
578+ "bifunctors",
579+ "control",
580+ "foldable-traversable",
581+ "lazy",
582+ "maybe",
583+ "newtype",
584+ "nonempty",
585+ "partial",
586+ "prelude",
587+ "tailrec",
588+ "tuples",
589+ "unfoldable"
590+ ]
591+ },
592+ "maybe": {
593+ "type": "registry",
594+ "version": "6.0.0",
595+ "integrity": "sha256-8X6XEgtZ5lqfkEuc1hWOofnL6mwhk8gSJTxwvbR/SbY=",
596+ "dependencies": [
597+ "control",
598+ "invariant",
599+ "newtype",
600+ "prelude"
601+ ]
602+ },
603+ "mmorph": {
604+ "type": "registry",
605+ "version": "7.0.0",
606+ "integrity": "sha256-Cfltaq+tfYjuSoSBN/b8cSQZbvZTqIRs6ebQewbIkGs=",
607+ "dependencies": [
608+ "bifunctors",
609+ "either",
610+ "free",
611+ "functors",
612+ "identity",
613+ "maybe",
614+ "newtype",
615+ "prelude",
616+ "transformers",
617+ "tuples"
618+ ]
619+ },
620+ "newtype": {
621+ "type": "registry",
622+ "version": "5.0.0",
623+ "integrity": "sha256-dt6w0cty4OS+Pjt9fsh3iU6ottqSU83mdAZkaEEvo6c=",
624+ "dependencies": [
625+ "prelude",
626+ "safe-coerce"
627+ ]
628+ },
629+ "nonempty": {
630+ "type": "registry",
631+ "version": "7.0.0",
632+ "integrity": "sha256-Ctkq8/+KiGqCE53Y/LvibA/coy7ev9HtRZU+GYS7yfQ=",
633+ "dependencies": [
634+ "control",
635+ "foldable-traversable",
636+ "maybe",
637+ "prelude",
638+ "tuples",
639+ "unfoldable"
640+ ]
641+ },
642+ "now": {
643+ "type": "registry",
644+ "version": "6.0.0",
645+ "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=",
646+ "dependencies": [
647+ "datetime",
648+ "effect",
649+ "prelude"
650+ ]
651+ },
652+ "nullable": {
653+ "type": "registry",
654+ "version": "6.0.0",
655+ "integrity": "sha256-CDAZk+L9Sc0GCFTkE8n+qdp4Ys8avvnkU+m2hVALt7M=",
656+ "dependencies": [
657+ "functions",
658+ "maybe",
659+ "prelude"
660+ ]
661+ },
662+ "numbers": {
663+ "type": "registry",
664+ "version": "9.0.1",
665+ "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
666+ "dependencies": [
667+ "functions",
668+ "maybe",
669+ "prelude"
670+ ]
671+ },
672+ "ordered-collections": {
673+ "type": "registry",
674+ "version": "3.2.0",
675+ "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
676+ "dependencies": [
677+ "arrays",
678+ "control",
679+ "foldable-traversable",
680+ "functions",
681+ "gen",
682+ "lists",
683+ "maybe",
684+ "newtype",
685+ "partial",
686+ "prelude",
687+ "safe-coerce",
688+ "tailrec",
689+ "tuples",
690+ "unfoldable"
691+ ]
692+ },
693+ "orders": {
694+ "type": "registry",
695+ "version": "6.0.0",
696+ "integrity": "sha256-nSMxZK7wxyDULkjZqvMyB71mVo6b0AyLLHosDVbv1XM=",
697+ "dependencies": [
698+ "newtype",
699+ "prelude"
700+ ]
701+ },
702+ "parallel": {
703+ "type": "registry",
704+ "version": "7.0.0",
705+ "integrity": "sha256-bAGpVhrFz2IyHkA4MR7+tE/19FZ7dzYBdYPgK5svkqw=",
706+ "dependencies": [
707+ "control",
708+ "effect",
709+ "either",
710+ "foldable-traversable",
711+ "functors",
712+ "maybe",
713+ "newtype",
714+ "prelude",
715+ "profunctor",
716+ "refs",
717+ "transformers"
718+ ]
719+ },
720+ "partial": {
721+ "type": "registry",
722+ "version": "4.0.0",
723+ "integrity": "sha256-mdrlJBAgFMh79hNRW39uv7c+BwnnaOrAMmdh7+VhEhs=",
724+ "dependencies": []
725+ },
726+ "pipes": {
727+ "type": "registry",
728+ "version": "8.0.0",
729+ "integrity": "sha256-dtUNpG8BGVr7pu6mQZyAnUAftUHgNS7f0QH3Kw5UTpQ=",
730+ "dependencies": [
731+ "aff",
732+ "control",
733+ "effect",
734+ "either",
735+ "foldable-traversable",
736+ "identity",
737+ "lists",
738+ "maybe",
739+ "mmorph",
740+ "newtype",
741+ "prelude",
742+ "tailrec",
743+ "transformers",
744+ "tuples"
745+ ]
746+ },
747+ "prelude": {
748+ "type": "registry",
749+ "version": "6.0.2",
750+ "integrity": "sha256-IqykrDRquGUD/LRSad7y75hZv3bUsGQ4knc8tdez8Qw=",
751+ "dependencies": []
752+ },
753+ "profunctor": {
754+ "type": "registry",
755+ "version": "6.0.1",
756+ "integrity": "sha256-Ow0mJC+PIIRq/a3mzCWY5vxuTgR/e+Ee68+mWeSRnWY=",
757+ "dependencies": [
758+ "control",
759+ "distributive",
760+ "either",
761+ "exists",
762+ "invariant",
763+ "newtype",
764+ "prelude",
765+ "tuples"
766+ ]
767+ },
768+ "record": {
769+ "type": "registry",
770+ "version": "4.0.0",
771+ "integrity": "sha256-WQ8fqp4X1wTA70J4qUcPW4Z7DCwx6e5KJnT/EuTdWSc=",
772+ "dependencies": [
773+ "functions",
774+ "prelude",
775+ "unsafe-coerce"
776+ ]
777+ },
778+ "refs": {
779+ "type": "registry",
780+ "version": "6.0.0",
781+ "integrity": "sha256-KKD5G9dR2SauuDGULUJsxjqDOEi1Jqm+2Sq5U2mY8YU=",
782+ "dependencies": [
783+ "effect",
784+ "prelude"
785+ ]
786+ },
787+ "safe-coerce": {
788+ "type": "registry",
789+ "version": "2.0.0",
790+ "integrity": "sha256-EJrxtKt5xC7TYjBZrSBO/J7Aw3DmtilYjt4olpAXWVc=",
791+ "dependencies": [
792+ "unsafe-coerce"
793+ ]
794+ },
795+ "spec": {
796+ "type": "registry",
797+ "version": "7.6.1",
798+ "integrity": "sha256-5GysDOS1gVbbGX75YXUdhww3h2S246Ck0sS6yt+ig+0=",
799+ "dependencies": [
800+ "aff",
801+ "ansi",
802+ "arrays",
803+ "avar",
804+ "bifunctors",
805+ "control",
806+ "datetime",
807+ "effect",
808+ "either",
809+ "exceptions",
810+ "foldable-traversable",
811+ "fork",
812+ "identity",
813+ "integers",
814+ "lists",
815+ "maybe",
816+ "newtype",
817+ "now",
818+ "ordered-collections",
819+ "parallel",
820+ "pipes",
821+ "prelude",
822+ "refs",
823+ "strings",
824+ "tailrec",
825+ "transformers",
826+ "tuples"
827+ ]
828+ },
829+ "st": {
830+ "type": "registry",
831+ "version": "6.2.0",
832+ "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
833+ "dependencies": [
834+ "effect",
835+ "partial",
836+ "prelude",
837+ "tailrec",
838+ "unsafe-coerce"
839+ ]
840+ },
841+ "strings": {
842+ "type": "registry",
843+ "version": "6.0.1",
844+ "integrity": "sha256-FNto2hoNW5Da6W6crIk77YXMagBvOGHZE1kO7eZ1vLM=",
845+ "dependencies": [
846+ "arrays",
847+ "control",
848+ "either",
849+ "enums",
850+ "foldable-traversable",
851+ "gen",
852+ "integers",
853+ "maybe",
854+ "newtype",
855+ "nonempty",
856+ "partial",
857+ "prelude",
858+ "tailrec",
859+ "tuples",
860+ "unfoldable",
861+ "unsafe-coerce"
862+ ]
863+ },
864+ "tailrec": {
865+ "type": "registry",
866+ "version": "6.1.0",
867+ "integrity": "sha256-xrorCfP0xV1wubs27idQCHQqdvSUuqmmL/am7Ji1wVU=",
868+ "dependencies": [
869+ "bifunctors",
870+ "effect",
871+ "either",
872+ "identity",
873+ "maybe",
874+ "partial",
875+ "prelude",
876+ "refs"
877+ ]
878+ },
879+ "transformers": {
880+ "type": "registry",
881+ "version": "6.1.0",
882+ "integrity": "sha256-QJmgNT/y7ljPHSsXaW4zxF/982BFiQ90KNL1g/NtEOQ=",
883+ "dependencies": [
884+ "control",
885+ "distributive",
886+ "effect",
887+ "either",
888+ "exceptions",
889+ "foldable-traversable",
890+ "identity",
891+ "lazy",
892+ "maybe",
893+ "newtype",
894+ "prelude",
895+ "st",
896+ "tailrec",
897+ "tuples",
898+ "unfoldable"
899+ ]
900+ },
901+ "tuples": {
902+ "type": "registry",
903+ "version": "7.0.0",
904+ "integrity": "sha256-BKy7EfK8S1KeTCwyWqyW1wwcpSAQuh3IsczSVgS6fzY=",
905+ "dependencies": [
906+ "control",
907+ "invariant",
908+ "prelude"
909+ ]
910+ },
911+ "type-equality": {
912+ "type": "registry",
913+ "version": "4.0.1",
914+ "integrity": "sha256-BBqYOSnAKmayRvR5mtZoCf4SlYs2ipatD/+5nQx73aw=",
915+ "dependencies": []
916+ },
917+ "typelevel-prelude": {
918+ "type": "registry",
919+ "version": "7.0.0",
920+ "integrity": "sha256-+pHi14/40mTj/+lmCWqL0T7iXIXD1+NSg/KItyyoB0A=",
921+ "dependencies": [
922+ "prelude",
923+ "type-equality"
924+ ]
925+ },
926+ "unfoldable": {
927+ "type": "registry",
928+ "version": "6.0.0",
929+ "integrity": "sha256-BK/6By1sKp/ztk+9CF3q43SV0QDTLVdsGqFerKHSmbg=",
930+ "dependencies": [
931+ "foldable-traversable",
932+ "maybe",
933+ "partial",
934+ "prelude",
935+ "tuples"
936+ ]
937+ },
938+ "unsafe-coerce": {
939+ "type": "registry",
940+ "version": "6.0.0",
941+ "integrity": "sha256-0L1QsaY20OILjfU5TV72d3U/tSjhmL9hJ32WMp857Lk=",
942+ "dependencies": []
943+ },
944+ "variant": {
945+ "type": "registry",
946+ "version": "8.0.0",
947+ "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
948+ "dependencies": [
949+ "control",
950+ "enums",
951+ "foldable-traversable",
952+ "lists",
953+ "maybe",
954+ "partial",
955+ "prelude",
956+ "type-equality",
957+ "unsafe-coerce"
958+ ]
959+ },
960+ "yoga-json": {
961+ "type": "registry",
962+ "version": "5.2.1",
963+ "integrity": "sha256-12I7qIf27oKV/D/YRcHtGpN8n7GQ0Bm3uzvI4ZjOQrs=",
964+ "dependencies": [
965+ "arrays",
966+ "bifunctors",
967+ "control",
968+ "datetime",
969+ "effect",
970+ "either",
971+ "exceptions",
972+ "foldable-traversable",
973+ "foreign",
974+ "foreign-object",
975+ "free",
976+ "identity",
977+ "integers",
978+ "js-bigints",
979+ "js-date",
980+ "lists",
981+ "maybe",
982+ "newtype",
983+ "nullable",
984+ "numbers",
985+ "ordered-collections",
986+ "partial",
987+ "prelude",
988+ "record",
989+ "safe-coerce",
990+ "strings",
991+ "transformers",
992+ "tuples",
993+ "typelevel-prelude",
994+ "unsafe-coerce",
995+ "variant",
996+ "yoga-tree"
997+ ]
998+ },
999+ "yoga-sql-types": {
1000+ "type": "registry",
1001+ "version": "0.2.0",
1002+ "integrity": "sha256-/fiSiEmSQ8UH2N7vp/E02Eo+Krbs6iGcre9haDV34Q8=",
1003+ "dependencies": [
1004+ "arrays",
1005+ "bifunctors",
1006+ "either",
1007+ "foldable-traversable",
1008+ "foreign",
1009+ "foreign-object",
1010+ "heterogeneous",
1011+ "identity",
1012+ "lists",
1013+ "maybe",
1014+ "newtype",
1015+ "nullable",
1016+ "ordered-collections",
1017+ "partial",
1018+ "prelude",
1019+ "record",
1020+ "safe-coerce",
1021+ "transformers",
1022+ "tuples",
1023+ "typelevel-prelude",
1024+ "unsafe-coerce",
1025+ "yoga-json"
1026+ ]
1027+ },
1028+ "yoga-test-docker": {
1029+ "type": "registry",
1030+ "version": "0.1.2",
1031+ "integrity": "sha256-J/jgHvvwb04ZOhicRGucmrQhaokrYTc9XJ2dutsktq8=",
1032+ "dependencies": [
1033+ "aff",
1034+ "datetime",
1035+ "exceptions",
1036+ "newtype",
1037+ "prelude"
1038+ ]
1039+ },
1040+ "yoga-tree": {
1041+ "type": "registry",
1042+ "version": "1.0.0",
1043+ "integrity": "sha256-s8rIVEQkRi1VrMc92T9ceXcY0X6r5A4QqT6f7GgmDQI=",
1044+ "dependencies": [
1045+ "arrays",
1046+ "control",
1047+ "foldable-traversable",
1048+ "free",
1049+ "maybe",
1050+ "prelude",
1051+ "tailrec"
1052+ ]
1053+ }
1054+ }
1055+}
spago.yaml+23−10
@@ -1,25 +1,38 @@
1−workspace:
2− package_set:
3− registry: 51.1.0
1+workspace: {}
42
53package:
64 name: yoga-scylladb
75 publish:
8− version: 0.1.0
6+ version: 0.1.1
97 license: MIT
108 location:
119 githubOwner: rowtype-yoga
1210 githubRepo: purescript-yoga-scylladb
1311 dependencies:
12+ - aff: ">=7.1.0 <8.0.0"
13+ - arrays: ">=7.3.0 <8.0.0"
14+ - datetime: ">=6.1.0 <7.0.0"
15+ - effect: ">=4.0.0 <5.0.0"
16+ - either: ">=6.1.0 <7.0.0"
17+ - exceptions: ">=6.1.0 <7.0.0"
18+ - foldable-traversable: ">=6.0.0 <7.0.0"
19+ - foreign: ">=7.0.0 <8.0.0"
20+ - foreign-object: ">=4.1.0 <5.0.0"
21+ - heterogeneous: ">=0.7.0 <0.8.0"
1422 - js-promise: ">=1.0.0 <2.0.0"
1523 - js-promise-aff: ">=1.0.0 <2.0.0"
16− - aff: ">=7.0.0 <8.0.0"
17− - effect: ">=4.0.0 <5.0.0"
18− - prelude: ">=6.0.0 <7.0.0"
19− - yoga-sql-types: "*"
20− - arrays: ">=7.0.0 <8.0.0"
21− - foreign-object: ">=4.0.0 <5.0.0"
24+ - maybe: ">=6.0.0 <7.0.0"
25+ - newtype: ">=5.0.0 <6.0.0"
2226 - nullable: ">=6.0.0 <7.0.0"
27+ - ordered-collections: ">=3.2.0 <4.0.0"
28+ - prelude: ">=6.0.2 <7.0.0"
29+ - record: ">=4.0.0 <5.0.0"
30+ - strings: ">=6.0.1 <7.0.0"
31+ - transformers: ">=6.1.0 <7.0.0"
32+ - typelevel-prelude: ">=7.0.0 <8.0.0"
33+ - unsafe-coerce: ">=6.0.0 <7.0.0"
34+ - yoga-json: ">=5.2.1 <6.0.0"
35+ - yoga-sql-types: ">=0.2.0 <0.3.0"
2336 test:
2437 main: Test.Scylladb.Main
2538 dependencies:
src/Yoga/ScyllaDB/Schema.pursnew file+1089
@@ -0,0 +1,1089 @@
1+module Yoga.ScyllaDB.Schema where
2+
3+import Prelude
4+
5+import Control.Monad.Except (runExcept)
6+import Data.Array (intercalate)
7+import Data.DateTime (DateTime)
8+import Data.Either (Either(..))
9+import Data.Foldable (foldl)
10+import Data.Map as Map
11+import Data.Maybe (Maybe(..))
12+import Data.String.Regex as Regex
13+import Data.String.Regex.Flags as RegexFlags
14+import Data.Symbol (class IsSymbol, reflectSymbol)
15+import Data.Traversable (traverse)
16+import Effect.Aff (Aff, throwError)
17+import Effect.Exception as Exception
18+import Foreign (Foreign, unsafeToForeign)
19+import Prim.Row as Row
20+import Prim.RowList as RL
21+import Prim.RowList (class RowToList)
22+import Prim.Symbol as Symbol
23+import Prim.TypeError (class Fail, Text)
24+import Record as Record
25+import Type.Proxy (Proxy(..))
26+import Type.RowList (class ListToRow)
27+import Unsafe.Coerce (unsafeCoerce)
28+import Yoga.JSON (class ReadForeign, readImpl)
29+import Yoga.ScyllaDB.ScyllaDB as Scylla
30+
31+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32+-- Core phantom types
33+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34+
35+data Table :: Symbol -> Row Type -> Type
36+data Table name columns = Table
37+
38+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39+-- Constraint wrappers (used with # operator from Type.Function)
40+-- Int # PartitionKey = PartitionKey Int
41+-- Int # ClusteringKey = ClusteringKey Int
42+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43+
44+data PartitionKey :: Type -> Type
45+data PartitionKey a
46+
47+data ClusteringKey :: Type -> Type
48+data ClusteringKey a
49+
50+data Static :: Type -> Type
51+data Static a
52+
53+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54+-- Nullability: inferred from Maybe
55+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56+
57+class IsNullable a where
58+ isNullable :: Proxy a -> Boolean
59+
60+instance IsNullable (Maybe a) where
61+ isNullable _ = true
62+else instance IsNullable a where
63+ isNullable _ = false
64+
65+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
66+-- ExtractType: recursively unwrap constraint wrappers to get base type
67+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68+
69+class ExtractType :: Type -> Type -> Constraint
70+class ExtractType wrapped typ | wrapped -> typ
71+
72+instance ExtractType a typ => ExtractType (PartitionKey a) typ
73+else instance ExtractType a typ => ExtractType (ClusteringKey a) typ
74+else instance ExtractType a typ => ExtractType (Static a) typ
75+else instance ExtractType a a
76+
77+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
78+-- PureScript type -> CQL type name
79+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80+
81+class CQLTypeName a where
82+ cqlTypeName :: Proxy a -> String
83+
84+instance CQLTypeName Int where
85+ cqlTypeName _ = "int"
86+
87+instance CQLTypeName String where
88+ cqlTypeName _ = "text"
89+
90+instance CQLTypeName Boolean where
91+ cqlTypeName _ = "boolean"
92+
93+instance CQLTypeName Number where
94+ cqlTypeName _ = "double"
95+
96+instance CQLTypeName DateTime where
97+ cqlTypeName _ = "timestamp"
98+
99+instance CQLTypeName Scylla.UUID where
100+ cqlTypeName _ = "uuid"
101+
102+instance CQLTypeName a => CQLTypeName (Maybe a) where
103+ cqlTypeName _ = cqlTypeName (Proxy :: Proxy a)
104+
105+instance CQLTypeName a => CQLTypeName (Array a) where
106+ cqlTypeName _ = "list<" <> cqlTypeName (Proxy :: Proxy a) <> ">"
107+
108+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
109+-- Constraint rendering (STATIC only — PK/CK handled in PRIMARY KEY clause)
110+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
111+
112+class RenderConstraint a where
113+ renderConstraint :: Proxy a -> String
114+
115+instance RenderConstraint a => RenderConstraint (PartitionKey a) where
116+ renderConstraint _ = renderConstraint (Proxy :: Proxy a)
117+
118+else instance RenderConstraint a => RenderConstraint (ClusteringKey a) where
119+ renderConstraint _ = renderConstraint (Proxy :: Proxy a)
120+
121+else instance RenderConstraint a => RenderConstraint (Static a) where
122+ renderConstraint _ = joinConstraints "STATIC" (renderConstraint (Proxy :: Proxy a))
123+
124+else instance RenderConstraint a where
125+ renderConstraint _ = ""
126+
127+joinConstraints :: String -> String -> String
128+joinConstraints a b = case a, b of
129+ "", s -> s
130+ s, "" -> s
131+ s1, s2 -> s1 <> " " <> s2
132+
133+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
134+-- Primary key detection
135+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
136+
137+class IsPartitionKey a where
138+ isPartitionKey :: Proxy a -> Boolean
139+
140+instance IsPartitionKey (PartitionKey a) where
141+ isPartitionKey _ = true
142+else instance IsPartitionKey a => IsPartitionKey (Static a) where
143+ isPartitionKey _ = isPartitionKey (Proxy :: Proxy a)
144+else instance IsPartitionKey a where
145+ isPartitionKey _ = false
146+
147+class IsClusteringKey a where
148+ isClusteringKey :: Proxy a -> Boolean
149+
150+instance IsClusteringKey (ClusteringKey a) where
151+ isClusteringKey _ = true
152+else instance IsClusteringKey a => IsClusteringKey (Static a) where
153+ isClusteringKey _ = isClusteringKey (Proxy :: Proxy a)
154+else instance IsClusteringKey a where
155+ isClusteringKey _ = false
156+
157+class IsKeyColumn a where
158+ isKeyColumn :: Proxy a -> Boolean
159+
160+instance IsKeyColumn (PartitionKey a) where
161+ isKeyColumn _ = true
162+else instance IsKeyColumn (ClusteringKey a) where
163+ isKeyColumn _ = true
164+else instance IsKeyColumn a where
165+ isKeyColumn _ = false
166+
167+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
168+-- DDL generation: column definitions
169+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
170+
171+class RenderColumnsRL :: RL.RowList Type -> Constraint
172+class RenderColumnsRL rl where
173+ renderColumnsRL :: Proxy rl -> Array String
174+
175+instance RenderColumnsRL RL.Nil where
176+ renderColumnsRL _ = []
177+
178+instance
179+ ( IsSymbol name
180+ , ExtractType entry typ
181+ , CQLTypeName typ
182+ , RenderConstraint entry
183+ , RenderColumnsRL tail
184+ ) =>
185+ RenderColumnsRL (RL.Cons name entry tail) where
186+ renderColumnsRL _ = do
187+ let colName = reflectSymbol (Proxy :: Proxy name)
188+ let colType = cqlTypeName (Proxy :: Proxy typ)
189+ let constraints = renderConstraint (Proxy :: Proxy entry)
190+ let constraintsSuffix = if constraints == "" then "" else " " <> constraints
191+ [ colName <> " " <> colType <> constraintsSuffix ] <> renderColumnsRL (Proxy :: Proxy tail)
192+
193+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
194+-- DDL generation: PRIMARY KEY clause
195+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
196+
197+class PartitionKeysRL :: RL.RowList Type -> Constraint
198+class PartitionKeysRL rl where
199+ partitionKeysRL :: Proxy rl -> Array String
200+
201+instance PartitionKeysRL RL.Nil where
202+ partitionKeysRL _ = []
203+
204+instance
205+ ( IsSymbol name
206+ , IsPartitionKey entry
207+ , PartitionKeysRL tail
208+ ) =>
209+ PartitionKeysRL (RL.Cons name entry tail) where
210+ partitionKeysRL _ =
211+ let rest = partitionKeysRL (Proxy :: Proxy tail)
212+ in
213+ if isPartitionKey (Proxy :: Proxy entry) then [ reflectSymbol (Proxy :: Proxy name) ] <> rest
214+ else rest
215+
216+class ClusteringKeysRL :: RL.RowList Type -> Constraint
217+class ClusteringKeysRL rl where
218+ clusteringKeysRL :: Proxy rl -> Array String
219+
220+instance ClusteringKeysRL RL.Nil where
221+ clusteringKeysRL _ = []
222+
223+instance
224+ ( IsSymbol name
225+ , IsClusteringKey entry
226+ , ClusteringKeysRL tail
227+ ) =>
228+ ClusteringKeysRL (RL.Cons name entry tail) where
229+ clusteringKeysRL _ =
230+ let rest = clusteringKeysRL (Proxy :: Proxy tail)
231+ in
232+ if isClusteringKey (Proxy :: Proxy entry) then [ reflectSymbol (Proxy :: Proxy name) ] <> rest
233+ else rest
234+
235+renderPrimaryKey :: Array String -> Array String -> String
236+renderPrimaryKey partitionKeys clusteringKeys = case partitionKeys, clusteringKeys of
237+ [], [] -> ""
238+ pks, [] -> "PRIMARY KEY ((" <> intercalate ", " pks <> "))"
239+ pks, cks -> "PRIMARY KEY ((" <> intercalate ", " pks <> "), " <> intercalate ", " cks <> ")"
240+
241+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
242+-- CREATE TABLE
243+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
244+
245+class CreateTableDDL a where
246+ createTableDDL :: String
247+
248+instance
249+ ( IsSymbol name
250+ , RowToList cols rl
251+ , RenderColumnsRL rl
252+ , PartitionKeysRL rl
253+ , ClusteringKeysRL rl
254+ ) =>
255+ CreateTableDDL (Table name cols) where
256+ createTableDDL = do
257+ let tableName = reflectSymbol (Proxy :: Proxy name)
258+ let columns = renderColumnsRL (Proxy :: Proxy rl)
259+ let pks = partitionKeysRL (Proxy :: Proxy rl)
260+ let cks = clusteringKeysRL (Proxy :: Proxy rl)
261+ let pkClause = renderPrimaryKey pks cks
262+ let allParts = columns <> (if pkClause == "" then [] else [pkClause])
263+ "CREATE TABLE IF NOT EXISTS " <> tableName <> " (" <> intercalate ", " allParts <> ")"
264+
265+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
266+-- INSERT SQL generation
267+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
268+
269+class InsertColumnsRL :: RL.RowList Type -> Constraint
270+class InsertColumnsRL rl where
271+ insertColumnsRL :: Proxy rl -> Array String
272+
273+instance InsertColumnsRL RL.Nil where
274+ insertColumnsRL _ = []
275+
276+instance
277+ ( IsSymbol name
278+ , InsertColumnsRL tail
279+ ) =>
280+ InsertColumnsRL (RL.Cons name entry tail) where
281+ insertColumnsRL _ =
282+ [ reflectSymbol (Proxy :: Proxy name) ] <> insertColumnsRL (Proxy :: Proxy tail)
283+
284+class InsertSQLFor a where
285+ insertSQLFor :: String
286+
287+instance
288+ ( IsSymbol name
289+ , RowToList cols rl
290+ , InsertColumnsRL rl
291+ ) =>
292+ InsertSQLFor (Table name cols) where
293+ insertSQLFor = do
294+ let tableName = reflectSymbol (Proxy :: Proxy name)
295+ let cols = insertColumnsRL (Proxy :: Proxy rl)
296+ let placeholders = map (const "?") cols
297+ "INSERT INTO " <> tableName
298+ <> " (" <> intercalate ", " cols <> ")"
299+ <> " VALUES (" <> intercalate ", " placeholders <> ")"
300+
301+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
302+-- SELECT SQL generation
303+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
304+
305+class SelectAllSQLFor a where
306+ selectAllSQLFor :: String
307+
308+instance
309+ ( IsSymbol name
310+ ) =>
311+ SelectAllSQLFor (Table name cols) where
312+ selectAllSQLFor = "SELECT * FROM " <> reflectSymbol (Proxy :: Proxy name)
313+
314+class WhereClauseRL :: RL.RowList Type -> Constraint
315+class WhereClauseRL rl where
316+ whereClauseRL :: Proxy rl -> Array String
317+
318+instance WhereClauseRL RL.Nil where
319+ whereClauseRL _ = []
320+
321+instance (IsSymbol name, WhereClauseRL tail) => WhereClauseRL (RL.Cons name typ tail) where
322+ whereClauseRL _ =
323+ [ reflectSymbol (Proxy :: Proxy name) <> " = ?" ]
324+ <> whereClauseRL (Proxy :: Proxy tail)
325+
326+class SelectWhereSQLFor a whereRow where
327+ selectWhereSQLFor :: String
328+
329+instance
330+ ( IsSymbol name
331+ , RowToList whereRow whereRL
332+ , WhereClauseRL whereRL
333+ ) =>
334+ SelectWhereSQLFor (Table name cols) whereRow where
335+ selectWhereSQLFor = do
336+ let tableName = reflectSymbol (Proxy :: Proxy name)
337+ let conditions = whereClauseRL (Proxy :: Proxy whereRL)
338+ "SELECT * FROM " <> tableName <> " WHERE " <> intercalate " AND " conditions
339+
340+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
341+-- UPDATE SQL generation
342+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
343+
344+class ColumnCountRL :: RL.RowList Type -> Constraint
345+class ColumnCountRL rl where
346+ columnCountRL :: Proxy rl -> Int
347+
348+instance ColumnCountRL RL.Nil where
349+ columnCountRL _ = 0
350+
351+instance ColumnCountRL tail => ColumnCountRL (RL.Cons name typ tail) where
352+ columnCountRL _ = 1 + columnCountRL (Proxy :: Proxy tail)
353+
354+class SetClauseRL :: RL.RowList Type -> Constraint
355+class SetClauseRL rl where
356+ setClauseRL :: Proxy rl -> Array String
357+
358+instance SetClauseRL RL.Nil where
359+ setClauseRL _ = []
360+
361+instance (IsSymbol name, SetClauseRL tail) => SetClauseRL (RL.Cons name typ tail) where
362+ setClauseRL _ =
363+ [ reflectSymbol (Proxy :: Proxy name) <> " = ?" ]
364+ <> setClauseRL (Proxy :: Proxy tail)
365+
366+class UpdateSQLFor table setRow whereRow where
367+ updateSQLFor :: String
368+
369+instance
370+ ( IsSymbol name
371+ , RowToList setRow setRL
372+ , RowToList whereRow whereRL
373+ , SetClauseRL setRL
374+ , WhereClauseRL whereRL
375+ ) =>
376+ UpdateSQLFor (Table name cols) setRow whereRow where
377+ updateSQLFor = do
378+ let tableName = reflectSymbol (Proxy :: Proxy name)
379+ let setClauses = setClauseRL (Proxy :: Proxy setRL)
380+ let whereClauses = whereClauseRL (Proxy :: Proxy whereRL)
381+ "UPDATE " <> tableName
382+ <> " SET " <> intercalate ", " setClauses
383+ <> " WHERE " <> intercalate " AND " whereClauses
384+
385+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
386+-- DELETE SQL generation
387+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
388+
389+class DeleteSQLFor table whereRow where
390+ deleteSQLFor :: String
391+
392+instance
393+ ( IsSymbol name
394+ , RowToList whereRow whereRL
395+ , WhereClauseRL whereRL
396+ ) =>
397+ DeleteSQLFor (Table name cols) whereRow where
398+ deleteSQLFor = do
399+ let tableName = reflectSymbol (Proxy :: Proxy name)
400+ let conditions = whereClauseRL (Proxy :: Proxy whereRL)
401+ "DELETE FROM " <> tableName <> " WHERE " <> intercalate " AND " conditions
402+
403+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
404+-- StripColumns: (name :: String # PartitionKey, ...) -> (name :: String, ...)
405+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
406+
407+class StripColumnsRL :: RL.RowList Type -> RL.RowList Type -> Constraint
408+class StripColumnsRL rl out | rl -> out
409+
410+instance StripColumnsRL RL.Nil RL.Nil
411+instance (ExtractType entry typ, StripColumnsRL tail out') => StripColumnsRL (RL.Cons name entry tail) (RL.Cons name typ out')
412+
413+class StripColumns :: Row Type -> Row Type -> Constraint
414+class StripColumns cols result | cols -> result
415+
416+instance (RowToList cols rl, StripColumnsRL rl outRL, ListToRow outRL result) => StripColumns cols result
417+
418+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
419+-- InsertNonKeyColumnsRL: columns that are NOT part of the primary key
420+-- (useful for building insert params excluding auto-generated key columns)
421+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
422+
423+class NonKeyColumnsRL :: RL.RowList Type -> Constraint
424+class NonKeyColumnsRL rl where
425+ nonKeyColumnsRL :: Proxy rl -> Array String
426+
427+instance NonKeyColumnsRL RL.Nil where
428+ nonKeyColumnsRL _ = []
429+
430+instance
431+ ( IsSymbol name
432+ , IsKeyColumn entry
433+ , NonKeyColumnsRL tail
434+ ) =>
435+ NonKeyColumnsRL (RL.Cons name entry tail) where
436+ nonKeyColumnsRL _ =
437+ let rest = nonKeyColumnsRL (Proxy :: Proxy tail)
438+ in
439+ if isKeyColumn (Proxy :: Proxy entry) then rest
440+ else [ reflectSymbol (Proxy :: Proxy name) ] <> rest
441+
442+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
443+-- Q type: Builder phantom type for CQL queries
444+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
445+
446+newtype Q :: Row (Row Type) -> Row Type -> Row Type -> Row Type -> Type
447+newtype Q tables result params stage = Q { sql :: String, values :: Array Foreign }
448+
449+toSQL :: forall tables result params stage. Q tables result params stage -> String
450+toSQL (Q q) = q.sql
451+
452+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
453+-- SingleTable: extract name and cols from a one-entry tables row
454+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
455+
456+class SingleTable :: Row (Row Type) -> Symbol -> Row Type -> Constraint
457+class SingleTable tables name cols | tables -> name cols
458+
459+instance
460+ ( RowToList tables (RL.Cons name cols RL.Nil)
461+ , IsSymbol name
462+ ) =>
463+ SingleTable tables name cols
464+
465+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
466+-- HasClause / HasAnyDML: stage constraints
467+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
468+
469+class HasClause :: Symbol -> Row Type -> Constraint
470+class HasClause label row
471+
472+instance Row.Cons label Unit rest row => HasClause label row
473+
474+class HasAnyDML :: Row Type -> Constraint
475+class HasAnyDML stage
476+
477+instance (RL.RowToList stage rl, HasAnyDMLRL rl) => HasAnyDML stage
478+
479+class HasAnyDMLRL :: RL.RowList Type -> Constraint
480+class HasAnyDMLRL rl
481+
482+instance HasAnyDMLRL (RL.Cons "select" Unit rest)
483+else instance HasAnyDMLRL (RL.Cons "set" Unit rest)
484+else instance HasAnyDMLRL (RL.Cons "delete" Unit rest)
485+else instance HasAnyDMLRL rest => HasAnyDMLRL (RL.Cons label typ rest)
486+else instance Fail (Text "WHERE requires a preceding SELECT, UPDATE (set), or DELETE") => HasAnyDMLRL RL.Nil
487+
488+-- Values use the public Scylla.ToCQLValue extension point. Maybe must unwrap
489+-- through that instance so wrapped domain values keep their database encoding.
490+
491+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
492+-- RecordValuesRL: walk a RowList and extract values from a record
493+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
494+
495+class RecordValuesRL :: RL.RowList Type -> Row Type -> Constraint
496+class RecordValuesRL rl row where
497+ recordValuesRL :: Proxy rl -> { | row } -> Array Foreign
498+
499+instance RecordValuesRL RL.Nil row where
500+ recordValuesRL _ _ = []
501+
502+instance
503+ ( IsSymbol name
504+ , Row.Cons name typ rest row
505+ , Scylla.ToCQLValue typ
506+ , RecordValuesRL tail row
507+ ) =>
508+ RecordValuesRL (RL.Cons name typ tail) row where
509+ recordValuesRL _ rec =
510+ [ Scylla.toCQLValue (Record.get (Proxy :: Proxy name) rec) ]
511+ <> recordValuesRL (Proxy :: Proxy tail) rec
512+
513+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
514+-- ColumnNamesRL: extract column names from a RowList
515+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
516+
517+class ColumnNamesRL :: RL.RowList Type -> Constraint
518+class ColumnNamesRL rl where
519+ columnNamesRL :: Proxy rl -> Array String
520+
521+instance ColumnNamesRL RL.Nil where
522+ columnNamesRL _ = []
523+
524+instance (IsSymbol name, ColumnNamesRL tail) => ColumnNamesRL (RL.Cons name typ tail) where
525+ columnNamesRL _ =
526+ [ reflectSymbol (Proxy :: Proxy name) ] <> columnNamesRL (Proxy :: Proxy tail)
527+
528+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
529+-- ValidateSetColumnsRL: ensure SET columns exist in the table
530+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
531+
532+class ValidateSetColumnsRL :: RL.RowList Type -> Row Type -> Constraint
533+class ValidateSetColumnsRL rl cols
534+
535+instance ValidateSetColumnsRL RL.Nil cols
536+
537+instance
538+ ( IsSymbol name
539+ , Row.Cons name colType rest cols
540+ , ValidateSetColumnsRL tail cols
541+ ) =>
542+ ValidateSetColumnsRL (RL.Cons name typ tail) cols
543+
544+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
545+-- Builder: from
546+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
547+
548+from
549+ :: forall name cols tables
550+ . IsSymbol name
551+ => Row.Cons name cols () tables
552+ => Proxy (Table name cols)
553+ -> Q tables () () ()
554+from _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
555+
556+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
557+-- Builder: selectAll
558+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
559+
560+selectAll
561+ :: forall tables name cols result r p stage stage'
562+ . SingleTable tables name cols
563+ => StripColumns cols result
564+ => Row.Lacks "select" stage
565+ => Row.Lacks "insert" stage
566+ => Row.Lacks "set" stage
567+ => Row.Lacks "delete" stage
568+ => Row.Lacks "where" stage
569+ => Row.Lacks "orderBy" stage
570+ => Row.Lacks "limit" stage
571+ => Row.Cons "select" Unit stage stage'
572+ => Q tables r p stage
573+ -> Q tables result p stage'
574+selectAll (Q q) = Q (q { sql = "SELECT * FROM " <> q.sql })
575+
576+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
577+-- Builder: delete
578+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
579+
580+delete
581+ :: forall tables name cols r p stage stage'
582+ . SingleTable tables name cols
583+ => IsSymbol name
584+ => Row.Lacks "select" stage
585+ => Row.Lacks "insert" stage
586+ => Row.Lacks "set" stage
587+ => Row.Lacks "delete" stage
588+ => Row.Cons "delete" Unit stage stage'
589+ => Q tables r p stage
590+ -> Q tables () () stage'
591+delete (Q _) = Q { sql: "DELETE FROM " <> reflectSymbol (Proxy :: Proxy name), values: [] }
592+
593+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
594+-- Builder SetClauseRL (for builder pattern — uses bare ?)
595+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
596+
597+class BuilderSetClauseRL :: RL.RowList Type -> Constraint
598+class BuilderSetClauseRL rl where
599+ builderSetClauseRL :: Proxy rl -> Array String
600+
601+instance BuilderSetClauseRL RL.Nil where
602+ builderSetClauseRL _ = []
603+
604+instance (IsSymbol name, BuilderSetClauseRL tail) => BuilderSetClauseRL (RL.Cons name typ tail) where
605+ builderSetClauseRL _ =
606+ [ reflectSymbol (Proxy :: Proxy name) <> " = ?" ]
607+ <> builderSetClauseRL (Proxy :: Proxy tail)
608+
609+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
610+-- Builder: insert
611+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
612+
613+insert
614+ :: forall tables name cols userRow userRowRL stage stage'
615+ . SingleTable tables name cols
616+ => RowToList userRow userRowRL
617+ => ColumnNamesRL userRowRL
618+ => RecordValuesRL userRowRL userRow
619+ => IsSymbol name
620+ => Row.Lacks "select" stage
621+ => Row.Lacks "insert" stage
622+ => Row.Lacks "set" stage
623+ => Row.Lacks "delete" stage
624+ => Row.Cons "insert" Unit stage stage'
625+ => { | userRow }
626+ -> Q tables () () stage
627+ -> Q tables () () stage'
628+insert rec (Q _) = Q { sql, values }
629+ where
630+ tableName = reflectSymbol (Proxy :: Proxy name)
631+ colNames = columnNamesRL (Proxy :: Proxy userRowRL)
632+ placeholders = map (const "?") colNames
633+ sql = "INSERT INTO " <> tableName
634+ <> " (" <> intercalate ", " colNames <> ")"
635+ <> " VALUES (" <> intercalate ", " placeholders <> ")"
636+ values = recordValuesRL (Proxy :: Proxy userRowRL) rec
637+
638+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
639+-- Builder: set (UPDATE)
640+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
641+
642+set
643+ :: forall tables name cols setRow setRL stage stage'
644+ . SingleTable tables name cols
645+ => RowToList setRow setRL
646+ => IsSymbol name
647+ => ValidateSetColumnsRL setRL cols
648+ => BuilderSetClauseRL setRL
649+ => RecordValuesRL setRL setRow
650+ => Row.Lacks "select" stage
651+ => Row.Lacks "insert" stage
652+ => Row.Lacks "set" stage
653+ => Row.Lacks "delete" stage
654+ => Row.Cons "set" Unit stage stage'
655+ => { | setRow }
656+ -> Q tables () () stage
657+ -> Q tables () () stage'
658+set rec (Q q) = Q { sql, values }
659+ where
660+ setClauses = builderSetClauseRL (Proxy :: Proxy setRL)
661+ sql = "UPDATE " <> q.sql <> " SET " <> intercalate ", " setClauses
662+ values = recordValuesRL (Proxy :: Proxy setRL) rec
663+
664+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
665+-- ParseWhere: extract $paramName tokens from WHERE clause
666+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
667+
668+class ParseWhere :: Symbol -> Row (Row Type) -> Row Type -> Constraint
669+class ParseWhere sym tables params | sym tables -> params
670+
671+instance
672+ ( Symbol.Cons h t sym
673+ , ParseWhereGo h t tables RL.Nil outRL
674+ , ListToRow outRL params
675+ ) =>
676+ ParseWhere sym tables params
677+
678+class ParseWhereGo :: Symbol -> Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
679+class ParseWhereGo head tail tables paramsIn paramsOut | head tail tables paramsIn -> paramsOut
680+
681+-- When we see $, collect the param name and resolve its type
682+instance
683+ ( CollectParamName rest "" paramName restAfter
684+ , SingleTable tables tableName cols
685+ , RowToList cols colsRL
686+ , ResolveParamType paramName colsRL typ
687+ , EndOrContinue restAfter tables (RL.Cons paramName typ paramsIn) paramsOut
688+ ) =>
689+ ParseWhereGo "$" rest tables paramsIn paramsOut
690+
691+-- End of string: done
692+else instance ParseWhereGo h "" tables paramsIn paramsIn
693+
694+-- Default: skip regular characters and continue
695+else instance
696+ ( Symbol.Cons h2 t2 rest
697+ , ParseWhereGo h2 t2 tables paramsIn paramsOut
698+ ) =>
699+ ParseWhereGo h rest tables paramsIn paramsOut
700+
701+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
702+-- CollectParamName: accumulate identifier chars after $
703+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
704+
705+class CollectParamName :: Symbol -> Symbol -> Symbol -> Symbol -> Constraint
706+class CollectParamName input acc name rest | input acc -> name rest
707+
708+instance CollectParamName "" acc acc ""
709+else instance
710+ ( Symbol.Cons h t input
711+ , CollectParamNameChar h t acc name rest
712+ ) =>
713+ CollectParamName input acc name rest
714+
715+class CollectParamNameChar :: Symbol -> Symbol -> Symbol -> Symbol -> Symbol -> Constraint
716+class CollectParamNameChar char tail acc name rest | char tail acc -> name rest
717+
718+instance
719+ ( Symbol.Cons " " tail rest
720+ ) =>
721+ CollectParamNameChar " " tail acc acc rest
722+
723+else instance
724+ ( Symbol.Cons ")" tail rest
725+ ) =>
726+ CollectParamNameChar ")" tail acc acc rest
727+
728+else instance
729+ ( Symbol.Cons "," tail rest
730+ ) =>
731+ CollectParamNameChar "," tail acc acc rest
732+
733+else instance
734+ ( Symbol.Append acc char acc'
735+ , CollectParamName tail acc' name rest
736+ ) =>
737+ CollectParamNameChar char tail acc name rest
738+
739+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
740+-- EndOrContinue: handle end-of-string or continue parsing
741+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
742+
743+class EndOrContinue :: Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
744+class EndOrContinue rest tables paramsIn paramsOut | rest tables paramsIn -> paramsOut
745+
746+instance EndOrContinue "" tables paramsIn paramsIn
747+else instance
748+ ( Symbol.Cons h t rest
749+ , ParseWhereGo h t tables paramsIn paramsOut
750+ ) =>
751+ EndOrContinue rest tables paramsIn paramsOut
752+
753+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
754+-- ResolveParamType: look up a param name in table columns
755+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
756+
757+class ResolveParamType :: Symbol -> RL.RowList Type -> Type -> Constraint
758+class ResolveParamType name rl typ | name rl -> typ
759+
760+instance Fail (Text "Column not found in table") => ResolveParamType name RL.Nil typ
761+else instance (ExtractType entry typ) => ResolveParamType name (RL.Cons name entry tail) typ
762+else instance ResolveParamType name tail typ => ResolveParamType name (RL.Cons other entry tail) typ
763+
764+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
765+-- Builder: where_
766+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
767+
768+where_
769+ :: forall @whr tables result params p stage stage'
770+ . IsSymbol whr
771+ => ParseWhere whr tables params
772+ => HasAnyDML stage
773+ => Row.Lacks "where" stage
774+ => Row.Lacks "insert" stage
775+ => Row.Lacks "orderBy" stage
776+ => Row.Lacks "limit" stage
777+ => Row.Cons "where" Unit stage stage'
778+ => Q tables result p stage
779+ -> Q tables result params stage'
780+where_ (Q q) = Q (q { sql = q.sql <> " WHERE " <> reflectSymbol (Proxy :: Proxy whr) })
781+
782+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
783+-- Builder: orderBy
784+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
785+
786+orderBy
787+ :: forall @cols tables result params stage stage'
788+ . IsSymbol cols
789+ => HasClause "select" stage
790+ => Row.Lacks "orderBy" stage
791+ => Row.Lacks "limit" stage
792+ => Row.Cons "orderBy" Unit stage stage'
793+ => Q tables result params stage
794+ -> Q tables result params stage'
795+orderBy (Q q) = Q (q { sql = q.sql <> " ORDER BY " <> reflectSymbol (Proxy :: Proxy cols) })
796+
797+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
798+-- ParseLimitOffset: accept numeric literal or $paramName
799+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
800+
801+class ParseLimitOffset :: Symbol -> Row Type -> Row Type -> Constraint
802+class ParseLimitOffset sym paramsIn paramsOut | sym paramsIn -> paramsOut
803+
804+instance
805+ ( Symbol.Cons h t sym
806+ , ParseLimitOffsetChar h t paramsIn paramsOut
807+ ) =>
808+ ParseLimitOffset sym paramsIn paramsOut
809+
810+class ParseLimitOffsetChar :: Symbol -> Symbol -> Row Type -> Row Type -> Constraint
811+class ParseLimitOffsetChar head tail paramsIn paramsOut | head tail paramsIn -> paramsOut
812+
813+instance
814+ ( Symbol.Append "" tail paramName
815+ , Row.Cons paramName Int paramsIn paramsOut
816+ ) =>
817+ ParseLimitOffsetChar "$" tail paramsIn paramsOut
818+
819+else instance ParseLimitOffsetChar h tail paramsIn paramsIn
820+
821+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
822+-- Builder: limit
823+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
824+
825+limit
826+ :: forall @sym tables result params params' stage stage'
827+ . ParseLimitOffset sym params params'
828+ => IsSymbol sym
829+ => HasClause "select" stage
830+ => Row.Lacks "limit" stage
831+ => Row.Cons "limit" Unit stage stage'
832+ => Q tables result params stage
833+ -> Q tables result params' stage'
834+limit (Q q) = Q (q { sql = q.sql <> " LIMIT " <> reflectSymbol (Proxy :: Proxy sym) })
835+
836+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
837+-- CQL-specific: ifNotExists (after INSERT)
838+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
839+
840+ifNotExists
841+ :: forall tables result params stage stage'
842+ . HasClause "insert" stage
843+ => Row.Lacks "ifNotExists" stage
844+ => Row.Cons "ifNotExists" Unit stage stage'
845+ => Q tables result params stage
846+ -> Q tables result params stage'
847+ifNotExists (Q q) = Q (q { sql = q.sql <> " IF NOT EXISTS" })
848+
849+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
850+-- CQL-specific: ifExists (after UPDATE/DELETE WHERE)
851+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
852+
853+ifExists
854+ :: forall tables result params stage stage'
855+ . HasClause "where" stage
856+ => Row.Lacks "ifExists" stage
857+ => Row.Cons "ifExists" Unit stage stage'
858+ => Q tables result params stage
859+ -> Q tables result params stage'
860+ifExists (Q q) = Q (q { sql = q.sql <> " IF EXISTS" })
861+
862+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
863+-- CQL-specific: usingTTL (after INSERT)
864+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
865+
866+usingTTL
867+ :: forall @ttl tables result params stage stage'
868+ . IsSymbol ttl
869+ => HasClause "insert" stage
870+ => Row.Lacks "usingTTL" stage
871+ => Row.Cons "usingTTL" Unit stage stage'
872+ => Q tables result params stage
873+ -> Q tables result params stage'
874+usingTTL (Q q) = Q (q { sql = q.sql <> " USING TTL " <> reflectSymbol (Proxy :: Proxy ttl) })
875+
876+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
877+-- CQL-specific: withTTL (before set, produces UPDATE t USING TTL n)
878+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
879+
880+withTTL
881+ :: forall @ttl tables result params stage stage'
882+ . IsSymbol ttl
883+ => Row.Lacks "set" stage
884+ => Row.Lacks "select" stage
885+ => Row.Lacks "insert" stage
886+ => Row.Lacks "delete" stage
887+ => Row.Lacks "withTTL" stage
888+ => Row.Cons "withTTL" Unit stage stage'
889+ => Q tables result params stage
890+ -> Q tables result params stage'
891+withTTL (Q q) = Q (q { sql = q.sql <> " USING TTL " <> reflectSymbol (Proxy :: Proxy ttl) })
892+
893+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
894+-- CQL-specific: allowFiltering (after SELECT WHERE)
895+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
896+
897+allowFiltering
898+ :: forall tables result params stage stage'
899+ . HasClause "select" stage
900+ => Row.Lacks "allowFiltering" stage
901+ => Row.Cons "allowFiltering" Unit stage stage'
902+ => Q tables result params stage
903+ -> Q tables result params stage'
904+allowFiltering (Q q) = Q (q { sql = q.sql <> " ALLOW FILTERING" })
905+
906+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
907+-- ParseSelect: parse "col1, col2" into a result row
908+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
909+
910+class ParseSelect :: Symbol -> Row (Row Type) -> Row Type -> Constraint
911+class ParseSelect sym tables result | sym tables -> result
912+
913+instance ParseSelect "" tables ()
914+else instance
915+ ( Symbol.Cons h t sym
916+ , ParseSelectGo h t "" tables RL.Nil outRL
917+ , ListToRow outRL result
918+ ) =>
919+ ParseSelect sym tables result
920+
921+class ParseSelectGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
922+class ParseSelectGo head tail acc tables resultIn resultOut | head tail acc tables resultIn -> resultOut
923+
924+-- Comma with accumulated column name: flush column and continue
925+instance
926+ ( SingleTable tables tableName cols
927+ , RowToList cols colsRL
928+ , ResolveParamType acc colsRL typ
929+ , Symbol.Cons h2 t2 rest
930+ , SkipSpaces h2 t2 h3 t3
931+ , ParseSelectGo h3 t3 "" tables (RL.Cons acc typ resultIn) resultOut
932+ ) =>
933+ ParseSelectGo "," rest acc tables resultIn resultOut
934+
935+-- Space: skip and continue accumulating
936+else instance
937+ ( Symbol.Cons h2 t2 rest
938+ , ParseSelectGo h2 t2 acc tables resultIn resultOut
939+ ) =>
940+ ParseSelectGo " " rest acc tables resultIn resultOut
941+
942+-- End of string: flush final column
943+else instance
944+ ( Symbol.Append acc h acc'
945+ , SingleTable tables tableName cols
946+ , RowToList cols colsRL
947+ , ResolveParamType acc' colsRL typ
948+ ) =>
949+ ParseSelectGo h "" acc tables resultIn (RL.Cons acc' typ resultIn)
950+
951+-- Regular character with more input: accumulate
952+else instance
953+ ( Symbol.Append acc h acc'
954+ , Symbol.Cons h2 t2 rest
955+ , ParseSelectGo h2 t2 acc' tables resultIn resultOut
956+ ) =>
957+ ParseSelectGo h rest acc tables resultIn resultOut
958+
959+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
960+-- SkipSpaces: skip leading spaces
961+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
962+
963+class SkipSpaces :: Symbol -> Symbol -> Symbol -> Symbol -> Constraint
964+class SkipSpaces head tail outHead outTail | head tail -> outHead outTail
965+
966+instance (Symbol.Cons h2 t2 rest, SkipSpaces h2 t2 outHead outTail) => SkipSpaces " " rest outHead outTail
967+else instance SkipSpaces h t h t
968+
969+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
970+-- Builder: select @"col1, col2"
971+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
972+
973+select
974+ :: forall @sel tables result r p stage stage'
975+ . IsSymbol sel
976+ => ParseSelect sel tables result
977+ => Row.Lacks "select" stage
978+ => Row.Lacks "insert" stage
979+ => Row.Lacks "set" stage
980+ => Row.Lacks "delete" stage
981+ => Row.Lacks "where" stage
982+ => Row.Lacks "orderBy" stage
983+ => Row.Lacks "limit" stage
984+ => Row.Cons "select" Unit stage stage'
985+ => Q tables r p stage
986+ -> Q tables result p stage'
987+select (Q q) = Q (q { sql = "SELECT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> q.sql })
988+
989+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
990+-- ParamsToArray: convert params record to Array { name, value }
991+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
992+
993+class ParamsToArray :: RL.RowList Type -> Row Type -> Constraint
994+class ParamsToArray rl row where
995+ paramsToArray :: Proxy rl -> { | row } -> Array { name :: String, value :: Foreign }
996+
997+instance ParamsToArray RL.Nil row where
998+ paramsToArray _ _ = []
999+
1000+instance
1001+ ( IsSymbol name
1002+ , Row.Cons name typ rest row
1003+ , Row.Lacks name rest
1004+ , ParamsToArray tail row
1005+ ) =>
1006+ ParamsToArray (RL.Cons name typ tail) row where
1007+ paramsToArray _ rec =
1008+ [ { name: reflectSymbol (Proxy :: Proxy name)
1009+ , value: unsafeToForeign (Record.get (Proxy :: Proxy name) rec)
1010+ }
1011+ ]
1012+ <> paramsToArray (Proxy :: Proxy tail) rec
1013+
1014+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1015+-- replaceNamedParams: replace $name with ? and build values array
1016+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1017+
1018+namedParamRegex :: Regex.Regex
1019+namedParamRegex = case Regex.regex "\\$[a-zA-Z_][a-zA-Z0-9_]*" RegexFlags.global of
1020+ Right r -> r
1021+ Left _ -> unsafeCoerce unit
1022+
1023+replaceNamedParams :: Array { name :: String, value :: Foreign } -> String -> { sql :: String, values :: Array Foreign }
1024+replaceNamedParams entries sql = do
1025+ let replacements = entries # foldl (\m e -> Map.insert ("$" <> e.name) "?" m) Map.empty
1026+ let
1027+ sql' = Regex.replace' namedParamRegex
1028+ ( \match_ _ -> case Map.lookup match_ replacements of
1029+ Nothing -> match_
1030+ Just v -> v
1031+ )
1032+ sql
1033+ { sql: sql', values: map (\e -> e.value) entries }
1034+
1035+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1036+-- Query execution
1037+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1038+
1039+decodeRows :: forall a. ReadForeign a => Array Foreign -> Either String (Array a)
1040+decodeRows rows = case traverse (\r -> readImpl r # runExcept) rows of
1041+ Left errs -> Left (show errs)
1042+ Right a -> Right a
1043+
1044+runQuery
1045+ :: forall tables result params paramsRL stage
1046+ . RowToList params paramsRL
1047+ => ParamsToArray paramsRL params
1048+ => ReadForeign { | result }
1049+ => Scylla.Client
1050+ -> { | params }
1051+ -> Q tables result params stage
1052+ -> Aff (Array { | result })
1053+runQuery client params (Q q) = do
1054+ let entries = paramsToArray (Proxy :: Proxy paramsRL) params
1055+ let { sql, values } = replaceNamedParams entries q.sql
1056+ let allValues = q.values <> values
1057+ result <- Scylla.execute (Scylla.CQL sql) allValues client
1058+ case decodeRows result.rows of
1059+ Left errs -> throwError (Exception.error ("Row decode failed: " <> errs))
1060+ Right rows -> pure rows
1061+
1062+runQueryOne
1063+ :: forall tables result params paramsRL stage
1064+ . RowToList params paramsRL
1065+ => ParamsToArray paramsRL params
1066+ => ReadForeign { | result }
1067+ => Scylla.Client
1068+ -> { | params }
1069+ -> Q tables result params stage
1070+ -> Aff (Maybe { | result })
1071+runQueryOne client params q = do
1072+ rows <- runQuery client params q
1073+ pure case rows of
1074+ [ x ] -> Just x
1075+ _ -> Nothing
1076+
1077+runExecute
1078+ :: forall tables params paramsRL stage
1079+ . RowToList params paramsRL
1080+ => ParamsToArray paramsRL params
1081+ => Scylla.Client
1082+ -> { | params }
1083+ -> Q tables () params stage
1084+ -> Aff Unit
1085+runExecute client params (Q q) = do
1086+ let entries = paramsToArray (Proxy :: Proxy paramsRL) params
1087+ let { sql, values } = replaceNamedParams entries q.sql
1088+ let allValues = q.values <> values
1089+ void $ Scylla.execute (Scylla.CQL sql) allValues client
src/Yoga/ScyllaDB/ScyllaDB.purs+11−2
@@ -3,9 +3,10 @@ module Yoga.ScyllaDB.ScyllaDB where
33import Prelude
44
55import Data.Array as Array
6−import Data.Maybe (Maybe)
6+import Data.DateTime (DateTime)
7+import Data.Maybe (Maybe(..))
78import Data.Newtype (class Newtype)
8−import Data.Nullable (Nullable)
9+import Data.Nullable (Nullable, toNullable)
910import Data.Nullable as Nullable
1011import Data.Time.Duration (Milliseconds)
1112import Effect (Effect)
@@ -307,6 +308,14 @@ instance ToCQLValue Foreign where
307308instance ToCQLValue UUID where
308309 toCQLValue = unsafeCoerce
309310
311+instance ToCQLValue DateTime where
312+ toCQLValue = unsafeCoerce
313+
314+instance ToCQLValue a => ToCQLValue (Maybe a) where
315+ toCQLValue = case _ of
316+ Nothing -> unsafeCoerce (toNullable Nothing)
317+ Just value -> toCQLValue value
318+
310319instance ToCQLValue a => ToCQLValue (Array a) where
311320 toCQLValue arr = unsafeCoerce (map toCQLValue arr)
312321
test/Main.purs+6−5
@@ -17,6 +17,7 @@ import Test.Spec.Reporter.Console (consoleReporter)
1717import Test.Spec.Runner (runSpec)
1818import Yoga.Test.Docker as Docker
1919import Yoga.ScyllaDB.ScyllaDB as Scylla
20+import Yoga.ScyllaDB.Schema.Test as SchemaTest
2021
2122-- Test configuration
2223testContactPoint :: Scylla.ContactPoint
@@ -391,19 +392,19 @@ spec = do
391392
392393main :: Effect Unit
393394main = launchAff_ do
395+ -- Schema tests are pure (no Docker needed)
396+ runSpec [ consoleReporter ] SchemaTest.spec
397+
394398 liftEffect $ log "\n🧪 Starting ScyllaDB Integration Tests (with Docker)\n"
395399
396400 bracket
397− -- Start Docker before tests
398401 ( do
399402 liftEffect $ log "⏳ Starting ScyllaDB and waiting for it to be ready (may take 30-60s)..."
400− Docker.startService "packages/yoga-scylladb/docker-compose.test.yml" 60
403+ Docker.startService (Docker.ComposeFile "packages/yoga-scylladb/docker-compose.test.yml") (Docker.Timeout (Milliseconds 60000.0))
401404 liftEffect $ log "✅ ScyllaDB is ready!\n"
402405 )
403− -- Stop Docker after tests (always runs!)
404406 ( \_ -> do
405− Docker.stopService "packages/yoga-scylladb/docker-compose.test.yml"
407+ Docker.stopService (Docker.ComposeFile "packages/yoga-scylladb/docker-compose.test.yml")
406408 liftEffect $ log "✅ Cleanup complete\n"
407409 )
408− -- Run tests
409410 (\_ -> runSpec [ consoleReporter ] spec)
test/Yoga/ScyllaDB/Schema.test.jsnew file+2
@@ -0,0 +1,2 @@
1+export const foreignTypeName = (value) => value === null ? "null" : typeof value;
2+export const foreignStringValue = (value) => value;
test/Yoga/ScyllaDB/Schema.test.pursnew file+212
@@ -0,0 +1,212 @@
1+module Yoga.ScyllaDB.Schema.Test where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Maybe (Maybe(..), fromJust)
7+import Foreign (Foreign, unsafeToForeign)
8+import Partial.Unsafe (unsafePartial)
9+import Unsafe.Coerce (unsafeCoerce)
10+import Type.Function (type (#))
11+import Type.Proxy (Proxy(..))
12+import Data.DateTime (DateTime)
13+import Test.Spec (Spec, describe, it)
14+import Test.Spec.Assertions (shouldEqual)
15+import Yoga.ScyllaDB.ScyllaDB as Scylla
16+import Yoga.ScyllaDB.Schema as S
17+
18+foreign import foreignTypeName :: Foreign -> String
19+foreign import foreignStringValue :: Foreign -> String
20+
21+
22+type UsersTable = S.Table "users"
23+ ( id :: Scylla.UUID # S.PartitionKey
24+ , name :: String
25+ , email :: String
26+ )
27+
28+type PostsTable = S.Table "posts"
29+ ( user_id :: Scylla.UUID # S.PartitionKey
30+ , post_id :: Scylla.UUID # S.ClusteringKey
31+ , title :: String
32+ , body :: String
33+ )
34+
35+type EventsTable = S.Table "events"
36+ ( sensor_id :: String # S.PartitionKey
37+ , event_time :: DateTime # S.ClusteringKey
38+ , data :: String # S.Static
39+ , value :: Number
40+ )
41+
42+newtype EncodedValue = EncodedValue String
43+
44+instance Scylla.ToCQLValue EncodedValue where
45+ toCQLValue _ = unsafeToForeign "encoded"
46+
47+type EncodedTable = S.Table "encoded"
48+ ( value :: Maybe EncodedValue
49+ )
50+
51+spec :: Spec Unit
52+spec = do
53+ describe "ScyllaDB Schema" do
54+
55+ describe "CREATE TABLE DDL" do
56+
57+ it "generates DDL for simple partition key" do
58+ let ddl = S.createTableDDL @UsersTable
59+ ddl `shouldEqual`
60+ "CREATE TABLE IF NOT EXISTS users (email text, id uuid, name text, PRIMARY KEY ((id)))"
61+
62+ it "generates DDL with partition + clustering key" do
63+ let ddl = S.createTableDDL @PostsTable
64+ ddl `shouldEqual`
65+ "CREATE TABLE IF NOT EXISTS posts (body text, post_id uuid, title text, user_id uuid, PRIMARY KEY ((user_id), post_id))"
66+
67+ it "generates DDL with STATIC column" do
68+ let ddl = S.createTableDDL @EventsTable
69+ ddl `shouldEqual`
70+ "CREATE TABLE IF NOT EXISTS events (data text STATIC, event_time timestamp, sensor_id text, value double, PRIMARY KEY ((sensor_id), event_time))"
71+
72+ describe "INSERT SQL" do
73+
74+ it "generates INSERT for users" do
75+ let sql = S.insertSQLFor @UsersTable
76+ sql `shouldEqual`
77+ "INSERT INTO users (email, id, name) VALUES (?, ?, ?)"
78+
79+ describe "SELECT SQL" do
80+
81+ it "generates SELECT ALL" do
82+ let sql = S.selectAllSQLFor @UsersTable
83+ sql `shouldEqual` "SELECT * FROM users"
84+
85+ it "generates SELECT WHERE" do
86+ let sql = S.selectWhereSQLFor @UsersTable @(id :: Scylla.UUID)
87+ sql `shouldEqual`
88+ "SELECT * FROM users WHERE id = ?"
89+
90+ describe "UPDATE SQL" do
91+
92+ it "generates UPDATE with SET and WHERE" do
93+ let sql = S.updateSQLFor @UsersTable @(name :: String) @(id :: Scylla.UUID)
94+ sql `shouldEqual`
95+ "UPDATE users SET name = ? WHERE id = ?"
96+
97+ describe "DELETE SQL" do
98+
99+ it "generates DELETE WHERE" do
100+ let sql = S.deleteSQLFor @UsersTable @(id :: Scylla.UUID)
101+ sql `shouldEqual`
102+ "DELETE FROM users WHERE id = ?"
103+
104+ describe "CQL type names" do
105+
106+ it "maps Int to int" do
107+ S.cqlTypeName (Proxy :: Proxy Int) `shouldEqual` "int"
108+
109+ it "maps String to text" do
110+ S.cqlTypeName (Proxy :: Proxy String) `shouldEqual` "text"
111+
112+ it "maps Boolean to boolean" do
113+ S.cqlTypeName (Proxy :: Proxy Boolean) `shouldEqual` "boolean"
114+
115+ it "maps Number to double" do
116+ S.cqlTypeName (Proxy :: Proxy Number) `shouldEqual` "double"
117+
118+ it "maps DateTime to timestamp" do
119+ S.cqlTypeName (Proxy :: Proxy DateTime) `shouldEqual` "timestamp"
120+
121+ it "maps UUID to uuid" do
122+ S.cqlTypeName (Proxy :: Proxy Scylla.UUID) `shouldEqual` "uuid"
123+
124+ it "maps Array String to list<text>" do
125+ S.cqlTypeName (Proxy :: Proxy (Array String)) `shouldEqual` "list<text>"
126+
127+ it "maps Maybe Int to int" do
128+ S.cqlTypeName (Proxy :: Proxy (Maybe Int)) `shouldEqual` "int"
129+
130+ describe "ToCQLValue" do
131+
132+ it "unwraps Maybe through the inner CQL value conversion" do
133+ let encoded = Scylla.toCQLValue (Just (EncodedValue "raw") :: Maybe EncodedValue)
134+ foreignTypeName encoded `shouldEqual` "string"
135+ foreignStringValue encoded `shouldEqual` "encoded"
136+ foreignTypeName (Scylla.toCQLValue (Nothing :: Maybe EncodedValue)) `shouldEqual` "null"
137+
138+ it "uses ToCQLValue when extracting typed insert values" do
139+ let S.Q query = S.from (Proxy :: Proxy EncodedTable) # S.insert { value: Just (EncodedValue "raw") }
140+ Array.length query.values `shouldEqual` 1
141+ foreignStringValue (unsafePartial $ fromJust $ Array.head query.values) `shouldEqual` "encoded"
142+
143+ describe "Builder: SELECT" do
144+
145+ it "generates SELECT * with builder" do
146+ let sql = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.toSQL
147+ sql `shouldEqual` "SELECT * FROM users"
148+
149+ it "generates SELECT * WHERE with builder" do
150+ let sql = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.where_ @"id = $id" # S.toSQL
151+ sql `shouldEqual` "SELECT * FROM users WHERE id = $id"
152+
153+ it "generates SELECT with ORDER BY, LIMIT" do
154+ let sql = S.from (Proxy :: Proxy PostsTable) # S.selectAll # S.where_ @"user_id = $user_id" # S.orderBy @"post_id DESC" # S.limit @"10" # S.toSQL
155+ sql `shouldEqual` "SELECT * FROM posts WHERE user_id = $user_id ORDER BY post_id DESC LIMIT 10"
156+
157+ it "generates SELECT with ALLOW FILTERING" do
158+ let sql = S.from (Proxy :: Proxy UsersTable) # S.selectAll # S.where_ @"name = $name" # S.allowFiltering # S.toSQL
159+ sql `shouldEqual` "SELECT * FROM users WHERE name = $name ALLOW FILTERING"
160+
161+ it "generates SELECT specific columns" do
162+ let sql = S.from (Proxy :: Proxy UsersTable) # S.select @"name, email" # S.toSQL
163+ sql `shouldEqual` "SELECT name, email FROM users"
164+
165+ describe "Builder: INSERT" do
166+
167+ it "generates INSERT with builder" do
168+ let sql = S.from (Proxy :: Proxy UsersTable) # S.insert { id: unsafeUUID, name: "Alice", email: "a@b.com" } # S.toSQL
169+ sql `shouldEqual` "INSERT INTO users (email, id, name) VALUES (?, ?, ?)"
170+
171+ it "generates INSERT IF NOT EXISTS" do
172+ let sql = S.from (Proxy :: Proxy UsersTable) # S.insert { id: unsafeUUID, name: "Alice", email: "a@b.com" } # S.ifNotExists # S.toSQL
173+ sql `shouldEqual` "INSERT INTO users (email, id, name) VALUES (?, ?, ?) IF NOT EXISTS"
174+
175+ it "generates INSERT IF NOT EXISTS USING TTL" do
176+ let sql = S.from (Proxy :: Proxy UsersTable) # S.insert { id: unsafeUUID, name: "Alice", email: "a@b.com" } # S.ifNotExists # S.usingTTL @"3600" # S.toSQL
177+ sql `shouldEqual` "INSERT INTO users (email, id, name) VALUES (?, ?, ?) IF NOT EXISTS USING TTL 3600"
178+
179+ describe "Builder: UPDATE" do
180+
181+ it "generates UPDATE with builder" do
182+ let sql = S.from (Proxy :: Proxy UsersTable) # S.set { name: "Bob" } # S.where_ @"id = $id" # S.toSQL
183+ sql `shouldEqual` "UPDATE users SET name = ? WHERE id = $id"
184+
185+ it "generates UPDATE with TTL" do
186+ let sql = S.from (Proxy :: Proxy UsersTable) # S.withTTL @"86400" # S.set { name: "Bob" } # S.where_ @"id = $id" # S.toSQL
187+ sql `shouldEqual` "UPDATE users USING TTL 86400 SET name = ? WHERE id = $id"
188+
189+ describe "Builder: DELETE" do
190+
191+ it "generates DELETE with builder" do
192+ let sql = S.from (Proxy :: Proxy UsersTable) # S.delete # S.where_ @"id = $id" # S.toSQL
193+ sql `shouldEqual` "DELETE FROM users WHERE id = $id"
194+
195+ it "generates DELETE IF EXISTS" do
196+ let sql = S.from (Proxy :: Proxy UsersTable) # S.delete # S.where_ @"id = $id" # S.ifExists # S.toSQL
197+ sql `shouldEqual` "DELETE FROM users WHERE id = $id IF EXISTS"
198+
199+ describe "replaceNamedParams" do
200+
201+ it "replaces $name with ? and extracts values" do
202+ let entries = [ { name: "id", value: unsafeToForeign "test-uuid" } ]
203+ let result = S.replaceNamedParams entries "SELECT * FROM users WHERE id = $id"
204+ result.sql `shouldEqual` "SELECT * FROM users WHERE id = ?"
205+
206+ it "replaces multiple params" do
207+ let entries = [ { name: "id", value: unsafeToForeign "test-uuid" }, { name: "name", value: unsafeToForeign "Alice" } ]
208+ let result = S.replaceNamedParams entries "SELECT * FROM users WHERE id = $id AND name = $name"
209+ result.sql `shouldEqual` "SELECT * FROM users WHERE id = ? AND name = ?"
210+
211+unsafeUUID :: Scylla.UUID
212+unsafeUUID = unsafeCoerce "00000000-0000-0000-0000-000000000000"