Auto-indexing service and GraphQL API for AT Protocol Records
at main 280 lines 7.1 kB view raw
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