🐿️ Type safe SQL in Gleam
0

Configure Feed

Select the types of activity you want to include in your feed.

:white_check_mark: Add integration tests

+355 -21
+2 -3
.github/workflows/test.yml
··· 64 64 with: 65 65 gleam_version: 1.4.1 66 66 erlang_version: ${{ matrix.erlang }} 67 - - uses: TanklesXL/gleam_actions/.github/actions/test@main 68 - with: 69 - target: "erlang" 67 + - name: "integration test" 68 + run: ./integration_test
+1
.gitignore
··· 4 4 erl_crash.dump 5 5 src/sql 6 6 src/sql.gleam 7 + integration_test_project
+17 -6
CONTRIBUTING.md
··· 31 31 case to the `squirrel/internal/query.gleam_type_to_field_type` function. 32 32 This is used when the type is in the values returned by the query to 33 33 write down the type of the corresponding field 34 - - And don't forget to add some tests :) 34 + - And don't forget to add some tests and integration tests :) 35 35 36 36 ## Writing tests 37 37 ··· 46 46 that it is easier to focus on a specific aspect of the code when reviewing it 47 47 - Use a long descriptive title for the snapshots: a title should describe what 48 48 one expects to see in the produced snapshot to guide the review process 49 + 50 + ## Integration tests 51 + 52 + Squirrel also has some integration tests to check that the generated code 53 + actually compiles and produces the expected results. 54 + 55 + - To add a new integration test you can add a new item to the 56 + `integration_tests` list in `test/integration_test.gleam` 57 + - To run the tests you can run the `./integration_test` script in this project. 58 + Read below to set up all the required bits for tests to work 49 59 50 60 ## Running the tests 51 61 ··· 55 65 56 66 - In CI this is taken care of automatically 57 67 - Locally you have two options: 58 - - Use Docker Compose: the project comes with a `docker-compose.yaml` file that sets up 59 - the instance, so you can run `docker compose up` to start it and run your tests 60 - - Manually set up a Postgres server: you'll have to make sure you have a server running 61 - with a user called `squirrel_test` that must be able to read and write to a database 62 - called `squirrel_test` 68 + - Use Docker Compose: the project comes with a `docker-compose.yaml` file that 69 + sets up the instance, so you can run `docker compose up` to start it and run 70 + your tests 71 + - Manually set up a Postgres server: you'll have to make sure you have a 72 + server running with a user called `squirrel_test` that must be able to read 73 + and write to a database called `squirrel_test`
+1 -1
README.md
··· 185 185 | postgres type | encoded as | decoded as | 186 186 | ------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | 187 187 | `bool` | `Bool` | `Bool` | 188 - | `text`, `char`, `bchar`, `varchar` | `String` | `String` | 188 + | `text`, `char`, `bpchar`, `varchar` | `String` | `String` | 189 189 | `float4`, `float8`, `numeric` | `Float` | `Float` | 190 190 | `int2`, `int4`, `int8` | `Int` | `Int` | 191 191 | `json`, `jsonb` | [`Json`](https://hexdocs.pm/gleam_json/gleam/json.html#Json) | `String` |
+3 -1
gleam.toml
··· 23 23 gleam_crypto = ">= 1.3.0 and < 2.0.0" 24 24 tom = ">= 1.0.1 and < 2.0.0" 25 25 youid = ">= 1.2.0 and < 2.0.0" 26 + gleam_pgo = ">= 0.14.0 and < 1.0.0" 26 27 27 28 [dev-dependencies] 28 29 gleeunit = ">= 1.0.0 and < 2.0.0" 29 30 birdie = ">= 1.1.8 and < 2.0.0" 30 31 temporary = ">= 1.0.0 and < 2.0.0" 31 - gleam_pgo = ">= 0.13.0 and < 1.0.0" 32 + shellout = ">= 1.6.0 and < 2.0.0" 33 + gleam_erlang = ">= 0.27.0 and < 1.0.0"
+2
integration_test
··· 1 + #!/bin/bash 2 + DATABASE_URL=postgres://squirrel_test@localhost/squirrel_test gleam test -- integration
+4 -1
manifest.toml
··· 28 28 { name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" }, 29 29 { name = "pgo", version = "0.14.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9" }, 30 30 { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, 31 + { name = "shellout", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "E2FCD18957F0E9F67E1F497FC9FF57393392F8A9BAEAEA4779541DE7A68DD7E0" }, 31 32 { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, 32 33 { name = "temporary", version = "1.0.0", build_tools = ["gleam"], requirements = ["envoy", "exception", "filepath", "gleam_crypto", "gleam_stdlib", "simplifile"], otp_app = "temporary", source = "hex", outer_checksum = "51C0FEF4D72CE7CA507BD188B21C1F00695B3D5B09D7DFE38240BFD3A8E1E9B3" }, 33 34 { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, ··· 47 48 glam = { version = ">= 2.0.1 and < 3.0.0" } 48 49 gleam_community_ansi = { version = ">= 1.4.0 and < 2.0.0" } 49 50 gleam_crypto = { version = ">= 1.3.0 and < 2.0.0" } 51 + gleam_erlang = { version = ">= 0.27.0 and < 1.0.0" } 50 52 gleam_json = { version = ">= 1.0.0 and < 3.0.0" } 51 - gleam_pgo = { version = ">= 0.13.0 and < 1.0.0" } 53 + gleam_pgo = { version = ">= 0.14.0 and < 1.0.0" } 52 54 gleam_stdlib = { version = ">= 0.39.0 and < 2.0.0" } 53 55 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 54 56 justin = { version = ">= 1.0.1 and < 2.0.0" } 55 57 mug = { version = ">= 1.1.0 and < 2.0.0" } 56 58 pgo = { version = ">= 0.14.0 and < 1.0.0" } 59 + shellout = { version = ">= 1.6.0 and < 2.0.0" } 57 60 simplifile = { version = ">= 2.0.1 and < 3.0.0" } 58 61 temporary = { version = ">= 1.0.0 and < 2.0.0" } 59 62 term_size = { version = ">= 1.0.1 and < 2.0.0" }
+314
test/integration_test.gleam
··· 1 + import argv 2 + import filepath 3 + import gleam/bool 4 + import gleam/erlang 5 + import gleam/erlang/atom 6 + import gleam/io 7 + import gleam/list 8 + import gleam/regex 9 + import gleam/string 10 + import shellout 11 + import simplifile 12 + import temporary 13 + 14 + /// An integration test for a specific postgres type. 15 + type TestType { 16 + TestType(postgres_type: String, values: List(TestValue)) 17 + } 18 + 19 + /// How a tested value looks in gleam code when it is fed into a query or 20 + /// returned as a result. 21 + /// 22 + type TestValue { 23 + TestInputOutput( 24 + /// How the value is supposed to look like when passed in as a query 25 + /// argument to a squirrel generated query: 26 + /// 27 + /// ```gleam 28 + /// let assert Ok(_) = 29 + /// sql.insert(db, <input>) 30 + /// // ^^^^^^^ This will be replaced with the input string 31 + /// ``` 32 + /// 33 + input: String, 34 + /// How the value is supposed to look like when returned as an output 35 + /// from a squirrel generated query: 36 + /// 37 + /// ```gleam 38 + /// let assert Ok(pgo.Returned(_, [result])) sql.select_one(db) 39 + /// let assert <output> = result.col 40 + /// // ^^^^^^^^ This will be replaced with the output string 41 + /// ``` 42 + /// 43 + output: String, 44 + ) 45 + TestValue(value: String) 46 + } 47 + 48 + const integration_tests = [ 49 + // Booleans 50 + TestType("bool", [TestValue("True"), TestValue("False")]), 51 + // Text data 52 + TestType("text", [TestValue("\"hello\"")]), 53 + TestType("char(1)", [TestValue("\"j\"")]), 54 + TestType("bpchar", [TestValue("\"j\"")]), 55 + TestType("varchar(3)", [TestValue("\"jak\"")]), 56 + // Integers 57 + TestType("int2", [TestValue("1")]), 58 + TestType("int4", [TestValue("1")]), 59 + TestType("int8", [TestValue("1")]), 60 + // Floats 61 + TestType("float4", [TestValue("1.0")]), 62 + TestType("float8", [TestValue("1.0")]), 63 + TestType("numeric", [TestValue("1.0")]), 64 + // Uuid 65 + TestType("uuid", [TestValue("uuid.v7()")]), 66 + // Bytea 67 + TestType("bytea", [TestValue("<<1, 2, 3>>")]), 68 + // Date 69 + TestType("date", [TestValue("#(1998, 10, 11)")]), 70 + // Timestamp 71 + TestType("timestamp", [TestValue("#(#(1998, 10, 11), #(22, 10, 00))")]), 72 + // Array 73 + TestType("int[]", [TestValue("[1, 2, 3]")]), 74 + // Json 75 + TestType( 76 + "json", 77 + [ 78 + TestInputOutput( 79 + input: "json.object([#(\"a\", json.int(1))])", 80 + output: "\"{\\\"a\\\":1}\"", 81 + ), 82 + ], 83 + ), 84 + TestType( 85 + "jsonb", 86 + [ 87 + TestInputOutput( 88 + input: "json.object([#(\"a\", json.int(1))])", 89 + output: "\"{\\\"a\\\": 1}\"", 90 + ), 91 + ], 92 + ), 93 + ] 94 + 95 + const project_toml = "name = \"integration_test_project\" 96 + version = \"1.0.0\" 97 + 98 + [dependencies] 99 + squirrel = { path = \"../..\" } 100 + " 101 + 102 + fn with_timeout(seconds seconds: Int, run fun: fn() -> a) { 103 + #(atom.create_from_string("timeout"), seconds, [fun]) 104 + } 105 + 106 + pub fn integration_test_() { 107 + // This test takes a while since it has to compile an entire gleam project. 108 + // So we have to increase the timeout 109 + use <- with_timeout(seconds: 60) 110 + 111 + // This test takes some time se we skip it unless one passes the "integration" 112 + // flag from command line. 113 + use <- bool.guard(when: argv.load().arguments != ["integration"], return: Nil) 114 + 115 + let assert Ok(result) = run_integration_tests(integration_tests) 116 + case result { 117 + Ok(_) -> Nil 118 + Error(#(_status_code, message)) -> { 119 + io.println(message) 120 + panic as "integration test failed" 121 + } 122 + } 123 + 124 + Nil 125 + } 126 + 127 + fn run_integration_tests( 128 + values: List(TestType), 129 + ) -> Result(Result(String, #(Int, String)), simplifile.FileError) { 130 + let integration_test_project = filepath.join(".", "integration_test_project") 131 + let _ = simplifile.create_directory(integration_test_project) 132 + use dir <- temporary.create( 133 + temporary.directory() |> temporary.in_directory(integration_test_project), 134 + ) 135 + 136 + scaffold_gleam_project(dir) 137 + let code = { 138 + use code, TestType(postgres_type, values) <- list.fold(values, "") 139 + let table_name = safe_name(postgres_type) <> "_table" 140 + create_database_table(table_name, postgres_type) 141 + let assertions = 142 + create_query_files_and_assertions(postgres_type, table_name, values, dir) 143 + code <> "\n\n" <> assertions 144 + } 145 + write_main(code, to: dir) 146 + 147 + let _ = erlang.get_line(">") 148 + test_project(dir) 149 + } 150 + 151 + fn create_database_table(table_name: String, postgres_type: String) -> Nil { 152 + let drop = "drop table if exists " <> table_name 153 + let create = "create table if not exists " <> table_name <> "( 154 + id bigserial primary key, 155 + col " <> postgres_type <> " not null 156 + )" 157 + 158 + let assert Ok(_) = 159 + shellout.command( 160 + run: "psql", 161 + with: ["-Usquirrel_test", "-dsquirrel_test", "-c" <> drop], 162 + in: ".", 163 + opt: [], 164 + ) 165 + 166 + let assert Ok(_) = 167 + shellout.command( 168 + run: "psql", 169 + with: ["-Usquirrel_test", "-dsquirrel_test", "-c" <> create], 170 + in: ".", 171 + opt: [], 172 + ) 173 + 174 + Nil 175 + } 176 + 177 + /// Creates the most basic structure of a gleam project needed to run the 178 + /// integration tests inside `dir`. 179 + /// 180 + fn scaffold_gleam_project(dir: String) -> Nil { 181 + // We set up the canonical squirrel project structure. 182 + // 183 + let src_dir = filepath.join(dir, "src") 184 + let sql_dir = filepath.join(src_dir, "sql") 185 + let assert Ok(_) = simplifile.create_directory(src_dir) 186 + let assert Ok(_) = simplifile.create_directory(sql_dir) 187 + 188 + // Write the `gleam.toml` needed to run the project. 189 + // 190 + let assert Ok(_) = 191 + simplifile.write(project_toml, to: filepath.join(dir, "gleam.toml")) 192 + 193 + Nil 194 + } 195 + 196 + /// Following squirrel's project structure, this writes a file for each query to 197 + /// be tested on the given table and returns the Gleam code that runs the 198 + /// assertions to check everything works fine. 199 + /// 200 + fn create_query_files_and_assertions( 201 + postgres_type: String, 202 + table_name: String, 203 + values: List(TestValue), 204 + dir: String, 205 + ) -> String { 206 + let src_dir = filepath.join(dir, "src") 207 + let sql_dir = filepath.join(src_dir, "sql") 208 + 209 + // Then we create all the queries to be fed into squirrel. 210 + // 211 + let insert = table_name <> "_insert" 212 + let assert Ok(_) = 213 + "insert into <table>(col) values ($1)" 214 + |> string.replace(each: "<table>", with: table_name) 215 + |> simplifile.write(to: filepath.join(sql_dir, insert <> ".sql")) 216 + 217 + let select_all = table_name <> "_select_all" 218 + let assert Ok(_) = 219 + "select col from <table>" 220 + |> string.replace(each: "<table>", with: table_name) 221 + |> simplifile.write(to: filepath.join(sql_dir, select_all <> ".sql")) 222 + 223 + let delete = table_name <> "_delete_rows" 224 + let assert Ok(_) = 225 + "delete from <table>" 226 + |> string.replace(each: "<table>", with: table_name) 227 + |> simplifile.write(to: filepath.join(sql_dir, delete <> ".sql")) 228 + 229 + let assertions = { 230 + use value <- list.map(values) 231 + let heading = case value { 232 + TestInputOutput(input:, output:) -> " 233 + let input = " <> input <> " 234 + let expected_output = " <> output <> "\n" 235 + TestValue(value:) -> " 236 + let input = " <> value <> " 237 + let expected_output = input" 238 + } 239 + 240 + " 241 + <heading> 242 + let assert Ok(pgo.Returned(1, [])) = sql.<insert>(db, input) 243 + let assert Ok(pgo.Returned(1, [res])) = sql.<select_all>(db) 244 + case expected_output == res.col { 245 + True -> Nil 246 + False -> { 247 + io.println(string.inspect(res.col)) 248 + panic as \" test for <postgres_type> type failed\" 249 + } 250 + } 251 + let assert Ok(pgo.Returned(1, [])) = sql.<delete>(db) 252 + " 253 + |> string.replace(each: "<postgres_type>", with: postgres_type) 254 + |> string.replace(each: "<heading>", with: heading) 255 + |> string.replace(each: "<insert>", with: insert) 256 + |> string.replace(each: "<select_all>", with: select_all) 257 + |> string.replace(each: "<delete>", with: delete) 258 + } 259 + 260 + string.join(assertions, with: "\n") 261 + } 262 + 263 + /// Writes the entry point of the Gleam project that will connect to the 264 + /// database and run all the assertions testing the generated squirrel code. 265 + /// 266 + fn write_main(assertions: String, to dir: String) -> Nil { 267 + let main = " 268 + import gleam/io 269 + import gleam/string 270 + import gleam/pgo 271 + import gleam/json 272 + import youid/uuid 273 + import sql 274 + 275 + pub fn main() { 276 + let config = 277 + pgo.Config( 278 + ..pgo.default_config(), 279 + port: 5432, 280 + user: \"squirrel_test\", 281 + host: \"localhost\", 282 + database: \"squirrel_test\", 283 + ) 284 + let db = pgo.connect(config) 285 + 286 + " <> assertions <> " 287 + }" 288 + 289 + let src_dir = filepath.join(dir, "src") 290 + let assert Ok(_) = 291 + simplifile.write(main, to: filepath.join(src_dir, "main.gleam")) 292 + 293 + Nil 294 + } 295 + 296 + /// First runs the squirrel command to generate code and then the main module of 297 + /// the project to run all the generated assertions. 298 + /// 299 + fn test_project(dir: String) -> Result(String, #(Int, String)) { 300 + let assert Ok(_) = 301 + shellout.command( 302 + run: "gleam", 303 + with: ["run", "-msquirrel"], 304 + in: dir, 305 + opt: [], 306 + ) 307 + 308 + shellout.command(run: "gleam", with: ["run", "-mmain"], in: dir, opt: []) 309 + } 310 + 311 + fn safe_name(string: String) { 312 + let assert Ok(regex) = regex.from_string("[()\\[\\]]") 313 + regex.replace(each: regex, with: "_", in: string) 314 + }
+11 -9
test/squirrel_test.gleam
··· 27 27 28 28 const port = 5432 29 29 30 + pub fn test_config() -> pgo.Config { 31 + pgo.Config( 32 + ..pgo.default_config(), 33 + port: port, 34 + user: user, 35 + host: host, 36 + database: database, 37 + ) 38 + } 39 + 30 40 fn setup_database() { 31 - let config = 32 - pgo.Config( 33 - ..pgo.default_config(), 34 - port: port, 35 - user: user, 36 - host: host, 37 - database: database, 38 - ) 39 - let db = pgo.connect(config) 41 + let db = pgo.connect(test_config()) 40 42 41 43 let assert Ok(_) = 42 44 "