🐿️ Type safe SQL in Gleam
0

Configure Feed

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

add support for the 'name' type

author
Giacomo Cavalieri
date (Nov 15, 2025, 3:39 PM +0100) commit a972aabf parent 93ff0c2c change-id vqsuqrlo
+227 -93
+3
CHANGELOG.md
··· 2 2 3 3 ## Unreleased 4 4 5 + - Added support for the `name` type, represented as a Gleam `String`. 6 + ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) 7 + 5 8 ## 4.5.0 - 2025-10-24 6 9 7 10 - Squirrel will no longer perform code generation if there's any errors in the
+14 -14
README.md
··· 191 191 192 192 The types that are currently supported are: 193 193 194 - | postgres type | encoded as | decoded as | 195 - | ------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | 196 - | `bool` | `Bool` | `Bool` | 197 - | `text`, `char`, `bpchar`, `varchar`, `citext` | `String` | `String` | 198 - | `float4`, `float8`, `numeric` | `Float` | `Float` | 199 - | `int2`, `int4`, `int8` | `Int` | `Int` | 200 - | `json`, `jsonb` | [`json.Json`](https://hexdocs.pm/gleam_json/gleam/json.html#Json) | `String` | 201 - | `uuid` | [`uuid.Uuid`](https://hexdocs.pm/youid/youid/uuid.html#Uuid) | [`uuid.Uuid`](https://hexdocs.pm/youid/youid/uuid.html#Uuid) | 202 - | `bytea` | `BitArray` | `BitArray` | 203 - | `date` | [`calendar.Date`](https://hexdocs.pm/gleam_time/gleam/time/calendar.html#Date) | [`calendar.Date`](https://hexdocs.pm/gleam_time/gleam/time/calendar.html#Date) | 204 - | `time` | [`calendar.TimeOfDay`](https://hexdocs.pm/gleam_time/gleam/time/calendar.html#TimeOfDay) | [`calendar.TimeOfDay`](https://hexdocs.pm/gleam_time/gleam/time/calendar.html#TimeOfDay) | 205 - | `timestamp` | [`timestamp.Timestamp`](https://hexdocs.pm/gleam_time/gleam/time/timestamp.html#Timestamp) | [`timestamp.Timestamp`](https://hexdocs.pm/gleam_time/gleam/time/timestamp.html#Timestamp) | 206 - | `<type>[]` (where `<type>` is any supported type) | `List(<type>)` | `List(<type>)` | 207 - | user-defined enum | [Gleam custom type](https://tour.gleam.run/data-types/custom-types/) | [Gleam custom type](https://tour.gleam.run/data-types/custom-types/) | 194 + | postgres type | encoded as | decoded as | 195 + | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | 196 + | `bool` | `Bool` | `Bool` | 197 + | `text`, `char`, `bpchar`, `varchar`, `citext`, `name` | `String` | `String` | 198 + | `float4`, `float8`, `numeric` | `Float` | `Float` | 199 + | `int2`, `int4`, `int8` | `Int` | `Int` | 200 + | `json`, `jsonb` | [`json.Json`](https://hexdocs.pm/gleam_json/gleam/json.html#Json) | `String` | 201 + | `uuid` | [`uuid.Uuid`](https://hexdocs.pm/youid/youid/uuid.html#Uuid) | [`uuid.Uuid`](https://hexdocs.pm/youid/youid/uuid.html#Uuid) | 202 + | `bytea` | `BitArray` | `BitArray` | 203 + | `date` | [`calendar.Date`](https://hexdocs.pm/gleam_time/gleam/time/calendar.html#Date) | [`calendar.Date`](https://hexdocs.pm/gleam_time/gleam/time/calendar.html#Date) | 204 + | `time` | [`calendar.TimeOfDay`](https://hexdocs.pm/gleam_time/gleam/time/calendar.html#TimeOfDay) | [`calendar.TimeOfDay`](https://hexdocs.pm/gleam_time/gleam/time/calendar.html#TimeOfDay) | 205 + | `timestamp` | [`timestamp.Timestamp`](https://hexdocs.pm/gleam_time/gleam/time/timestamp.html#Timestamp) | [`timestamp.Timestamp`](https://hexdocs.pm/gleam_time/gleam/time/timestamp.html#Timestamp) | 206 + | `<type>[]` (where `<type>` is any supported type) | `List(<type>)` | `List(<type>)` | 207 + | user-defined enum | [Gleam custom type](https://tour.gleam.run/data-types/custom-types/) | [Gleam custom type](https://tour.gleam.run/data-types/custom-types/) | 208 208 209 209 ### Enums 210 210
+28
birdie_snapshots/array_of_names_decoding.accepted
··· 1 + --- 2 + version: 1.4.0 3 + title: array of names decoding 4 + file: ./test/squirrel_test.gleam 5 + test_name: array_of_names_decoding_test 6 + --- 7 + 8 + import gleam/dynamic/decode 9 + import pog 10 + 11 + pub type QueryRow { 12 + QueryRow(res: List(String)) 13 + } 14 + 15 + pub fn query( 16 + db: pog.Connection, 17 + ) -> Result(pog.Returned(QueryRow), pog.QueryError) { 18 + let decoder = { 19 + use res <- decode.field(0, decode.list(decode.string)) 20 + decode.success(QueryRow(res:)) 21 + } 22 + 23 + "select '{}'::name[] as res" 24 + |> pog.query 25 + |> pog.returning(decoder) 26 + |> pog.execute(db) 27 + } 28 +
+30
birdie_snapshots/array_of_names_encoding.accepted
··· 1 + --- 2 + version: 1.4.0 3 + title: array of names encoding 4 + file: ./test/squirrel_test.gleam 5 + test_name: array_of_names_encoding_test 6 + --- 7 + 8 + import gleam/dynamic/decode 9 + import pog 10 + 11 + pub type QueryRow { 12 + QueryRow(res: Int) 13 + } 14 + 15 + pub fn query( 16 + db: pog.Connection, 17 + arg_1: List(String), 18 + ) -> Result(pog.Returned(QueryRow), pog.QueryError) { 19 + let decoder = { 20 + use res <- decode.field(0, decode.int) 21 + decode.success(QueryRow(res:)) 22 + } 23 + 24 + "select 1 as res where $1 = '{}'::name[]" 25 + |> pog.query 26 + |> pog.parameter(pog.array(fn(value) { pog.text(value) }, arg_1)) 27 + |> pog.returning(decoder) 28 + |> pog.execute(db) 29 + } 30 +
+28
birdie_snapshots/name_decoding.accepted
··· 1 + --- 2 + version: 1.4.0 3 + title: name decoding 4 + file: ./test/squirrel_test.gleam 5 + test_name: name_decoding_test 6 + --- 7 + 8 + import gleam/dynamic/decode 9 + import pog 10 + 11 + pub type QueryRow { 12 + QueryRow(res: String) 13 + } 14 + 15 + pub fn query( 16 + db: pog.Connection, 17 + ) -> Result(pog.Returned(QueryRow), pog.QueryError) { 18 + let decoder = { 19 + use res <- decode.field(0, decode.string) 20 + decode.success(QueryRow(res:)) 21 + } 22 + 23 + "select 'name'::name as res" 24 + |> pog.query 25 + |> pog.returning(decoder) 26 + |> pog.execute(db) 27 + } 28 +
+30
birdie_snapshots/name_encoding.accepted
··· 1 + --- 2 + version: 1.4.0 3 + title: name encoding 4 + file: ./test/squirrel_test.gleam 5 + test_name: name_encoding_test 6 + --- 7 + 8 + import gleam/dynamic/decode 9 + import pog 10 + 11 + pub type QueryRow { 12 + QueryRow(res: Int) 13 + } 14 + 15 + pub fn query( 16 + db: pog.Connection, 17 + arg_1: String, 18 + ) -> Result(pog.Returned(QueryRow), pog.QueryError) { 19 + let decoder = { 20 + use res <- decode.field(0, decode.int) 21 + decode.success(QueryRow(res:)) 22 + } 23 + 24 + "select 1 as res where $1 = 'name'::name" 25 + |> pog.query 26 + |> pog.parameter(pog.text(arg_1)) 27 + |> pog.returning(decoder) 28 + |> pog.execute(db) 29 + } 30 +
+63 -73
src/squirrel/internal/database/postgres.gleam
··· 47 47 name:, 48 48 comment: [], 49 49 content: " 50 + with recursive types as ( 51 + -- This selects the initial type, it might be an array! 52 + select 53 + pg_type.oid as oid, 54 + pg_type.typname as name, 55 + pg_type.typelem as elem, 56 + pg_type.typtype as kind, 57 + 0 as jumps 58 + from pg_type 59 + where pg_type.oid = $1 60 + union all 61 + -- So we keep selecting the type contained in it recursively until we 62 + -- reach a base type (where the `elem` field is 0 and can't be joined with 63 + -- any other type). 64 + select 65 + pg_type.oid as oid, 66 + pg_type.typname as name, 67 + pg_type.typelem as elem, 68 + pg_type.typtype as kind, 69 + types.jumps + 1 as jumps 70 + from pg_type 71 + join types 72 + on pg_type.oid = types.elem 73 + -- We need to special case the built-in `name` type: for some reason 74 + -- that's treated as a char array, so we have to stop the recursion 75 + -- earlier 76 + and types.name != 'name' 77 + ) 78 + -- Finally we only get the last base type (keeping track of how many jumps we 79 + -- did to properly wrap it in an array type the correct number of times) 50 80 select 51 - -- The name of the type or, if the type is an array, the name of its 52 - -- elements' type. 53 - case 54 - when elem.typname is null then type.typname 55 - else elem.typname 56 - end as type, 57 - 58 - -- The oid of the type or the array item type. 59 - case 60 - when elem.typname is null then type.oid 61 - else elem.oid 62 - end as oid, 63 - 64 - -- Tells us how to interpret the first column: if this is true then the first 65 - -- column is the type of the elements of the array type. 66 - -- Otherwise it means we've found a base type. 67 - case 68 - when elem.typname is null then false 69 - else true 70 - end as is_array, 71 - 72 - -- The type of the type/array item. 73 - -- It will be 'e' if the thing is an enum. 74 - case 75 - when elem.typname is null then type.typtype 76 - else elem.typtype 77 - end as kind 78 - from 79 - pg_type as type 80 - left join pg_type as elem on type.typelem = elem.oid 81 - where 82 - type.oid = $1 81 + types.oid, 82 + types.name, 83 + types.kind, 84 + types.jumps 85 + from types 86 + order by types.jumps desc 87 + limit 1 83 88 ", 84 89 ) 85 90 } ··· 93 98 name:, 94 99 comment: [], 95 100 content: " 96 - select 97 - enumlabel 98 - from 99 - pg_enum 100 - where 101 - enumtypid = $1 102 - order by 103 - enumsortorder asc 101 + select enumlabel 102 + from pg_enum 103 + where enumtypid = $1 104 + order by enumsortorder asc 104 105 ", 105 106 ) 106 107 } ··· 117 118 select 118 119 -- Whether the column has a not-null constraint. 119 120 attnotnull 120 - from 121 - pg_attribute 121 + from pg_attribute 122 122 where 123 123 -- The oid of the table the column comes from. 124 124 attrelid = $1 ··· 152 152 /// 153 153 PBase(name: String) 154 154 155 - /// An array type like `int[]`, `text[]`, ... 156 - /// 157 - PArray(inner: PgType) 158 - 159 155 /// An enum, for example: 160 156 /// 161 157 /// ```sql ··· 168 164 /// 169 165 /// 170 166 PEnum(name: String, variants: List(String)) 171 - 172 - /// A type that could also be `NULL`, this is particularly common for columns 173 - /// that do not have a `not null` constraint; or for those coming from partial 174 - /// joins. 175 - /// 176 - POption(inner: PgType) 177 167 } 178 168 179 169 /// The context in which all database-related actions will take place. ··· 261 251 fn pg_to_gleam_type( 262 252 query: UntypedQuery, 263 253 type_: PgType, 254 + // How many times the base type needs to be wrapped in a list type. For 255 + // example if we get 2, that means we're dealing with an array of arrays in 256 + // postgres (like `text[][]`). 257 + list_wrappings: Int, 264 258 ) -> Result(gleam.Type, Error) { 265 259 case type_ { 266 - PArray(inner:) -> 267 - pg_to_gleam_type(query, inner) 268 - |> result.map(gleam.List) 269 - 270 - POption(inner:) -> 271 - pg_to_gleam_type(query, inner) 272 - |> result.map(gleam.Option) 273 - 274 260 PBase(name:) -> 275 261 case name { 276 262 "bool" -> Ok(gleam.Bool) 277 - "text" | "char" | "bpchar" | "varchar" | "citext" -> Ok(gleam.String) 263 + "text" | "char" | "bpchar" | "varchar" | "citext" | "name" -> 264 + Ok(gleam.String) 278 265 "float4" | "float8" -> Ok(gleam.Float) 279 266 "numeric" -> Ok(gleam.Numeric) 280 267 "int2" | "int4" | "int8" -> Ok(gleam.Int) ··· 290 277 PEnum(name:, variants:) -> 291 278 gleam.try_make_enum(name, variants) 292 279 |> result.map_error(invalid_enum_error(query, name, _)) 280 + } 281 + |> result.map(wrap_in_list(_, list_wrappings)) 282 + } 283 + 284 + /// Wraps a Gleam type in the list type the given number of times. 285 + /// For example `wrap_in_list(String, 2)` will become `List(List(String))`. 286 + /// 287 + fn wrap_in_list(value: gleam.Type, times: Int) -> gleam.Type { 288 + case times <= 0 { 289 + True -> value 290 + _ -> wrap_in_list(gleam.List(value), times - 1) 293 291 } 294 292 } 295 293 ··· 731 729 // check wether it is an array or not and the type of the type / array item. 732 730 // It's safe to assert because this query is hard coded in our code and the 733 731 // output shape cannot change without us changing that query. 734 - let assert [[name, oid, is_array, kind]] = res 732 + let assert [[oid, name, kind, list_wrappings]] = res 733 + let assert <<oid:size(32)>> = oid 735 734 let assert Ok(name) = bit_array.to_string(name) 736 735 let assert Ok(kind) = bit_array.to_string(kind) 737 - let assert <<oid:size(32)>> = oid 736 + let assert <<list_wrappings:size(32)>> = list_wrappings 738 737 739 738 use type_ <- eval.try(case kind { 740 739 "e" -> resolve_enum_type(name, oid) 741 740 _ -> eval.return(PBase(name)) 742 741 }) 743 742 744 - // We then decode the bitarrays we got as a result: 745 - // - `name` is just a string 746 - // - `is_array` is a pg boolean 747 - // 748 - let type_ = case bit_array_to_bool(is_array) { 749 - True -> PArray(type_) 750 - False -> type_ 751 - } 752 - 753 - pg_to_gleam_type(query, type_) 743 + pg_to_gleam_type(query, type_, list_wrappings) 754 744 |> eval.from_result 755 745 } 756 746
+6 -6
test/integration_test.gleam
··· 61 61 // Booleans 62 62 TestType("bool", [TestValue("True"), TestValue("False")]), 63 63 // Text data 64 + TestType("name", [TestValue("\"hello\"")]), 64 65 TestType("text", [TestValue("\"hello\"")]), 65 66 TestType("char(1)", [TestValue("\"j\"")]), 66 67 TestType("bpchar", [TestValue("\"j\"")]), ··· 360 361 } 361 362 362 363 fn safe_name(string: String) -> String { 363 - let assert Ok(regex) = regexp.from_string("[()\\[\\]]") 364 + let assert Ok(parens) = regexp.from_string("[()]") 365 + let assert Ok(array) = regexp.from_string("\\[\\]") 364 366 365 - let safe_string = regexp.replace(each: regex, with: "_", in: string) 366 - case string.ends_with(string, "[]") { 367 - False -> safe_string 368 - True -> safe_string <> "array" 369 - } 367 + string 368 + |> regexp.replace(each: parens, with: "_") 369 + |> regexp.replace(each: array, with: "array") 370 370 }
+25
test/squirrel_test.gleam
··· 578 578 |> birdie.snap(title: "enum array decoding") 579 579 } 580 580 581 + // https://github.com/giacomocavalieri/squirrel/issues/119 582 + pub fn name_encoding_test() { 583 + "select 1 as res where $1 = 'name'::name" 584 + |> should_codegen 585 + |> birdie.snap(title: "name encoding") 586 + } 587 + 588 + pub fn name_decoding_test() { 589 + "select 'name'::name as res" 590 + |> should_codegen 591 + |> birdie.snap(title: "name decoding") 592 + } 593 + 594 + pub fn array_of_names_encoding_test() { 595 + "select 1 as res where $1 = '{}'::name[]" 596 + |> should_codegen 597 + |> birdie.snap(title: "array of names encoding") 598 + } 599 + 600 + pub fn array_of_names_decoding_test() { 601 + "select '{}'::name[] as res" 602 + |> should_codegen 603 + |> birdie.snap(title: "array of names decoding") 604 + } 605 + 581 606 // --- CODEGEN STRUCTURE TESTS ------------------------------------------------- 582 607 // This is a group of tests to ensure the generated code has some specific 583 608 // structure (e.g. the names and comments are what we expect...)