@@ -0,0 +1,409 @@
1+module Test.Scylladb.Main where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Either (Either(..))
7+import Data.Maybe (Maybe(..))
8+import Effect (Effect)
9+import Effect.Aff (Aff, bracket, delay, launchAff_, throwError, try)
10+import Effect.Class (liftEffect)
11+import Effect.Console (log)
12+import Effect.Exception (error)
13+import Data.Time.Duration (Milliseconds(..))
14+import Test.Spec (Spec, around, describe, it)
15+import Test.Spec.Assertions (shouldEqual, shouldSatisfy)
16+import Test.Spec.Reporter.Console (consoleReporter)
17+import Test.Spec.Runner (runSpec)
18+import Yoga.Test.Docker as Docker
19+import Yoga.ScyllaDB.ScyllaDB as Scylla
20+
21+-- Test configuration
22+testContactPoint :: Scylla.ContactPoint
23+testContactPoint = Scylla.ContactPoint "127.0.0.1"
24+
25+testDatacenter :: Scylla.Datacenter
26+testDatacenter = Scylla.Datacenter "datacenter1"
27+
28+testKeyspace :: Scylla.Keyspace
29+testKeyspace = Scylla.Keyspace "test_ks"
30+
31+testPort :: Int
32+testPort = 9043 -- Test port from docker-compose.test.yml
33+
34+-- Helper to create and manage ScyllaDB connection
35+withScylla :: (Scylla.Client -> Aff Unit) -> Aff Unit
36+withScylla test = do
37+ client <- liftEffect $ Scylla.createClient
38+ { contactPoints: [ testContactPoint ]
39+ , localDataCenter: testDatacenter
40+ , protocolOptions: { port: testPort, maxSchemaAgreementWaitSeconds: 10 }
41+ }
42+ _ <- Scylla.connect client
43+
44+ -- Wait for cluster to be ready
45+ delay (Milliseconds 1000.0)
46+
47+ -- Clean up test keyspace before each test
48+ _ <- try $ Scylla.dropKeyspace testKeyspace client
49+
50+ test client
51+
52+ _ <- Scylla.shutdown client
53+ pure unit
54+
55+-- Helper to check if value is Left
56+isLeft :: forall a b. Either a b -> Boolean
57+isLeft (Left _) = true
58+isLeft _ = false
59+
60+-- Helper to set up test keyspace
61+setupKeyspace :: Scylla.Client -> Aff Unit
62+setupKeyspace client = do
63+ _ <- Scylla.createKeyspace testKeyspace (Scylla.ReplicationFactor 1) client
64+ _ <- Scylla.useKeyspace testKeyspace client
65+ pure unit
66+
67+-- Helper to set up users table
68+setupUsersTable :: Scylla.Client -> Aff Unit
69+setupUsersTable client = do
70+ _ <- Scylla.execute
71+ ( Scylla.CQL
72+ """
73+ CREATE TABLE test_ks.users (
74+ id uuid PRIMARY KEY,
75+ name text,
76+ email text,
77+ age int,
78+ active boolean
79+ )
80+ """
81+ )
82+ []
83+ client
84+ pure unit
85+
86+spec :: Spec Unit
87+spec = do
88+ describe "Yoga.ScyllaDB Integration Tests" do
89+
90+ -- Connection Management Tests
91+ around withScylla do
92+ describe "Connection Management" do
93+ it "connects to ScyllaDB successfully" \client -> do
94+ healthy <- Scylla.ping client
95+ healthy `shouldEqual` true
96+
97+ it "retrieves cluster hosts" \client -> do
98+ hosts <- liftEffect $ Scylla.getHosts client
99+ (Array.length hosts) `shouldSatisfy` (\l -> l > 0)
100+
101+ describe "Connection Errors" do
102+ it "handles connection failures" do
103+ result <- try do
104+ client <- liftEffect $ Scylla.createClient
105+ { contactPoints: [ Scylla.ContactPoint "invalid-host-9999" ]
106+ , localDataCenter: testDatacenter
107+ , protocolOptions: { port: 9999, maxSchemaAgreementWaitSeconds: 10 }
108+ }
109+ Scylla.connect client
110+ result `shouldSatisfy` isLeft
111+
112+ -- Keyspace Operations Tests
113+ around withScylla do
114+ describe "Keyspace Operations" do
115+ it "creates and drops keyspaces" \client -> do
116+ _ <- Scylla.createKeyspace testKeyspace (Scylla.ReplicationFactor 1) client
117+ _ <- Scylla.dropKeyspace testKeyspace client
118+ pure unit
119+
120+ it "uses keyspace" \client -> do
121+ setupKeyspace client
122+ result <- Scylla.useKeyspace testKeyspace client
123+ result.rowLength `shouldEqual` 0
124+
125+ it "retrieves keyspace metadata" \client -> do
126+ setupKeyspace client
127+ meta <- liftEffect $ Scylla.getKeyspace testKeyspace client
128+ case meta of
129+ Just _ -> pure unit
130+ Nothing -> throwError (error "Expected to find keyspace metadata")
131+
132+ -- Table Operations Tests
133+ around withScylla do
134+ describe "Table Operations" do
135+ it "creates tables with various column types" \client -> do
136+ setupKeyspace client
137+ result <- Scylla.execute
138+ ( Scylla.CQL
139+ """
140+ CREATE TABLE test_ks.users (
141+ id uuid PRIMARY KEY,
142+ name text,
143+ age int,
144+ email text,
145+ active boolean,
146+ created_at timestamp
147+ )
148+ """
149+ )
150+ []
151+ client
152+ result.rowLength `shouldEqual` 0
153+
154+ it "inserts and queries data" \client -> do
155+ setupKeyspace client
156+ setupUsersTable client
157+
158+ userId <- liftEffect Scylla.uuid
159+ _ <- Scylla.execute (Scylla.CQL "INSERT INTO test_ks.users (id, name, age) VALUES (?, ?, ?)")
160+ [ Scylla.toCQLValue userId, Scylla.toCQLValue "Alice", Scylla.toCQLValue 30 ]
161+ client
162+
163+ result <- Scylla.execute (Scylla.CQL "SELECT * FROM test_ks.users WHERE id = ?")
164+ [ Scylla.toCQLValue userId ]
165+ client
166+ result.rowLength `shouldEqual` 1
167+
168+ it "updates data" \client -> do
169+ setupKeyspace client
170+ setupUsersTable client
171+
172+ userId <- liftEffect Scylla.uuid
173+ _ <- Scylla.execute (Scylla.CQL "INSERT INTO test_ks.users (id, name, email) VALUES (?, ?, ?)")
174+ [ Scylla.toCQLValue userId, Scylla.toCQLValue "Bob", Scylla.toCQLValue "bob@example.com" ]
175+ client
176+
177+ _ <- Scylla.execute (Scylla.CQL "UPDATE test_ks.users SET email = ? WHERE id = ?")
178+ [ Scylla.toCQLValue "bob.new@example.com", Scylla.toCQLValue userId ]
179+ client
180+
181+ result <- Scylla.execute (Scylla.CQL "SELECT * FROM test_ks.users WHERE id = ?")
182+ [ Scylla.toCQLValue userId ]
183+ client
184+ result.rowLength `shouldEqual` 1
185+
186+ it "deletes data" \client -> do
187+ setupKeyspace client
188+ setupUsersTable client
189+
190+ userId <- liftEffect Scylla.uuid
191+ _ <- Scylla.execute (Scylla.CQL "INSERT INTO test_ks.users (id, name) VALUES (?, ?)")
192+ [ Scylla.toCQLValue userId, Scylla.toCQLValue "Charlie" ]
193+ client
194+
195+ _ <- Scylla.execute (Scylla.CQL "DELETE FROM test_ks.users WHERE id = ?")
196+ [ Scylla.toCQLValue userId ]
197+ client
198+
199+ result <- Scylla.execute (Scylla.CQL "SELECT * FROM test_ks.users WHERE id = ?")
200+ [ Scylla.toCQLValue userId ]
201+ client
202+ result.rowLength `shouldEqual` 0
203+
204+ -- Consistency Level Tests
205+ around withScylla do
206+ describe "Consistency Levels" do
207+ it "executes with ONE consistency level" \client -> do
208+ setupKeyspace client
209+ result <- Scylla.executeWithOptions
210+ (Scylla.CQL "SELECT * FROM system.local")
211+ []
212+ { consistency: Scylla.One }
213+ client
214+ result.rowLength `shouldSatisfy` (\l -> l > 0)
215+
216+ it "executes with QUORUM consistency level" \client -> do
217+ setupKeyspace client
218+ result <- Scylla.executeWithOptions
219+ (Scylla.CQL "SELECT * FROM system.local")
220+ []
221+ { consistency: Scylla.Quorum }
222+ client
223+ result.rowLength `shouldSatisfy` (\l -> l > 0)
224+
225+ it "executes with LOCAL_ONE consistency level" \client -> do
226+ setupKeyspace client
227+ result <- Scylla.executeWithOptions
228+ (Scylla.CQL "SELECT * FROM system.local")
229+ []
230+ { consistency: Scylla.LocalOne }
231+ client
232+ result.rowLength `shouldSatisfy` (\l -> l > 0)
233+
234+ -- Prepared Statement Tests
235+ around withScylla do
236+ describe "Prepared Statements" do
237+ it "prepares and executes statements" \client -> do
238+ setupKeyspace client
239+ setupUsersTable client
240+
241+ prepared <- Scylla.prepare
242+ (Scylla.CQL "INSERT INTO test_ks.users (id, name) VALUES (?, ?)")
243+ client
244+
245+ userId <- liftEffect Scylla.uuid
246+ result <- Scylla.executePrepared prepared
247+ [ Scylla.toCQLValue userId, Scylla.toCQLValue "Diana" ]
248+ client
249+ result.rowLength `shouldEqual` 0
250+
251+ it "reuses prepared statements for multiple inserts" \client -> do
252+ setupKeyspace client
253+ setupUsersTable client
254+
255+ prepared <- Scylla.prepare
256+ (Scylla.CQL "INSERT INTO test_ks.users (id, name) VALUES (?, ?)")
257+ client
258+
259+ userId1 <- liftEffect Scylla.uuid
260+ userId2 <- liftEffect Scylla.uuid
261+ userId3 <- liftEffect Scylla.uuid
262+
263+ _ <- Scylla.executePrepared prepared [ Scylla.toCQLValue userId1, Scylla.toCQLValue "Eve" ] client
264+ _ <- Scylla.executePrepared prepared [ Scylla.toCQLValue userId2, Scylla.toCQLValue "Frank" ] client
265+ _ <- Scylla.executePrepared prepared [ Scylla.toCQLValue userId3, Scylla.toCQLValue "Grace" ] client
266+
267+ result <- Scylla.execute (Scylla.CQL "SELECT * FROM test_ks.users") [] client
268+ result.rowLength `shouldEqual` 3
269+
270+ -- Batch Operations Tests
271+ around withScylla do
272+ describe "Batch Operations" do
273+ it "executes batch inserts" \client -> do
274+ setupKeyspace client
275+ setupUsersTable client
276+
277+ userId1 <- liftEffect Scylla.uuid
278+ userId2 <- liftEffect Scylla.uuid
279+
280+ let
281+ batchQueries =
282+ [ { query: Scylla.CQL "INSERT INTO test_ks.users (id, name) VALUES (?, ?)"
283+ , params: [ Scylla.toCQLValue userId1, Scylla.toCQLValue "Henry" ]
284+ }
285+ , { query: Scylla.CQL "INSERT INTO test_ks.users (id, name) VALUES (?, ?)"
286+ , params: [ Scylla.toCQLValue userId2, Scylla.toCQLValue "Iris" ]
287+ }
288+ ]
289+
290+ _ <- Scylla.batch batchQueries client
291+
292+ result <- Scylla.execute (Scylla.CQL "SELECT * FROM test_ks.users") [] client
293+ result.rowLength `shouldEqual` 2
294+
295+ it "executes batch with consistency options" \client -> do
296+ setupKeyspace client
297+ setupUsersTable client
298+
299+ userId <- liftEffect Scylla.uuid
300+
301+ let
302+ batchQueries =
303+ [ { query: Scylla.CQL "INSERT INTO test_ks.users (id, name) VALUES (?, ?)"
304+ , params: [ Scylla.toCQLValue userId, Scylla.toCQLValue "Jack" ]
305+ }
306+ ]
307+
308+ result <- Scylla.batchWithOptions batchQueries
309+ { consistency: Scylla.One, logged: true }
310+ client
311+ result.rowLength `shouldEqual` 0
312+
313+ -- UUID Tests
314+ around withScylla do
315+ describe "UUID Operations" do
316+ it "generates UUIDs" \_ -> do
317+ _ <- liftEffect Scylla.uuid
318+ _ <- liftEffect Scylla.uuid
319+ pure unit -- Just check it doesn't error
320+
321+ it "generates time-based UUIDs" \_ -> do
322+ _ <- liftEffect Scylla.timeUuid
323+ _ <- liftEffect Scylla.timeUuid
324+ pure unit -- Just check it doesn't error
325+
326+ it "parses UUIDs from strings" \_ -> do
327+ -- Valid UUID v4 format
328+ maybeUuid <- liftEffect $ Scylla.uuidFromString "550e8400-e29b-41d4-a716-446655440000"
329+ case maybeUuid of
330+ Just _ -> pure unit
331+ Nothing -> throwError (error "Expected to parse valid UUID")
332+
333+ it "returns Nothing for invalid UUID strings" \_ -> do
334+ maybeUuid <- liftEffect $ Scylla.uuidFromString "not-a-valid-uuid"
335+ case maybeUuid of
336+ Nothing -> pure unit
337+ Just _ -> throwError (error "Expected Nothing for invalid UUID")
338+
339+ -- Data Type Tests
340+ around withScylla do
341+ describe "Data Types" do
342+ it "handles various CQL data types" \client -> do
343+ setupKeyspace client
344+ setupUsersTable client
345+
346+ userId <- liftEffect Scylla.uuid
347+ _ <- Scylla.execute (Scylla.CQL "INSERT INTO test_ks.users (id, name, age, active) VALUES (?, ?, ?, ?)")
348+ [ Scylla.toCQLValue userId, Scylla.toCQLValue "Kate", Scylla.toCQLValue 25, Scylla.toCQLValue true ]
349+ client
350+
351+ result <- Scylla.execute (Scylla.CQL "SELECT * FROM test_ks.users WHERE id = ?")
352+ [ Scylla.toCQLValue userId ]
353+ client
354+ result.rowLength `shouldEqual` 1
355+
356+ it "handles null values" \client -> do
357+ setupKeyspace client
358+ setupUsersTable client
359+
360+ userId <- liftEffect Scylla.uuid
361+ _ <- Scylla.execute (Scylla.CQL "INSERT INTO test_ks.users (id, name) VALUES (?, ?)")
362+ [ Scylla.toCQLValue userId, Scylla.toCQLValue "Leo" ]
363+ client
364+
365+ result <- Scylla.execute (Scylla.CQL "SELECT * FROM test_ks.users WHERE id = ?")
366+ [ Scylla.toCQLValue userId ]
367+ client
368+ result.rowLength `shouldEqual` 1
369+
370+ -- Error Handling Tests
371+ around withScylla do
372+ describe "Error Handling" do
373+ it "handles invalid CQL syntax" \client -> do
374+ result <- try $ Scylla.execute (Scylla.CQL "INVALID CQL SYNTAX") [] client
375+ case result of
376+ Left _ -> pure unit
377+ Right _ -> throwError (error "Expected error for invalid CQL syntax")
378+
379+ it "handles missing table errors" \client -> do
380+ setupKeyspace client
381+ result <- try $ Scylla.execute (Scylla.CQL "SELECT * FROM test_ks.non_existent_table") [] client
382+ case result of
383+ Left _ -> pure unit
384+ Right _ -> throwError (error "Expected error for missing table")
385+
386+ it "handles missing keyspace errors" \client -> do
387+ result <- try $ Scylla.execute (Scylla.CQL "SELECT * FROM nonexistent_ks.users") [] client
388+ case result of
389+ Left _ -> pure unit
390+ Right _ -> throwError (error "Expected error for missing keyspace")
391+
392+main :: Effect Unit
393+main = launchAff_ do
394+ liftEffect $ log "\n🧪 Starting ScyllaDB Integration Tests (with Docker)\n"
395+
396+ bracket
397+ -- Start Docker before tests
398+ ( do
399+ 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
401+ liftEffect $ log "✅ ScyllaDB is ready!\n"
402+ )
403+ -- Stop Docker after tests (always runs!)
404+ ( \_ -> do
405+ Docker.stopService "packages/yoga-scylladb/docker-compose.test.yml"
406+ liftEffect $ log "✅ Cleanup complete\n"
407+ )
408+ -- Run tests
409+ (\_ -> runSpec [ consoleReporter ] spec)