forked from
slices.network/quickslice
Auto-indexing service and GraphQL API for AT Protocol Records
1/// Integration tests for database executors
2///
3/// These tests verify that both SQLite and PostgreSQL executors work correctly
4/// with actual database connections.
5///
6/// SQLite tests run with in-memory databases automatically.
7/// PostgreSQL tests require POSTGRES_TEST_URL environment variable to be set.
8/// If not set, PostgreSQL tests will be skipped.
9import database/executor.{type Executor, Bool, Int, PostgreSQL, SQLite, Text}
10import database/postgres/connection as pg_connection
11import database/sqlite/connection as sqlite_connection
12import envoy
13import gleam/dynamic/decode
14import gleam/int
15import gleam/io
16import gleam/option.{None, Some}
17import gleeunit/should
18
19// ===== SQLite Integration Tests =====
20
21pub fn sqlite_connect_test() {
22 let result = sqlite_connection.connect("sqlite::memory:")
23 result |> should.be_ok
24}
25
26pub fn sqlite_create_table_test() {
27 let assert Ok(exec) = sqlite_connection.connect("sqlite::memory:")
28
29 let result =
30 executor.exec(
31 exec,
32 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
33 [],
34 )
35
36 result |> should.be_ok
37}
38
39pub fn sqlite_insert_and_select_test() {
40 let assert Ok(exec) = sqlite_connection.connect("sqlite::memory:")
41
42 // Create table
43 let assert Ok(_) =
44 executor.exec(
45 exec,
46 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, active INTEGER NOT NULL)",
47 [],
48 )
49
50 // Insert data
51 let assert Ok(_) =
52 executor.exec(
53 exec,
54 "INSERT INTO users (id, name, active) VALUES (?, ?, ?)",
55 [
56 Int(1),
57 Text("Alice"),
58 Bool(True),
59 ],
60 )
61
62 let assert Ok(_) =
63 executor.exec(
64 exec,
65 "INSERT INTO users (id, name, active) VALUES (?, ?, ?)",
66 [
67 Int(2),
68 Text("Bob"),
69 Bool(False),
70 ],
71 )
72
73 // Query data
74 let decoder = {
75 use id <- decode.field(0, decode.int)
76 use name <- decode.field(1, decode.string)
77 use active <- decode.field(2, decode.int)
78 decode.success(#(id, name, active))
79 }
80
81 let result =
82 executor.query(
83 exec,
84 "SELECT id, name, active FROM users ORDER BY id",
85 [],
86 decoder,
87 )
88
89 result |> should.be_ok
90 let assert Ok(rows) = result
91 rows |> should.equal([#(1, "Alice", 1), #(2, "Bob", 0)])
92}
93
94pub fn sqlite_placeholder_format_test() {
95 let assert Ok(exec) = sqlite_connection.connect("sqlite::memory:")
96
97 // SQLite should use ? placeholders
98 executor.placeholder(exec, 1) |> should.equal("?")
99 executor.placeholder(exec, 5) |> should.equal("?")
100}
101
102pub fn sqlite_dialect_test() {
103 let assert Ok(exec) = sqlite_connection.connect("sqlite::memory:")
104 executor.dialect(exec) |> should.equal(SQLite)
105}
106
107// ===== PostgreSQL Integration Tests =====
108// These tests require POSTGRES_TEST_URL to be set
109
110fn get_postgres_exec() -> Result(Executor, Nil) {
111 case envoy.get("POSTGRES_TEST_URL") {
112 Ok(url) -> {
113 case pg_connection.connect(url) {
114 Ok(exec) -> Ok(exec)
115 Error(_err) -> {
116 io.println("PostgreSQL connection failed")
117 Error(Nil)
118 }
119 }
120 }
121 Error(_) -> Error(Nil)
122 }
123}
124
125fn skip_if_no_postgres(test_fn: fn(Executor) -> Nil) -> Nil {
126 case get_postgres_exec() {
127 Ok(exec) -> test_fn(exec)
128 Error(_) -> {
129 // Silently skip - PostgreSQL not configured
130 Nil
131 }
132 }
133}
134
135pub fn postgres_connect_test() {
136 skip_if_no_postgres(fn(exec) {
137 // If we got here, connection succeeded
138 executor.dialect(exec) |> should.equal(PostgreSQL)
139 })
140}
141
142pub fn postgres_placeholder_format_test() {
143 skip_if_no_postgres(fn(exec) {
144 // PostgreSQL should use $1, $2, etc. placeholders
145 executor.placeholder(exec, 1) |> should.equal("$1")
146 executor.placeholder(exec, 5) |> should.equal("$5")
147 })
148}
149
150pub fn postgres_create_and_drop_table_test() {
151 skip_if_no_postgres(fn(exec) {
152 // Use a unique table name to avoid conflicts
153 let table_name = "test_integration_" <> int.to_string(erlang_timestamp())
154
155 // Create table
156 let create_result =
157 executor.exec(
158 exec,
159 "CREATE TABLE "
160 <> table_name
161 <> " (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
162 [],
163 )
164 create_result |> should.be_ok
165
166 // Drop table (cleanup)
167 let drop_result = executor.exec(exec, "DROP TABLE " <> table_name, [])
168 drop_result |> should.be_ok
169 })
170}
171
172pub fn postgres_insert_and_select_test() {
173 skip_if_no_postgres(fn(exec) {
174 let table_name = "test_crud_" <> int.to_string(erlang_timestamp())
175
176 // Create table
177 let assert Ok(_) =
178 executor.exec(
179 exec,
180 "CREATE TABLE "
181 <> table_name
182 <> " (id SERIAL PRIMARY KEY, name TEXT NOT NULL, active BOOLEAN NOT NULL)",
183 [],
184 )
185
186 // Insert data using PostgreSQL placeholders
187 let assert Ok(_) =
188 executor.exec(
189 exec,
190 "INSERT INTO " <> table_name <> " (name, active) VALUES ($1, $2)",
191 [Text("Alice"), Bool(True)],
192 )
193
194 let assert Ok(_) =
195 executor.exec(
196 exec,
197 "INSERT INTO " <> table_name <> " (name, active) VALUES ($1, $2)",
198 [Text("Bob"), Bool(False)],
199 )
200
201 // Query data
202 let decoder = {
203 use name <- decode.field(0, decode.string)
204 use active <- decode.field(1, decode.bool)
205 decode.success(#(name, active))
206 }
207
208 let result =
209 executor.query(
210 exec,
211 "SELECT name, active FROM " <> table_name <> " ORDER BY name",
212 [],
213 decoder,
214 )
215
216 result |> should.be_ok
217 let assert Ok(rows) = result
218 rows |> should.equal([#("Alice", True), #("Bob", False)])
219
220 // Cleanup
221 let assert Ok(_) = executor.exec(exec, "DROP TABLE " <> table_name, [])
222 Nil
223 })
224}
225
226pub fn postgres_null_handling_test() {
227 skip_if_no_postgres(fn(exec) {
228 let table_name = "test_null_" <> int.to_string(erlang_timestamp())
229
230 // Create table with nullable column
231 let assert Ok(_) =
232 executor.exec(
233 exec,
234 "CREATE TABLE " <> table_name <> " (id SERIAL PRIMARY KEY, value TEXT)",
235 [],
236 )
237
238 // Insert NULL value
239 let assert Ok(_) =
240 executor.exec(
241 exec,
242 "INSERT INTO " <> table_name <> " (value) VALUES ($1)",
243 [executor.Null],
244 )
245
246 // Insert actual value
247 let assert Ok(_) =
248 executor.exec(
249 exec,
250 "INSERT INTO " <> table_name <> " (value) VALUES ($1)",
251 [Text("not null")],
252 )
253
254 // Query and check
255 let decoder = {
256 use value <- decode.field(0, decode.optional(decode.string))
257 decode.success(value)
258 }
259
260 let result =
261 executor.query(
262 exec,
263 "SELECT value FROM " <> table_name <> " ORDER BY id",
264 [],
265 decoder,
266 )
267
268 result |> should.be_ok
269 let assert Ok(rows) = result
270 rows |> should.equal([None, Some("not null")])
271
272 // Cleanup
273 let assert Ok(_) = executor.exec(exec, "DROP TABLE " <> table_name, [])
274 Nil
275 })
276}
277
278// Helper to get a unique timestamp for table names
279@external(erlang, "os", "system_time")
280fn erlang_timestamp() -> Int