diff --git a/README.md b/README.md index 9aeffcf..97e439a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Interface with PostgreSQL databases from Roc. -This package implements a PostgreSQL client on pure Roc that depends only on a TCP effect from the platform. +This package implements a PostgreSQL client on pure Roc that depends only on a TCP effect from the platform. It exposes a simple API that allows you to run SQL commands as strings and a query builder that helps you write composable type-safe queries against your schema. @@ -27,24 +27,26 @@ The plan is to support all the other SQL commands, but that's coming later. In t Connecting and performing a query ```haskell -task : Task (List { name: Str, price: Dec }) _ -task = - client <- Pg.Client.withConnect { - host: "localhost", - port: 5432, - user: "postgres", - database: "postgres", - auth: Password "password" - } - - Pg.Cmd.new "select name, price from products" - |> Pg.Cmd.expectN ( - Pg.Result.succeed { - name: <- Pg.Result.str "name" |> Pg.Result.apply, - price: <- Pg.Result.dec "price" |> Pg.Result.apply - } - ) - |> Pg.Client.command client +products! : {} => Result (List { name : Str, price : Dec }) _ +products! = |_| + client = Pg.Client.connect!( + { + host: "localhost", + port: 5432, + user: "postgres", + auth: None, + database: "postgres", + }, + )? + + Pg.Cmd.new("select name, price from products") + |> Pg.Cmd.expect_n( + { Pg.Result.combine <- + name: Pg.Result.str("name"), + price: Pg.Result.dec("price"), + }, + ) + |> Pg.Client.command!(client) ```
@@ -53,15 +55,15 @@ Parameterized queries ```elm -Pg.Cmd.new "select name, price from products where id = $1" -|> Pg.Cmd.bind [ Pg.Cmd.u32 productId ] -|> Pg.Cmd.expect1 ( - Pg.Result.succeed { - name: <- Pg.Result.str "name" |> Pg.Result.apply, - price: <- Pg.Result.dec "price" |> Pg.Result.apply - } -) -|> Pg.Client.command client +Pg.Cmd.new("select name, price from products where id = $1") +|> Pg.Cmd.bind([ Pg.Cmd.u32(product_id) ]) +|> Pg.Cmd.expect1( + { Pg.Result.combine <- + name: Pg.Result.str("name"), + price: Pg.Result.dec("price"), + }, +) +|> Pg.Client.command!(client)? ```
@@ -72,16 +74,14 @@ Prepared statements ```elm -selectUser <- +select_user = "select email from users where id = $1" - |> Pg.Client.prepare { client, name: "selectUser" } - |> await - -selectUser -|> Pg.Cmd.bind [ Pg.Cmd.u32 userId ] -|> Pg.Cmd.expect1 (Pg.Result.str "email") -|> Pg.Client.command client + |> Pg.Client.prepare!({ client, name: "select_user" })? +select_user +|> Pg.Cmd.bind([Pg.Cmd.u32(user_id)]) +|> Pg.Cmd.expect1(Pg.Result.str("email")) +|> Pg.Client.command!(client)? ``` @@ -92,33 +92,36 @@ Batch commands in a single roundtrip (applicative) ```elm -Pg.Batch.succeed \email -> \products -> { email, products } -|> Pg.Batch.with +Pg.Batch.succeed(|email| |products| { email, products }) +|> Pg.Batch.with( ( - selectUser - |> Pg.Cmd.bind [ Pg.Cmd.u32 userId ] - |> Pg.Cmd.expect1 (Pg.Result.str "email") - ) -|> Pg.Batch.with + select_user + |> Pg.Cmd.bind([Pg.Cmd.u32(user_id)]) + |> Pg.Cmd.expect1(Pg.Result.str("email")) + ), +) +|> Pg.Batch.with( ( - Pg.Cmd.new + Pg.Cmd.new( """ select name, price from products inner join orders on orders.product_id = products.id where orders.id = $1 - """ - |> Pg.Cmd.bind [ Pg.Cmd.u32 orderId ] - |> Pg.Cmd.expectN ( - Pg.Result.succeed { - name: <- Pg.Result.str "name" |> Pg.Result.apply, - price: <- Pg.Result.dec "price" |> Pg.Result.apply - } - ) - ) -|> Pg.Client.batch client + """, + ) + |> Pg.Cmd.bind([Pg.Cmd.u32(order_id)]) + |> Pg.Cmd.expect_n( + { Pg.Result.combine <- + name: Pg.Result.str("name"), + price: Pg.Result.dec("price"), + }, + ) + ), +) +|> Pg.Client.batch!(client)? ``` -Note: `selectUser` referes to prepared statement in the previous example +Note: `select_user` referes to prepared statement in the previous example @@ -128,14 +131,14 @@ Batch commands in a single roundtrip (list) ```elm -updateCmd = \product -> - Pg.Cmd.new "update products set desc = $1 where id = $2" - |> Pg.Cmd.bind [ Pg.Cmd.str product.desc, Pg.Cmd.u32 product.id ] +update_cmd = |product| + Pg.Cmd.new("update products set desc = $1 where id = $2") + |> Pg.Cmd.bind([Pg.Cmd.str(product.desc), Pg.Cmd.u32(product.id)]) -productsToUpdate -|> List.map updateCmd +products_to_update +|> List.map(update_cmd) |> Pg.Batch.sequence -|> Pg.Client.batch client +|> Pg.Client.batch!(client)? ``` Note: `roc-pg` automatically reuses statements in a batch by only parsing (and describing) once per unique SQL string. This also works with applicative batches. diff --git a/examples/basic-webserver.roc b/examples/basic-webserver.roc new file mode 100644 index 0000000..33c167a --- /dev/null +++ b/examples/basic-webserver.roc @@ -0,0 +1,63 @@ +app [Model, init!, respond!] { + pf: platform "https://github.com/roc-lang/basic-webserver/releases/download/0.12.0/Q4h_In-sz1BqAvlpmCsBHhEJnn_YvfRRMiNACB_fBbk.tar.br", + pg: "../src/main.roc", +} + +import pf.Stdout +import pg.Pg.Client exposing [Client] +import pg.Pg.Cmd +import pg.Pg.Result + +Model : { + client : Client, +} + +init! = |_| + _ = Stdout.line!("Start!") + + client = Pg.Client.connect!( + { + host: "localhost", + port: 5432, + user: "postgres", + auth: None, + database: "postgres", + }, + )? + + _ = Stdout.line!("Connected!") + + Ok({ client }) + +respond! = |_request, model| + rows = + Pg.Cmd.new( + """ + select $1 as name, $2 as age + union all + select 'Julio' as name, 23 as age + """, + ) + |> Pg.Cmd.bind([Pg.Cmd.str("John"), Pg.Cmd.u8(32)]) + |> Pg.Cmd.expect_n( + { Pg.Result.combine <- + name: Pg.Result.str("name"), + age: Pg.Result.u8("age"), + }, + ) + |> Pg.Client.command!(model.client)? + + response_body = + rows + |> List.map(Inspect.to_str) + |> Str.join_with("\n") + + Stdout.line!("Responding with: ${response_body}")? + + Ok( + { + status: 200, + headers: [], + body: Str.to_utf8(response_body), + }, + ) diff --git a/examples/batch.roc b/examples/batch.roc index 11f8c5c..f1ef18e 100644 --- a/examples/batch.roc +++ b/examples/batch.roc @@ -1,58 +1,65 @@ -app [main] { +app [main!] { + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br", pg: "../src/main.roc", - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br", } import pf.Stdout import pg.Pg.Cmd -import pg.Pg.BasicCliClient +import pg.Pg.Client import pg.Pg.Batch import pg.Pg.Result -main = - client = Pg.BasicCliClient.connect! { - host: "localhost", - port: 5432, - user: "postgres", - auth: None, - database: "postgres", - } - - Stdout.line! "Connected!" - - resultWith = - Pg.Batch.succeed (\hi -> \eleven -> \thirtyOne -> { hi, fortyTwo: eleven + thirtyOne }) - |> Pg.Batch.with - ( - Pg.Cmd.new "select 'hi' as value" - |> Pg.Cmd.expect1 (Pg.Result.str "value") - ) - |> Pg.Batch.with - ( - Pg.Cmd.new "select $1::int as value" - |> Pg.Cmd.bind [Pg.Cmd.u8 11] - |> Pg.Cmd.expect1 (Pg.Result.u8 "value") - ) - |> Pg.Batch.with - ( - Pg.Cmd.new "select $1::int as value" - |> Pg.Cmd.bind [Pg.Cmd.u8 31] - |> Pg.Cmd.expect1 (Pg.Result.u8 "value") - ) - |> Pg.BasicCliClient.batch! client - - str42 = Num.toStr resultWith.fortyTwo - Stdout.line! "$(resultWith.hi) $(str42)" - - resultSeq = - List.range { start: At 0, end: At 20 } - |> List.map \num -> - Pg.Cmd.new "select $1::int as value" - |> Pg.Cmd.bind [Pg.Cmd.u8 num] - |> Pg.Cmd.expect1 (Pg.Result.u8 "value") - |> Pg.Batch.sequence - |> Pg.BasicCliClient.batch! client - - resultSeqStr = resultSeq |> List.map Num.toStr |> Str.joinWith ", " - - Stdout.line resultSeqStr +main! = |_| + client = Pg.Client.connect!( + { + host: "localhost", + port: 5432, + user: "postgres", + auth: None, + database: "postgres", + }, + )? + + _ = Stdout.line!("Connected!") + + result_with = + Pg.Batch.succeed(|hi| |eleven| |thirty_one| { hi, forty_two: eleven + thirty_one }) + |> Pg.Batch.with( + ( + Pg.Cmd.new("select 'hi' as value") + |> Pg.Cmd.expect1(Pg.Result.str("value")) + ), + ) + |> Pg.Batch.with( + ( + Pg.Cmd.new("select $1::int as value") + |> Pg.Cmd.bind([Pg.Cmd.u8(11)]) + |> Pg.Cmd.expect1(Pg.Result.u8("value")) + ), + ) + |> Pg.Batch.with( + ( + Pg.Cmd.new("select $1::int as value") + |> Pg.Cmd.bind([Pg.Cmd.u8(31)]) + |> Pg.Cmd.expect1(Pg.Result.u8("value")) + ), + ) + |> Pg.Client.batch!(client)? + + str42 = Num.to_str(result_with.forty_two) + _ = Stdout.line!("${result_with.hi} ${str42}") + + result_seq = + List.range({ start: At(0), end: At(20) }) + |> List.map( + |num| + Pg.Cmd.new("select $1::int as value") + |> Pg.Cmd.bind([Pg.Cmd.u8(num)]) + |> Pg.Cmd.expect1(Pg.Result.u8("value")), + ) + |> Pg.Batch.sequence + |> Pg.Client.batch!(client)? + + result_seq_str = result_seq |> List.map(Num.to_str) |> Str.join_with(", ") + + Stdout.line!(result_seq_str) diff --git a/examples/prepared.roc b/examples/prepared.roc index 00e5eaf..21771e4 100644 --- a/examples/prepared.roc +++ b/examples/prepared.roc @@ -1,40 +1,42 @@ -app [main] { +app [main!] { + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br", pg: "../src/main.roc", - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br", } import pf.Stdout import pg.Pg.Cmd -import pg.Pg.BasicCliClient +import pg.Pg.Client import pg.Pg.Result -main = - client = Pg.BasicCliClient.connect! { - host: "localhost", - port: 5432, - user: "postgres", - auth: None, - database: "postgres", - } +main! = |_| + client = Pg.Client.connect!( + { + host: "localhost", + port: 5432, + user: "postgres", + auth: None, + database: "postgres", + }, + )? - Stdout.line! "Connected!" + _ = Stdout.line!("Connected!") - addCmd = + add_cmd = "select $1::int + $2::int as result" - |> Pg.BasicCliClient.prepare! { client, name: "add" } + |> Pg.Client.prepare!({ client, name: "add" })? - addAndPrint = \a, b -> + add_and_print! = |a, b| result = - addCmd - |> Pg.Cmd.bind [Pg.Cmd.u8 a, Pg.Cmd.u8 b] - |> Pg.Cmd.expect1 (Pg.Result.u8 "result") - |> Pg.BasicCliClient.command! client + add_cmd + |> Pg.Cmd.bind([Pg.Cmd.u8(a), Pg.Cmd.u8(b)]) + |> Pg.Cmd.expect1(Pg.Result.u8("result")) + |> Pg.Client.command!(client)? - aStr = Num.toStr a - bStr = Num.toStr b - resultStr = Num.toStr result + a_str = Num.to_str(a) + b_str = Num.to_str(b) + result_str = Num.to_str(result) - Stdout.line "$(aStr) + $(bStr) = $(resultStr)" + Stdout.line!("${a_str} + ${b_str} = ${result_str}") - addAndPrint! 1 2 - addAndPrint! 11 31 + _ = add_and_print!(1, 2) + add_and_print!(11, 31) diff --git a/examples/query.roc b/examples/query.roc index 78f2189..17acc93 100644 --- a/examples/query.roc +++ b/examples/query.roc @@ -1,43 +1,46 @@ -app [main] { +app [main!] { + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br", pg: "../src/main.roc", - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br", } import pf.Stdout import pg.Pg.Cmd -import pg.Pg.BasicCliClient +import pg.Pg.Client import pg.Pg.Result -main = - client = Pg.BasicCliClient.connect! { - host: "localhost", - port: 5432, - user: "postgres", - auth: None, - database: "postgres", - } +main! = |_| + client = Pg.Client.connect!( + { + host: "localhost", + port: 5432, + user: "postgres", + auth: None, + database: "postgres", + }, + )? - Stdout.line! "Connected!" + _ = Stdout.line!("Connected!") rows = - Pg.Cmd.new + Pg.Cmd.new( """ select $1 as name, $2 as age union all select 'Julio' as name, 23 as age - """ - |> Pg.Cmd.bind [Pg.Cmd.str "John", Pg.Cmd.u8 32] - |> Pg.Cmd.expectN - ( - Pg.Result.succeed - (\name -> \age -> - ageStr = Num.toStr age + """, + ) + |> Pg.Cmd.bind([Pg.Cmd.str("John"), Pg.Cmd.u8(32)]) + |> Pg.Cmd.expect_n( + { Pg.Result.combine <- + name: Pg.Result.str("name"), + age: Pg.Result.u8("age"), + }, + ) + |> Pg.Client.command!(client)? - "$(name): $(ageStr)" - ) - |> Pg.Result.with (Pg.Result.str "name") - |> Pg.Result.with (Pg.Result.u8 "age") - ) - |> Pg.BasicCliClient.command! client + str = + rows + |> List.map(Inspect.to_str) + |> Str.join_with("\n") - Stdout.line (Str.joinWith rows "\n") + Stdout.line!("Got records:\n${str}") diff --git a/sql-cli/src/Generate.roc b/sql-cli/src/Generate.roc index ba43b6a..5d35aca 100644 --- a/sql-cli/src/Generate.roc +++ b/sql-cli/src/Generate.roc @@ -4,178 +4,178 @@ import Name import Schema exposing [Schema] module : Schema -> Str -module = \schema -> - moduleName = Name.module (Schema.getName schema) - tables = Schema.getTables schema - - tableDefs : List { name : Str, def : Str } - tableDefs = - List.map tables \table -> tableDef table schema - - rocExposes : Str - rocExposes = - tableDefs - |> List.map \{ name } -> indent "$(name)," 2 - |> Str.joinWith "\n" - - rocDefs : Str - rocDefs = - tableDefs - |> List.map .def - |> Str.joinWith "\n\n" +module = |schema| + module_name = Name.module(Schema.get_name(schema)) + tables = Schema.get_tables(schema) + + table_defs : List { name : Str, def : Str } + table_defs = + List.map(tables, |table| table_def(table, schema)) + + roc_exposes : Str + roc_exposes = + table_defs + |> List.map(|{ name }| indent("${name},", 2)) + |> Str.join_with("\n") + + roc_defs : Str + roc_defs = + table_defs + |> List.map(.def) + |> Str.join_with("\n\n") """ # This file was automatically generated by roc-sql - interface $(moduleName) + interface ${module_name} exposes [ - $(rocExposes) + ${roc_exposes} ] imports [ pg.Sql.{ identifier }, pg.Sql.Types, ] - $(rocDefs) + ${roc_defs} """ -tableDef : Schema.Table, Schema -> { name : Str, def : Str } -tableDef = \table, schema -> - defName : Str - defName = Name.identifier table.name +table_def : Schema.Table, Schema -> { name : Str, def : Str } +table_def = |table, schema| + def_name : Str + def_name = Name.identifier(table.name) - columnDefs : List { field : Str, primaryDef : Result Str [None] } - columnDefs = - List.map table.columns \column -> columnDef column table.id schema + column_defs : List { field : Str, primary_def : Result Str [None] } + column_defs = + List.map(table.columns, |column| column_def(column, table.id, schema)) - primaryColumns : Str - primaryColumns = - columnDefs - |> List.keepOks .primaryDef - |> Str.joinWith "\n\n" + primary_columns : Str + primary_columns = + column_defs + |> List.keep_oks(.primary_def) + |> Str.join_with("\n\n") - columnsFields : Str - columnsFields = - columnDefs - |> List.map .field - |> Str.joinWith "\n" + columns_fields : Str + columns_fields = + column_defs + |> List.map(.field) + |> Str.join_with("\n") def : Str def = """ - $(primaryColumns) + ${primary_columns} - $(defName) = { - schema: $(Name.sqlName (Schema.getName schema)), - name: $(Name.sqlName table.name), - alias: "$(Name.tableAlias table.name)", + ${def_name} = { + schema: ${Name.sql_name(Schema.get_name(schema))}, + name: ${Name.sql_name(table.name)}, + alias: "${Name.table_alias(table.name)}", columns: \\alias -> { - $(columnsFields) + ${columns_fields} }, } """ - { name: defName, def } + { name: def_name, def } -columnDef : Schema.Column, I32, Schema -> { field : Str, primaryDef : Result Str [None] } -columnDef = \column, tableId, schema -> - fieldName = Name.identifier column.name +column_def : Schema.Column, I32, Schema -> { field : Str, primary_def : Result Str [None] } +column_def = |column, table_id, schema| + field_name = Name.identifier(column.name) - columnId = (tableId, column.num) + column_id = (table_id, column.num) - { primaryDef, type } = - when Schema.primaryColumn schema columnId is - Ok pcol -> - qualifiedName = "$(pcol.tableName)_$(pcol.columnName)" - defName = Name.identifier qualifiedName + { primary_def, type } = + when Schema.primary_column(schema, column_id) is + Ok(pcol) -> + qualified_name = "${pcol.table_name}_${pcol.column_name}" + def_name = Name.identifier(qualified_name) pdef = - if pcol.id == columnId then - (expr, pg, roc) = columnType column + if pcol.id == column_id then + (expr, pg, roc) = column_type(column) - pgWithKey = + pg_with_key = # TODO: make this more elegant - Str.replaceFirst pg "*" "{ $(defName): {} }" + Str.replace_first(pg, "*", "{ ${def_name}: {} }") """ - $(defName) : Sql.Types.Decode $(pgWithKey) $(roc) - $(defName) = $(expr) + ${def_name} : Sql.Types.Decode ${pg_with_key} ${roc} + ${def_name} = ${expr} """ |> Ok else - Err None + Err(None) - { primaryDef: pdef, type: defName } + { primary_def: pdef, type: def_name } - Err KeyNotFound -> - { primaryDef: Err None, type: (columnType column).0 } + Err(KeyNotFound) -> + { primary_def: Err(None), type: (column_type(column)).0 } - decoderExpr = - if column.isNullable then - asNullable type + decoder_expr = + if column.is_nullable then + as_nullable(type) else type - columnName = Name.sqlName column.name + column_name = Name.sql_name(column.name) field = - "$(fieldName): identifier alias $(columnName) $(decoderExpr)," - |> indent 2 + "${field_name}: identifier alias ${column_name} ${decoder_expr}," + |> indent(2) - { field, primaryDef } + { field, primary_def } -columnType : Schema.Column -> (Str, Str, Str) -columnType = \column -> - when (column.typeCategory, column.elemDataType) is - ("A", NotNull elemType) -> - (expr, pg, roc) = pgType elemType +column_type : Schema.Column -> (Str, Str, Str) +column_type = |column| + when (column.type_category, column.elem_data_type) is + ("A", NotNull(elem_type)) -> + (expr, pg, roc) = pg_type(elem_type) ( - "(Sql.Types.array $(asNullable expr))", - "(Sql.Types.PgArray ($(pg)) *)", - "(List $(roc))", + "(Sql.Types.array ${as_nullable(expr)})", + "(Sql.Types.PgArray (${pg}) *)", + "(List ${roc})", ) _ -> - pgType column.dataType + pg_type(column.data_type) -pgType : Str -> (Str, Str, Str) -pgType = \sqlType -> - when unqualifiedPgType sqlType is - Ok (expr, pg, roc) -> - ("Sql.Types.$(expr)", "(Sql.Types.$(pg) *)", roc) +pg_type : Str -> (Str, Str, Str) +pg_type = |sql_type| + when unqualified_pg_type(sql_type) is + Ok((expr, pg, roc)) -> + ("Sql.Types.${expr}", "(Sql.Types.${pg} *)", roc) - Err Unsupported -> - ("(Sql.Types.unsupported \"$(sqlType)\")", "*", "Sql.Types.Raw") + Err(Unsupported) -> + ("(Sql.Types.unsupported \"${sql_type}\")", "*", "Sql.Types.Raw") -unqualifiedPgType : Str -> Result (Str, Str, Str) [Unsupported] -unqualifiedPgType = \sqlType -> - when sqlType is +unqualified_pg_type : Str -> Result (Str, Str, Str) [Unsupported] +unqualified_pg_type = |sql_type| + when sql_type is "int2" -> - Ok ("i16", "PgI16", "I16") + Ok(("i16", "PgI16", "I16")) "int4" | "oid" | "xid" -> - Ok ("i32", "PgI32", "I32") + Ok(("i32", "PgI32", "I32")) "int8" -> - Ok ("i64", "PgI64", "I64") + Ok(("i64", "PgI64", "I64")) "float4" -> - Ok ("f32", "PgF32", "F32") + Ok(("f32", "PgF32", "F32")) "float8" -> - Ok ("f64", "PgF64", "F64") + Ok(("f64", "PgF64", "F64")) "numeric" -> - Ok ("dec", "PgDec", "Dec") + Ok(("dec", "PgDec", "Dec")) "text" | "char" | "name" | "bpchar" | "varchar" -> - Ok ("str", "PgText", "Str") + Ok(("str", "PgText", "Str")) "bool" -> - Ok ("bool", "PgBool", "Bool") + Ok(("bool", "PgBool", "Bool")) "uuid" -> - Ok ("uuid", "PgUuid", "Str") + Ok(("uuid", "PgUuid", "Str")) _ -> # TODO: @@ -189,12 +189,12 @@ unqualifiedPgType = \sqlType -> # - text search # - json # - composite - Err Unsupported + Err(Unsupported) -asNullable : Str -> Str -asNullable = \inner -> "(Sql.Types.nullable $(inner))" +as_nullable : Str -> Str +as_nullable = |inner| "(Sql.Types.nullable ${inner})" indent : Str, U64 -> Str -indent = \line, count -> - spaces = Str.repeat " " count - "$(spaces)$(line)" +indent = |line, count| + spaces = Str.repeat(" ", count) + "${spaces}${line}" diff --git a/sql-cli/src/Keyword.roc b/sql-cli/src/Keyword.roc index aa9d3c2..7ccb4a9 100644 --- a/sql-cli/src/Keyword.roc +++ b/sql-cli/src/Keyword.roc @@ -1,11 +1,11 @@ module [ - isReserved, + is_reserved, ] # Reserved Keywords: https://www.postgresql.org/docs/current/sql-keywords-appendix.html -isReserved : List U8 -> Bool -isReserved = \value -> +is_reserved : List U8 -> Bool +is_reserved = |value| when value is ['a', 'l', 'l'] | ['a', 'n', 'a', 'l', 'y', 's', 'e'] diff --git a/sql-cli/src/Name.roc b/sql-cli/src/Name.roc index 07e5a78..7a898ff 100644 --- a/sql-cli/src/Name.roc +++ b/sql-cli/src/Name.roc @@ -2,8 +2,8 @@ module [ module, type, identifier, - tableAlias, - sqlName, + table_alias, + sql_name, ] import Keyword @@ -11,63 +11,67 @@ import Keyword # Roc module : Str -> Str -module = to upperCamelCase +module = to(upper_camel_case) type : Str -> Str -type = to upperCamelCase +type = to(upper_camel_case) identifier : Str -> Str -identifier = to lowerCamelCase +identifier = to(lower_camel_case) # SQL -tableAlias : Str -> Str -tableAlias = - wordList <- to - - List.keepOks wordList \word -> - when word is - [initial, ..] -> - Ok initial - - _ -> - Err {} - -expect tableAlias "Product" == "p" -expect tableAlias "product_users" == "pu" -expect tableAlias "order_Products" == "op" - -sqlName : Str -> Str -sqlName = \value -> - if isValidSqlName value then - quote value +table_alias : Str -> Str +table_alias = + to( + |word_list| + List.keep_oks( + word_list, + |word| + when word is + [initial, ..] -> + Ok(initial) + + _ -> + Err({}), + ), + ) + +expect table_alias("Product") == "p" +expect table_alias("product_users") == "pu" +expect table_alias("order_Products") == "op" + +sql_name : Str -> Str +sql_name = |value| + if is_valid_sql_name(value) then + quote(value) else value - |> sqlQuote + |> sql_quote |> quote quote : Str -> Str -quote = \value -> - "\"$(value)\"" +quote = |value| + "\"${value}\"" -sqlQuote : Str -> Str -sqlQuote = \value -> +sql_quote : Str -> Str +sql_quote = |value| doubled = value - |> Str.replaceEach "\"" "\\\"\\\"" - - "\\\"$(doubled)\\\"" - -expect sqlName "products" == "\"products\"" -expect sqlName "product_orders" == "\"product_orders\"" -expect sqlName "p2" == "\"p2\"" -expect sqlName "2p" == "\"\\\"2p\\\"\"" -expect sqlName "productOrders" == "\"\\\"productOrders\\\"\"" -expect sqlName "where" == "\"\\\"where\\\"\"" -expect sqlName "users_where" == "\"users_where\"" -expect sqlName "class-name" == "\"\\\"class-name\\\"\"" + |> Str.replace_each("\"", "\\\"\\\"") + + "\\\"${doubled}\\\"" + +expect sql_name("products") == "\"products\"" +expect sql_name("product_orders") == "\"product_orders\"" +expect sql_name("p2") == "\"p2\"" +expect sql_name("2p") == "\"\\\"2p\\\"\"" +expect sql_name("productOrders") == "\"\\\"productOrders\\\"\"" +expect sql_name("where") == "\"\\\"where\\\"\"" +expect sql_name("users_where") == "\"users_where\"" +expect sql_name("class-name") == "\"\\\"class-name\\\"\"" expect - generated = sqlName "User \"Bond\"" + generated = sql_name("User \"Bond\"") expected = """ @@ -75,185 +79,196 @@ expect """ generated == expected -isValidSqlName : Str -> Bool -isValidSqlName = \value -> - bytes = Str.toUtf8 value +is_valid_sql_name : Str -> Bool +is_valid_sql_name = |value| + bytes = Str.to_utf8(value) when bytes is [] -> Bool.false [first, ..] -> - (isLowerAlpha first || first == '_') - && ( + (is_lower_alpha(first) or first == '_') + and ( bytes - |> List.dropFirst 1 - |> List.all \char -> - isLowerAlpha char || char == '_' || isNumeric char + |> List.drop_first(1) + |> List.all( + |char| + is_lower_alpha(char) or char == '_' or is_numeric(char), + ) ) - && !(Keyword.isReserved bytes) + and !(Keyword.is_reserved(bytes)) # Casing to : (List (List U8) -> List U8) -> (Str -> Str) -to = \fn -> \name -> +to = |fn| + |name| name |> words |> fn - |> Str.fromUtf8 - |> Result.withDefault "" - -upperCamelCase : List (List U8) -> List U8 -upperCamelCase = \wordList -> - List.joinMap wordList \word -> - when word is - [initial, ..] -> - List.set word 0 (toUpper initial) - - _ -> - word - -expect (to upperCamelCase) "Product" == "Product" -expect (to upperCamelCase) "product_users" == "ProductUsers" -expect (to upperCamelCase) "order_Products" == "OrderProducts" -expect (to upperCamelCase) "OrderProducts" == "OrderProducts" -expect (to upperCamelCase) "orderProducts" == "OrderProducts" -expect (to upperCamelCase) "123" == "N123" - -lowerCamelCase : List (List U8) -> List U8 -lowerCamelCase = \wordList -> - when wordList is + |> Str.from_utf8 + |> Result.with_default("") + +upper_camel_case : List (List U8) -> List U8 +upper_camel_case = |word_list| + List.join_map( + word_list, + |word| + when word is + [initial, ..] -> + List.set(word, 0, to_upper(initial)) + + _ -> + word, + ) + +expect (to(upper_camel_case))("Product") == "Product" +expect (to(upper_camel_case))("product_users") == "ProductUsers" +expect (to(upper_camel_case))("order_Products") == "OrderProducts" +expect (to(upper_camel_case))("OrderProducts") == "OrderProducts" +expect (to(upper_camel_case))("orderProducts") == "OrderProducts" +expect (to(upper_camel_case))("123") == "N123" + +lower_camel_case : List (List U8) -> List U8 +lower_camel_case = |word_list| + when word_list is [] -> [] [first, ..] -> rest = - wordList - |> List.dropFirst 1 - |> upperCamelCase + word_list + |> List.drop_first(1) + |> upper_camel_case - List.concat first rest + List.concat(first, rest) -expect (to lowerCamelCase) "Product" == "product" -expect (to lowerCamelCase) "product_users" == "productUsers" -expect (to lowerCamelCase) "order_Products" == "orderProducts" -expect (to lowerCamelCase) "OrderProducts" == "orderProducts" -expect (to lowerCamelCase) "orderProducts" == "orderProducts" -expect (to lowerCamelCase) "123" == "n123" +expect (to(lower_camel_case))("Product") == "product" +expect (to(lower_camel_case))("product_users") == "productUsers" +expect (to(lower_camel_case))("order_Products") == "orderProducts" +expect (to(lower_camel_case))("OrderProducts") == "orderProducts" +expect (to(lower_camel_case))("orderProducts") == "orderProducts" +expect (to(lower_camel_case))("123") == "n123" # Helpers words : Str -> List (List U8) -words = \value -> +words = |value| initial = { - word: List.withCapacity 16, - wordList: List.withCapacity 4, + word: List.with_capacity(16), + word_list: List.with_capacity(4), } - addWord = \state -> - lower = List.map state.word toLower - List.append state.wordList lower + add_word = |state| + lower = List.map(state.word, to_lower) + List.append(state.word_list, lower) value - |> Str.toUtf8 - |> List.walk initial \state, char -> - if char == '_' || char == ' ' then - { - word: List.withCapacity 16, - wordList: addWord state, - } - else if isUpperAlpha char && endsWithLower state.word then - { - word: List.reserve [char] 15, - wordList: addWord state, - } - else if isNumeric char && List.isEmpty state.word then - { - word: state.word |> List.append 'n' |> List.append char, - wordList: state.wordList, - } - else if isAlphaNum char then - { - word: List.append state.word char, - wordList: state.wordList, - } - else - code = Str.toUtf8 "c$(Num.toStr char)" - - { - word: List.concat state.word code, - wordList: state.wordList, - } - |> addWord - -endsWithLower : List U8 -> Bool -endsWithLower = \word -> - when List.last word is - Ok last -> - isLowerAlpha last - - Err ListWasEmpty -> + |> Str.to_utf8 + |> List.walk( + initial, + |state, char| + if char == '_' or char == ' ' then + { + word: List.with_capacity(16), + word_list: add_word(state), + } + else if is_upper_alpha(char) and ends_with_lower(state.word) then + { + word: List.reserve([char], 15), + word_list: add_word(state), + } + else if is_numeric(char) and List.is_empty(state.word) then + { + word: state.word |> List.append('n') |> List.append(char), + word_list: state.word_list, + } + else if is_alpha_num(char) then + { + word: List.append(state.word, char), + word_list: state.word_list, + } + else + code = Str.to_utf8("c${Num.to_str(char)}") + + { + word: List.concat(state.word, code), + word_list: state.word_list, + }, + ) + |> add_word + +ends_with_lower : List U8 -> Bool +ends_with_lower = |word| + when List.last(word) is + Ok(last) -> + is_lower_alpha(last) + + Err(ListWasEmpty) -> Bool.false -wordsStr : Str -> List Str -wordsStr = \value -> - words value - |> List.map \word -> - word - |> Str.fromUtf8 - |> Result.withDefault "" - -expect wordsStr "product" == ["product"] -expect wordsStr "product_id" == ["product", "id"] -expect wordsStr "product_user_id" == ["product", "user", "id"] -expect wordsStr "_product_id" == ["", "product", "id"] -expect wordsStr "product_id_" == ["product", "id", ""] -expect wordsStr "Product" == ["product"] -expect wordsStr "productId" == ["product", "id"] -expect wordsStr "productID" == ["product", "id"] -expect wordsStr "ProductId" == ["product", "id"] -expect wordsStr "productUserId" == ["product", "user", "id"] -expect wordsStr "productUser_Id" == ["product", "user", "id"] -expect wordsStr "productUser__Id" == ["product", "user", "", "id"] -expect wordsStr "countA" == ["count", "a"] -expect wordsStr "123_x" == ["n123", "x"] -expect wordsStr "nice day!" == ["nice", "dayc33"] - -caseDiff : U8 -caseDiff = 'a' - 'A' - -toLower : U8 -> U8 -toLower = \char -> - if isUpperAlpha char then - char + caseDiff +words_str : Str -> List Str +words_str = |value| + words(value) + |> List.map( + |word| + word + |> Str.from_utf8 + |> Result.with_default(""), + ) + +expect words_str("product") == ["product"] +expect words_str("product_id") == ["product", "id"] +expect words_str("product_user_id") == ["product", "user", "id"] +expect words_str("_product_id") == ["", "product", "id"] +expect words_str("product_id_") == ["product", "id", ""] +expect words_str("Product") == ["product"] +expect words_str("productId") == ["product", "id"] +expect words_str("productID") == ["product", "id"] +expect words_str("ProductId") == ["product", "id"] +expect words_str("productUserId") == ["product", "user", "id"] +expect words_str("productUser_Id") == ["product", "user", "id"] +expect words_str("productUser__Id") == ["product", "user", "", "id"] +expect words_str("countA") == ["count", "a"] +expect words_str("123_x") == ["n123", "x"] +expect words_str("nice day!") == ["nice", "dayc33"] + +case_diff : U8 +case_diff = 'a' - 'A' + +to_lower : U8 -> U8 +to_lower = |char| + if is_upper_alpha(char) then + char + case_diff else char -expect toLower 'G' == 'g' -expect toLower 'm' == 'm' +expect to_lower('G') == 'g' +expect to_lower('m') == 'm' -toUpper : U8 -> U8 -toUpper = \char -> - if isLowerAlpha char then - char - caseDiff +to_upper : U8 -> U8 +to_upper = |char| + if is_lower_alpha(char) then + char - case_diff else char -expect toUpper 'l' == 'L' -expect toUpper 'E' == 'E' +expect to_upper('l') == 'L' +expect to_upper('E') == 'E' -isAlphaNum : U8 -> Bool -isAlphaNum = \char -> - isUpperAlpha char || isLowerAlpha char || isNumeric char +is_alpha_num : U8 -> Bool +is_alpha_num = |char| + is_upper_alpha(char) or is_lower_alpha(char) or is_numeric(char) -isUpperAlpha : U8 -> Bool -isUpperAlpha = \char -> - char >= 'A' && char <= 'Z' +is_upper_alpha : U8 -> Bool +is_upper_alpha = |char| + char >= 'A' and char <= 'Z' -isLowerAlpha : U8 -> Bool -isLowerAlpha = \char -> - char >= 'a' && char <= 'z' +is_lower_alpha : U8 -> Bool +is_lower_alpha = |char| + char >= 'a' and char <= 'z' -isNumeric : U8 -> Bool -isNumeric = \char -> - char >= '0' && char <= '9' +is_numeric : U8 -> Bool +is_numeric = |char| + char >= '0' and char <= '9' diff --git a/sql-cli/src/PgCatalog.roc b/sql-cli/src/PgCatalog.roc index dd7175b..ec36d9c 100644 --- a/sql-cli/src/PgCatalog.roc +++ b/sql-cli/src/PgCatalog.roc @@ -157,7 +157,7 @@ pgStatistic = { schema: "pg_catalog", name: "pg_statistic", alias: "ps", - columns: \alias -> { + columns: |alias| { stainherit: identifier alias "stainherit" pgStatisticStainherit, stakind5: identifier alias "stakind5" Sql.Types.i16, stakind4: identifier alias "stakind4" Sql.Types.i16, @@ -199,7 +199,7 @@ pgType = { schema: "pg_catalog", name: "pg_type", alias: "pt", - columns: \alias -> { + columns: |alias| { typnotnull: identifier alias "typnotnull" Sql.Types.bool, typisdefined: identifier alias "typisdefined" Sql.Types.bool, typispreferred: identifier alias "typispreferred" Sql.Types.bool, @@ -242,7 +242,7 @@ pgForeignTable = { schema: "pg_catalog", name: "pg_foreign_table", alias: "pft", - columns: \alias -> { + columns: |alias| { ftserver: identifier alias "ftserver" Sql.Types.i32, ftrelid: identifier alias "ftrelid" pgForeignTableFtrelid, ftoptions: identifier alias "ftoptions" (Sql.Types.nullable (Sql.Types.array (Sql.Types.nullable Sql.Types.str))), @@ -256,7 +256,7 @@ pgAuthid = { schema: "pg_catalog", name: "pg_authid", alias: "pa", - columns: \alias -> { + columns: |alias| { rolbypassrls: identifier alias "rolbypassrls" Sql.Types.bool, rolreplication: identifier alias "rolreplication" Sql.Types.bool, rolcanlogin: identifier alias "rolcanlogin" Sql.Types.bool, @@ -276,7 +276,7 @@ pgShadow = { schema: "pg_catalog", name: "pg_shadow", alias: "ps", - columns: \alias -> { + columns: |alias| { usebypassrls: identifier alias "usebypassrls" (Sql.Types.nullable Sql.Types.bool), userepl: identifier alias "userepl" (Sql.Types.nullable Sql.Types.bool), usesuper: identifier alias "usesuper" (Sql.Types.nullable Sql.Types.bool), @@ -293,7 +293,7 @@ pgRoles = { schema: "pg_catalog", name: "pg_roles", alias: "pr", - columns: \alias -> { + columns: |alias| { rolbypassrls: identifier alias "rolbypassrls" (Sql.Types.nullable Sql.Types.bool), rolreplication: identifier alias "rolreplication" (Sql.Types.nullable Sql.Types.bool), rolcanlogin: identifier alias "rolcanlogin" (Sql.Types.nullable Sql.Types.bool), @@ -320,7 +320,7 @@ pgStatisticExtData = { schema: "pg_catalog", name: "pg_statistic_ext_data", alias: "psed", - columns: \alias -> { + columns: |alias| { stxdinherit: identifier alias "stxdinherit" pgStatisticExtDataStxdinherit, stxoid: identifier alias "stxoid" pgStatisticExtDataStxoid, stxdndistinct: identifier alias "stxdndistinct" (Sql.Types.nullable (Sql.Types.unsupported "pg_ndistinct")), @@ -334,7 +334,7 @@ pgSettings = { schema: "pg_catalog", name: "pg_settings", alias: "ps", - columns: \alias -> { + columns: |alias| { pendingRestart: identifier alias "pending_restart" (Sql.Types.nullable Sql.Types.bool), sourceline: identifier alias "sourceline" (Sql.Types.nullable Sql.Types.i32), sourcefile: identifier alias "sourcefile" (Sql.Types.nullable Sql.Types.str), @@ -359,7 +359,7 @@ pgFileSettings = { schema: "pg_catalog", name: "pg_file_settings", alias: "pfs", - columns: \alias -> { + columns: |alias| { applied: identifier alias "applied" (Sql.Types.nullable Sql.Types.bool), seqno: identifier alias "seqno" (Sql.Types.nullable Sql.Types.i32), sourceline: identifier alias "sourceline" (Sql.Types.nullable Sql.Types.i32), @@ -374,7 +374,7 @@ pgHbaFileRules = { schema: "pg_catalog", name: "pg_hba_file_rules", alias: "phfr", - columns: \alias -> { + columns: |alias| { lineNumber: identifier alias "line_number" (Sql.Types.nullable Sql.Types.i32), error: identifier alias "error" (Sql.Types.nullable Sql.Types.str), authMethod: identifier alias "auth_method" (Sql.Types.nullable Sql.Types.str), @@ -391,7 +391,7 @@ pgIdentFileMappings = { schema: "pg_catalog", name: "pg_ident_file_mappings", alias: "pifm", - columns: \alias -> { + columns: |alias| { lineNumber: identifier alias "line_number" (Sql.Types.nullable Sql.Types.i32), error: identifier alias "error" (Sql.Types.nullable Sql.Types.str), pgUsername: identifier alias "pg_username" (Sql.Types.nullable Sql.Types.str), @@ -404,7 +404,7 @@ pgConfig = { schema: "pg_catalog", name: "pg_config", alias: "pc", - columns: \alias -> { + columns: |alias| { setting: identifier alias "setting" (Sql.Types.nullable Sql.Types.str), name: identifier alias "name" (Sql.Types.nullable Sql.Types.str), }, @@ -414,7 +414,7 @@ pgShmemAllocations = { schema: "pg_catalog", name: "pg_shmem_allocations", alias: "psa", - columns: \alias -> { + columns: |alias| { allocatedSize: identifier alias "allocated_size" (Sql.Types.nullable Sql.Types.i64), size: identifier alias "size" (Sql.Types.nullable Sql.Types.i64), off: identifier alias "off" (Sql.Types.nullable Sql.Types.i64), @@ -426,7 +426,7 @@ pgBackendMemoryContexts = { schema: "pg_catalog", name: "pg_backend_memory_contexts", alias: "pbmc", - columns: \alias -> { + columns: |alias| { usedBytes: identifier alias "used_bytes" (Sql.Types.nullable Sql.Types.i64), freeChunks: identifier alias "free_chunks" (Sql.Types.nullable Sql.Types.i64), freeBytes: identifier alias "free_bytes" (Sql.Types.nullable Sql.Types.i64), @@ -446,7 +446,7 @@ pgUserMapping = { schema: "pg_catalog", name: "pg_user_mapping", alias: "pum", - columns: \alias -> { + columns: |alias| { umserver: identifier alias "umserver" Sql.Types.i32, umuser: identifier alias "umuser" Sql.Types.i32, oid: identifier alias "oid" pgUserMappingOid, @@ -458,7 +458,7 @@ pgStatActivity = { schema: "pg_catalog", name: "pg_stat_activity", alias: "psa", - columns: \alias -> { + columns: |alias| { usename: identifier alias "usename" (Sql.Types.nullable Sql.Types.str), datname: identifier alias "datname" (Sql.Types.nullable Sql.Types.str), queryId: identifier alias "query_id" (Sql.Types.nullable Sql.Types.i64), @@ -488,7 +488,7 @@ pgReplicationOriginStatus = { schema: "pg_catalog", name: "pg_replication_origin_status", alias: "pros", - columns: \alias -> { + columns: |alias| { externalId: identifier alias "external_id" (Sql.Types.nullable Sql.Types.str), localId: identifier alias "local_id" (Sql.Types.nullable Sql.Types.i32), localLsn: identifier alias "local_lsn" (Sql.Types.nullable (Sql.Types.unsupported "pg_lsn")), @@ -503,7 +503,7 @@ pgSubscription = { schema: "pg_catalog", name: "pg_subscription", alias: "ps", - columns: \alias -> { + columns: |alias| { subdisableonerr: identifier alias "subdisableonerr" Sql.Types.bool, substream: identifier alias "substream" Sql.Types.bool, subbinary: identifier alias "subbinary" Sql.Types.bool, @@ -531,7 +531,7 @@ pgAttribute = { schema: "pg_catalog", name: "pg_attribute", alias: "pa", - columns: \alias -> { + columns: |alias| { attislocal: identifier alias "attislocal" Sql.Types.bool, attisdropped: identifier alias "attisdropped" Sql.Types.bool, atthasmissing: identifier alias "atthasmissing" Sql.Types.bool, @@ -568,7 +568,7 @@ pgProc = { schema: "pg_catalog", name: "pg_proc", alias: "pp", - columns: \alias -> { + columns: |alias| { proretset: identifier alias "proretset" Sql.Types.bool, proisstrict: identifier alias "proisstrict" Sql.Types.bool, proleakproof: identifier alias "proleakproof" Sql.Types.bool, @@ -609,7 +609,7 @@ pgClass = { schema: "pg_catalog", name: "pg_class", alias: "pc", - columns: \alias -> { + columns: |alias| { relispartition: identifier alias "relispartition" Sql.Types.bool, relispopulated: identifier alias "relispopulated" Sql.Types.bool, relforcerowsecurity: identifier alias "relforcerowsecurity" Sql.Types.bool, @@ -653,7 +653,7 @@ pgAttrdef = { schema: "pg_catalog", name: "pg_attrdef", alias: "pa", - columns: \alias -> { + columns: |alias| { adnum: identifier alias "adnum" Sql.Types.i16, adrelid: identifier alias "adrelid" Sql.Types.i32, oid: identifier alias "oid" pgAttrdefOid, @@ -668,7 +668,7 @@ pgConstraint = { schema: "pg_catalog", name: "pg_constraint", alias: "pc", - columns: \alias -> { + columns: |alias| { connoinherit: identifier alias "connoinherit" Sql.Types.bool, conislocal: identifier alias "conislocal" Sql.Types.bool, convalidated: identifier alias "convalidated" Sql.Types.bool, @@ -708,7 +708,7 @@ pgInherits = { schema: "pg_catalog", name: "pg_inherits", alias: "pi", - columns: \alias -> { + columns: |alias| { inhdetachpending: identifier alias "inhdetachpending" Sql.Types.bool, inhseqno: identifier alias "inhseqno" pgInheritsInhseqno, inhparent: identifier alias "inhparent" Sql.Types.i32, @@ -723,7 +723,7 @@ pgIndex = { schema: "pg_catalog", name: "pg_index", alias: "pi", - columns: \alias -> { + columns: |alias| { indisreplident: identifier alias "indisreplident" Sql.Types.bool, indislive: identifier alias "indislive" Sql.Types.bool, indisready: identifier alias "indisready" Sql.Types.bool, @@ -752,7 +752,7 @@ pgStatReplication = { schema: "pg_catalog", name: "pg_stat_replication", alias: "psr", - columns: \alias -> { + columns: |alias| { usename: identifier alias "usename" (Sql.Types.nullable Sql.Types.str), syncPriority: identifier alias "sync_priority" (Sql.Types.nullable Sql.Types.i32), clientPort: identifier alias "client_port" (Sql.Types.nullable Sql.Types.i32), @@ -780,7 +780,7 @@ pgStatSlru = { schema: "pg_catalog", name: "pg_stat_slru", alias: "pss", - columns: \alias -> { + columns: |alias| { truncates: identifier alias "truncates" (Sql.Types.nullable Sql.Types.i64), flushes: identifier alias "flushes" (Sql.Types.nullable Sql.Types.i64), blksExists: identifier alias "blks_exists" (Sql.Types.nullable Sql.Types.i64), @@ -797,7 +797,7 @@ pgStatWalReceiver = { schema: "pg_catalog", name: "pg_stat_wal_receiver", alias: "pswr", - columns: \alias -> { + columns: |alias| { senderPort: identifier alias "sender_port" (Sql.Types.nullable Sql.Types.i32), receivedTli: identifier alias "received_tli" (Sql.Types.nullable Sql.Types.i32), receiveStartTli: identifier alias "receive_start_tli" (Sql.Types.nullable Sql.Types.i32), @@ -820,7 +820,7 @@ pgStatRecoveryPrefetch = { schema: "pg_catalog", name: "pg_stat_recovery_prefetch", alias: "psrp", - columns: \alias -> { + columns: |alias| { skipRep: identifier alias "skip_rep" (Sql.Types.nullable Sql.Types.i64), skipFpw: identifier alias "skip_fpw" (Sql.Types.nullable Sql.Types.i64), skipNew: identifier alias "skip_new" (Sql.Types.nullable Sql.Types.i64), @@ -841,7 +841,7 @@ pgOperator = { schema: "pg_catalog", name: "pg_operator", alias: "po", - columns: \alias -> { + columns: |alias| { oprcanhash: identifier alias "oprcanhash" Sql.Types.bool, oprcanmerge: identifier alias "oprcanmerge" Sql.Types.bool, oprkind: identifier alias "oprkind" Sql.Types.str, @@ -867,7 +867,7 @@ pgOpfamily = { schema: "pg_catalog", name: "pg_opfamily", alias: "po", - columns: \alias -> { + columns: |alias| { opfname: identifier alias "opfname" Sql.Types.str, opfowner: identifier alias "opfowner" Sql.Types.i32, opfnamespace: identifier alias "opfnamespace" Sql.Types.i32, @@ -883,7 +883,7 @@ pgOpclass = { schema: "pg_catalog", name: "pg_opclass", alias: "po", - columns: \alias -> { + columns: |alias| { opcdefault: identifier alias "opcdefault" Sql.Types.bool, opcname: identifier alias "opcname" Sql.Types.str, opckeytype: identifier alias "opckeytype" Sql.Types.i32, @@ -903,7 +903,7 @@ pgAm = { schema: "pg_catalog", name: "pg_am", alias: "pa", - columns: \alias -> { + columns: |alias| { amtype: identifier alias "amtype" Sql.Types.str, amname: identifier alias "amname" Sql.Types.str, amhandler: identifier alias "amhandler" (Sql.Types.unsupported "regproc"), @@ -918,7 +918,7 @@ pgAmop = { schema: "pg_catalog", name: "pg_amop", alias: "pa", - columns: \alias -> { + columns: |alias| { amoppurpose: identifier alias "amoppurpose" Sql.Types.str, amopstrategy: identifier alias "amopstrategy" Sql.Types.i16, amopsortfamily: identifier alias "amopsortfamily" Sql.Types.i32, @@ -938,7 +938,7 @@ pgAmproc = { schema: "pg_catalog", name: "pg_amproc", alias: "pa", - columns: \alias -> { + columns: |alias| { amprocnum: identifier alias "amprocnum" Sql.Types.i16, amproc: identifier alias "amproc" (Sql.Types.unsupported "regproc"), amprocrighttype: identifier alias "amprocrighttype" Sql.Types.i32, @@ -955,7 +955,7 @@ pgLanguage = { schema: "pg_catalog", name: "pg_language", alias: "pl", - columns: \alias -> { + columns: |alias| { lanpltrusted: identifier alias "lanpltrusted" Sql.Types.bool, lanispl: identifier alias "lanispl" Sql.Types.bool, lanname: identifier alias "lanname" Sql.Types.str, @@ -975,7 +975,7 @@ pgLargeobjectMetadata = { schema: "pg_catalog", name: "pg_largeobject_metadata", alias: "plm", - columns: \alias -> { + columns: |alias| { lomowner: identifier alias "lomowner" Sql.Types.i32, oid: identifier alias "oid" pgLargeobjectMetadataOid, lomacl: identifier alias "lomacl" (Sql.Types.nullable (Sql.Types.array (Sql.Types.nullable (Sql.Types.unsupported "aclitem")))), @@ -989,7 +989,7 @@ pgAggregate = { schema: "pg_catalog", name: "pg_aggregate", alias: "pa", - columns: \alias -> { + columns: |alias| { aggmfinalextra: identifier alias "aggmfinalextra" Sql.Types.bool, aggfinalextra: identifier alias "aggfinalextra" Sql.Types.bool, aggmfinalmodify: identifier alias "aggmfinalmodify" Sql.Types.str, @@ -1022,7 +1022,7 @@ pgStatisticExt = { schema: "pg_catalog", name: "pg_statistic_ext", alias: "pse", - columns: \alias -> { + columns: |alias| { stxname: identifier alias "stxname" Sql.Types.str, stxkeys: identifier alias "stxkeys" (Sql.Types.array (Sql.Types.nullable Sql.Types.i16)), stxstattarget: identifier alias "stxstattarget" Sql.Types.i32, @@ -1042,7 +1042,7 @@ pgRewrite = { schema: "pg_catalog", name: "pg_rewrite", alias: "pr", - columns: \alias -> { + columns: |alias| { isInstead: identifier alias "is_instead" Sql.Types.bool, evEnabled: identifier alias "ev_enabled" Sql.Types.str, evType: identifier alias "ev_type" Sql.Types.str, @@ -1061,7 +1061,7 @@ pgTrigger = { schema: "pg_catalog", name: "pg_trigger", alias: "pt", - columns: \alias -> { + columns: |alias| { tginitdeferred: identifier alias "tginitdeferred" Sql.Types.bool, tgdeferrable: identifier alias "tgdeferrable" Sql.Types.bool, tgisinternal: identifier alias "tgisinternal" Sql.Types.bool, @@ -1091,7 +1091,7 @@ pgEventTrigger = { schema: "pg_catalog", name: "pg_event_trigger", alias: "pet", - columns: \alias -> { + columns: |alias| { evtenabled: identifier alias "evtenabled" Sql.Types.str, evtevent: identifier alias "evtevent" Sql.Types.str, evtname: identifier alias "evtname" Sql.Types.str, @@ -1115,7 +1115,7 @@ pgDescription = { schema: "pg_catalog", name: "pg_description", alias: "pd", - columns: \alias -> { + columns: |alias| { objsubid: identifier alias "objsubid" pgDescriptionObjsubid, description: identifier alias "description" Sql.Types.str, classoid: identifier alias "classoid" pgDescriptionClassoid, @@ -1130,7 +1130,7 @@ pgCast = { schema: "pg_catalog", name: "pg_cast", alias: "pc", - columns: \alias -> { + columns: |alias| { castmethod: identifier alias "castmethod" Sql.Types.str, castcontext: identifier alias "castcontext" Sql.Types.str, castfunc: identifier alias "castfunc" Sql.Types.i32, @@ -1147,7 +1147,7 @@ pgEnum = { schema: "pg_catalog", name: "pg_enum", alias: "pe", - columns: \alias -> { + columns: |alias| { enumlabel: identifier alias "enumlabel" Sql.Types.str, enumtypid: identifier alias "enumtypid" Sql.Types.i32, oid: identifier alias "oid" pgEnumOid, @@ -1162,7 +1162,7 @@ pgNamespace = { schema: "pg_catalog", name: "pg_namespace", alias: "pn", - columns: \alias -> { + columns: |alias| { nspname: identifier alias "nspname" Sql.Types.str, nspowner: identifier alias "nspowner" Sql.Types.i32, oid: identifier alias "oid" pgNamespaceOid, @@ -1177,7 +1177,7 @@ pgConversion = { schema: "pg_catalog", name: "pg_conversion", alias: "pc", - columns: \alias -> { + columns: |alias| { condefault: identifier alias "condefault" Sql.Types.bool, conname: identifier alias "conname" Sql.Types.str, contoencoding: identifier alias "contoencoding" Sql.Types.i32, @@ -1193,7 +1193,7 @@ pgDepend = { schema: "pg_catalog", name: "pg_depend", alias: "pd", - columns: \alias -> { + columns: |alias| { deptype: identifier alias "deptype" Sql.Types.str, refobjsubid: identifier alias "refobjsubid" Sql.Types.i32, objsubid: identifier alias "objsubid" Sql.Types.i32, @@ -1211,7 +1211,7 @@ pgDatabase = { schema: "pg_catalog", name: "pg_database", alias: "pd", - columns: \alias -> { + columns: |alias| { datallowconn: identifier alias "datallowconn" Sql.Types.bool, datistemplate: identifier alias "datistemplate" Sql.Types.bool, datlocprovider: identifier alias "datlocprovider" Sql.Types.str, @@ -1241,7 +1241,7 @@ pgDbRoleSetting = { schema: "pg_catalog", name: "pg_db_role_setting", alias: "pdrs", - columns: \alias -> { + columns: |alias| { setrole: identifier alias "setrole" pgDbRoleSettingSetrole, setdatabase: identifier alias "setdatabase" pgDbRoleSettingSetdatabase, setconfig: identifier alias "setconfig" (Sql.Types.nullable (Sql.Types.array (Sql.Types.nullable Sql.Types.str))), @@ -1255,7 +1255,7 @@ pgTablespace = { schema: "pg_catalog", name: "pg_tablespace", alias: "pt", - columns: \alias -> { + columns: |alias| { spcname: identifier alias "spcname" Sql.Types.str, spcowner: identifier alias "spcowner" Sql.Types.i32, oid: identifier alias "oid" pgTablespaceOid, @@ -1274,7 +1274,7 @@ pgAuthMembers = { schema: "pg_catalog", name: "pg_auth_members", alias: "pam", - columns: \alias -> { + columns: |alias| { adminOption: identifier alias "admin_option" Sql.Types.bool, grantor: identifier alias "grantor" Sql.Types.i32, member: identifier alias "member" pgAuthMembersMember, @@ -1286,7 +1286,7 @@ pgShdepend = { schema: "pg_catalog", name: "pg_shdepend", alias: "ps", - columns: \alias -> { + columns: |alias| { deptype: identifier alias "deptype" Sql.Types.str, objsubid: identifier alias "objsubid" Sql.Types.i32, refobjid: identifier alias "refobjid" Sql.Types.i32, @@ -1307,7 +1307,7 @@ pgShdescription = { schema: "pg_catalog", name: "pg_shdescription", alias: "ps", - columns: \alias -> { + columns: |alias| { description: identifier alias "description" Sql.Types.str, classoid: identifier alias "classoid" pgShdescriptionClassoid, objoid: identifier alias "objoid" pgShdescriptionObjoid, @@ -1321,7 +1321,7 @@ pgTsConfig = { schema: "pg_catalog", name: "pg_ts_config", alias: "ptc", - columns: \alias -> { + columns: |alias| { cfgname: identifier alias "cfgname" Sql.Types.str, cfgparser: identifier alias "cfgparser" Sql.Types.i32, cfgowner: identifier alias "cfgowner" Sql.Types.i32, @@ -1343,7 +1343,7 @@ pgTsConfigMap = { schema: "pg_catalog", name: "pg_ts_config_map", alias: "ptcm", - columns: \alias -> { + columns: |alias| { mapseqno: identifier alias "mapseqno" pgTsConfigMapMapseqno, maptokentype: identifier alias "maptokentype" pgTsConfigMapMaptokentype, mapdict: identifier alias "mapdict" Sql.Types.i32, @@ -1358,7 +1358,7 @@ pgTsDict = { schema: "pg_catalog", name: "pg_ts_dict", alias: "ptd", - columns: \alias -> { + columns: |alias| { dictname: identifier alias "dictname" Sql.Types.str, dictinitoption: identifier alias "dictinitoption" (Sql.Types.nullable Sql.Types.str), dicttemplate: identifier alias "dicttemplate" Sql.Types.i32, @@ -1375,7 +1375,7 @@ pgTsParser = { schema: "pg_catalog", name: "pg_ts_parser", alias: "ptp", - columns: \alias -> { + columns: |alias| { prsname: identifier alias "prsname" Sql.Types.str, prslextype: identifier alias "prslextype" (Sql.Types.unsupported "regproc"), prsheadline: identifier alias "prsheadline" (Sql.Types.unsupported "regproc"), @@ -1394,7 +1394,7 @@ pgTsTemplate = { schema: "pg_catalog", name: "pg_ts_template", alias: "ptt", - columns: \alias -> { + columns: |alias| { tmplname: identifier alias "tmplname" Sql.Types.str, tmpllexize: identifier alias "tmpllexize" (Sql.Types.unsupported "regproc"), tmplinit: identifier alias "tmplinit" (Sql.Types.unsupported "regproc"), @@ -1410,7 +1410,7 @@ pgExtension = { schema: "pg_catalog", name: "pg_extension", alias: "pe", - columns: \alias -> { + columns: |alias| { extrelocatable: identifier alias "extrelocatable" Sql.Types.bool, extname: identifier alias "extname" Sql.Types.str, extversion: identifier alias "extversion" Sql.Types.str, @@ -1429,7 +1429,7 @@ pgForeignDataWrapper = { schema: "pg_catalog", name: "pg_foreign_data_wrapper", alias: "pfdw", - columns: \alias -> { + columns: |alias| { fdwname: identifier alias "fdwname" Sql.Types.str, fdwvalidator: identifier alias "fdwvalidator" Sql.Types.i32, fdwhandler: identifier alias "fdwhandler" Sql.Types.i32, @@ -1447,7 +1447,7 @@ pgForeignServer = { schema: "pg_catalog", name: "pg_foreign_server", alias: "pfs", - columns: \alias -> { + columns: |alias| { srvname: identifier alias "srvname" Sql.Types.str, srvversion: identifier alias "srvversion" (Sql.Types.nullable Sql.Types.str), srvtype: identifier alias "srvtype" (Sql.Types.nullable Sql.Types.str), @@ -1466,7 +1466,7 @@ pgPolicy = { schema: "pg_catalog", name: "pg_policy", alias: "pp", - columns: \alias -> { + columns: |alias| { polpermissive: identifier alias "polpermissive" Sql.Types.bool, polcmd: identifier alias "polcmd" Sql.Types.str, polname: identifier alias "polname" Sql.Types.str, @@ -1485,7 +1485,7 @@ pgReplicationOrigin = { schema: "pg_catalog", name: "pg_replication_origin", alias: "pro", - columns: \alias -> { + columns: |alias| { roname: identifier alias "roname" Sql.Types.str, roident: identifier alias "roident" pgReplicationOriginRoident, }, @@ -1498,7 +1498,7 @@ pgDefaultAcl = { schema: "pg_catalog", name: "pg_default_acl", alias: "pda", - columns: \alias -> { + columns: |alias| { defaclobjtype: identifier alias "defaclobjtype" Sql.Types.str, defaclnamespace: identifier alias "defaclnamespace" Sql.Types.i32, defaclrole: identifier alias "defaclrole" Sql.Types.i32, @@ -1520,7 +1520,7 @@ pgInitPrivs = { schema: "pg_catalog", name: "pg_init_privs", alias: "pip", - columns: \alias -> { + columns: |alias| { privtype: identifier alias "privtype" Sql.Types.str, objsubid: identifier alias "objsubid" pgInitPrivsObjsubid, classoid: identifier alias "classoid" pgInitPrivsClassoid, @@ -1545,7 +1545,7 @@ pgSeclabel = { schema: "pg_catalog", name: "pg_seclabel", alias: "ps", - columns: \alias -> { + columns: |alias| { objsubid: identifier alias "objsubid" pgSeclabelObjsubid, label: identifier alias "label" Sql.Types.str, provider: identifier alias "provider" pgSeclabelProvider, @@ -1567,7 +1567,7 @@ pgShseclabel = { schema: "pg_catalog", name: "pg_shseclabel", alias: "ps", - columns: \alias -> { + columns: |alias| { label: identifier alias "label" Sql.Types.str, provider: identifier alias "provider" pgShseclabelProvider, classoid: identifier alias "classoid" pgShseclabelClassoid, @@ -1582,7 +1582,7 @@ pgCollation = { schema: "pg_catalog", name: "pg_collation", alias: "pc", - columns: \alias -> { + columns: |alias| { collisdeterministic: identifier alias "collisdeterministic" Sql.Types.bool, collprovider: identifier alias "collprovider" Sql.Types.str, collname: identifier alias "collname" Sql.Types.str, @@ -1604,7 +1604,7 @@ pgParameterAcl = { schema: "pg_catalog", name: "pg_parameter_acl", alias: "ppa", - columns: \alias -> { + columns: |alias| { parname: identifier alias "parname" Sql.Types.str, oid: identifier alias "oid" pgParameterAclOid, paracl: identifier alias "paracl" (Sql.Types.nullable (Sql.Types.array (Sql.Types.nullable (Sql.Types.unsupported "aclitem")))), @@ -1618,7 +1618,7 @@ pgPartitionedTable = { schema: "pg_catalog", name: "pg_partitioned_table", alias: "ppt", - columns: \alias -> { + columns: |alias| { partstrat: identifier alias "partstrat" Sql.Types.str, partnatts: identifier alias "partnatts" Sql.Types.i16, partattrs: identifier alias "partattrs" (Sql.Types.array (Sql.Types.nullable Sql.Types.i16)), @@ -1637,7 +1637,7 @@ pgRange = { schema: "pg_catalog", name: "pg_range", alias: "pr", - columns: \alias -> { + columns: |alias| { rngsubdiff: identifier alias "rngsubdiff" (Sql.Types.unsupported "regproc"), rngcanonical: identifier alias "rngcanonical" (Sql.Types.unsupported "regproc"), rngsubopc: identifier alias "rngsubopc" Sql.Types.i32, @@ -1655,7 +1655,7 @@ pgTransform = { schema: "pg_catalog", name: "pg_transform", alias: "pt", - columns: \alias -> { + columns: |alias| { trftosql: identifier alias "trftosql" (Sql.Types.unsupported "regproc"), trffromsql: identifier alias "trffromsql" (Sql.Types.unsupported "regproc"), trflang: identifier alias "trflang" Sql.Types.i32, @@ -1671,7 +1671,7 @@ pgSequence = { schema: "pg_catalog", name: "pg_sequence", alias: "ps", - columns: \alias -> { + columns: |alias| { seqcycle: identifier alias "seqcycle" Sql.Types.bool, seqcache: identifier alias "seqcache" Sql.Types.i64, seqmin: identifier alias "seqmin" Sql.Types.i64, @@ -1690,7 +1690,7 @@ pgPublication = { schema: "pg_catalog", name: "pg_publication", alias: "pp", - columns: \alias -> { + columns: |alias| { pubviaroot: identifier alias "pubviaroot" Sql.Types.bool, pubtruncate: identifier alias "pubtruncate" Sql.Types.bool, pubdelete: identifier alias "pubdelete" Sql.Types.bool, @@ -1710,7 +1710,7 @@ pgPublicationNamespace = { schema: "pg_catalog", name: "pg_publication_namespace", alias: "ppn", - columns: \alias -> { + columns: |alias| { pnnspid: identifier alias "pnnspid" Sql.Types.i32, pnpubid: identifier alias "pnpubid" Sql.Types.i32, oid: identifier alias "oid" pgPublicationNamespaceOid, @@ -1724,7 +1724,7 @@ pgPublicationRel = { schema: "pg_catalog", name: "pg_publication_rel", alias: "ppr", - columns: \alias -> { + columns: |alias| { prattrs: identifier alias "prattrs" (Sql.Types.nullable (Sql.Types.array (Sql.Types.nullable Sql.Types.i16))), prrelid: identifier alias "prrelid" Sql.Types.i32, prpubid: identifier alias "prpubid" Sql.Types.i32, @@ -1743,7 +1743,7 @@ pgSubscriptionRel = { schema: "pg_catalog", name: "pg_subscription_rel", alias: "psr", - columns: \alias -> { + columns: |alias| { srsubstate: identifier alias "srsubstate" Sql.Types.str, srrelid: identifier alias "srrelid" pgSubscriptionRelSrrelid, srsubid: identifier alias "srsubid" pgSubscriptionRelSrsubid, @@ -1755,7 +1755,7 @@ pgGroup = { schema: "pg_catalog", name: "pg_group", alias: "pg", - columns: \alias -> { + columns: |alias| { groname: identifier alias "groname" (Sql.Types.nullable Sql.Types.str), grosysid: identifier alias "grosysid" (Sql.Types.nullable Sql.Types.i32), grolist: identifier alias "grolist" (Sql.Types.nullable (Sql.Types.array (Sql.Types.nullable Sql.Types.i32))), @@ -1766,7 +1766,7 @@ pgUser = { schema: "pg_catalog", name: "pg_user", alias: "pu", - columns: \alias -> { + columns: |alias| { usebypassrls: identifier alias "usebypassrls" (Sql.Types.nullable Sql.Types.bool), userepl: identifier alias "userepl" (Sql.Types.nullable Sql.Types.bool), usesuper: identifier alias "usesuper" (Sql.Types.nullable Sql.Types.bool), @@ -1783,7 +1783,7 @@ pgPolicies = { schema: "pg_catalog", name: "pg_policies", alias: "pp", - columns: \alias -> { + columns: |alias| { policyname: identifier alias "policyname" (Sql.Types.nullable Sql.Types.str), tablename: identifier alias "tablename" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), @@ -1799,7 +1799,7 @@ pgRules = { schema: "pg_catalog", name: "pg_rules", alias: "pr", - columns: \alias -> { + columns: |alias| { rulename: identifier alias "rulename" (Sql.Types.nullable Sql.Types.str), tablename: identifier alias "tablename" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), @@ -1811,7 +1811,7 @@ pgViews = { schema: "pg_catalog", name: "pg_views", alias: "pv", - columns: \alias -> { + columns: |alias| { viewowner: identifier alias "viewowner" (Sql.Types.nullable Sql.Types.str), viewname: identifier alias "viewname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), @@ -1823,7 +1823,7 @@ pgTables = { schema: "pg_catalog", name: "pg_tables", alias: "pt", - columns: \alias -> { + columns: |alias| { rowsecurity: identifier alias "rowsecurity" (Sql.Types.nullable Sql.Types.bool), hastriggers: identifier alias "hastriggers" (Sql.Types.nullable Sql.Types.bool), hasrules: identifier alias "hasrules" (Sql.Types.nullable Sql.Types.bool), @@ -1839,7 +1839,7 @@ pgMatviews = { schema: "pg_catalog", name: "pg_matviews", alias: "pm", - columns: \alias -> { + columns: |alias| { ispopulated: identifier alias "ispopulated" (Sql.Types.nullable Sql.Types.bool), hasindexes: identifier alias "hasindexes" (Sql.Types.nullable Sql.Types.bool), tablespace: identifier alias "tablespace" (Sql.Types.nullable Sql.Types.str), @@ -1854,7 +1854,7 @@ pgIndexes = { schema: "pg_catalog", name: "pg_indexes", alias: "pi", - columns: \alias -> { + columns: |alias| { tablespace: identifier alias "tablespace" (Sql.Types.nullable Sql.Types.str), indexname: identifier alias "indexname" (Sql.Types.nullable Sql.Types.str), tablename: identifier alias "tablename" (Sql.Types.nullable Sql.Types.str), @@ -1867,7 +1867,7 @@ pgSequences = { schema: "pg_catalog", name: "pg_sequences", alias: "ps", - columns: \alias -> { + columns: |alias| { cycle: identifier alias "cycle" (Sql.Types.nullable Sql.Types.bool), sequenceowner: identifier alias "sequenceowner" (Sql.Types.nullable Sql.Types.str), sequencename: identifier alias "sequencename" (Sql.Types.nullable Sql.Types.str), @@ -1886,7 +1886,7 @@ pgStats = { schema: "pg_catalog", name: "pg_stats", alias: "ps", - columns: \alias -> { + columns: |alias| { inherited: identifier alias "inherited" (Sql.Types.nullable Sql.Types.bool), attname: identifier alias "attname" (Sql.Types.nullable Sql.Types.str), tablename: identifier alias "tablename" (Sql.Types.nullable Sql.Types.str), @@ -1908,7 +1908,7 @@ pgStatsExt = { schema: "pg_catalog", name: "pg_stats_ext", alias: "pse", - columns: \alias -> { + columns: |alias| { inherited: identifier alias "inherited" (Sql.Types.nullable Sql.Types.bool), statisticsOwner: identifier alias "statistics_owner" (Sql.Types.nullable Sql.Types.str), statisticsName: identifier alias "statistics_name" (Sql.Types.nullable Sql.Types.str), @@ -1931,7 +1931,7 @@ pgStatsExtExprs = { schema: "pg_catalog", name: "pg_stats_ext_exprs", alias: "psee", - columns: \alias -> { + columns: |alias| { inherited: identifier alias "inherited" (Sql.Types.nullable Sql.Types.bool), statisticsOwner: identifier alias "statistics_owner" (Sql.Types.nullable Sql.Types.str), statisticsName: identifier alias "statistics_name" (Sql.Types.nullable Sql.Types.str), @@ -1956,7 +1956,7 @@ pgPublicationTables = { schema: "pg_catalog", name: "pg_publication_tables", alias: "ppt", - columns: \alias -> { + columns: |alias| { tablename: identifier alias "tablename" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), pubname: identifier alias "pubname" (Sql.Types.nullable Sql.Types.str), @@ -1969,7 +1969,7 @@ pgLocks = { schema: "pg_catalog", name: "pg_locks", alias: "pl", - columns: \alias -> { + columns: |alias| { fastpath: identifier alias "fastpath" (Sql.Types.nullable Sql.Types.bool), granted: identifier alias "granted" (Sql.Types.nullable Sql.Types.bool), objsubid: identifier alias "objsubid" (Sql.Types.nullable Sql.Types.i16), @@ -1993,7 +1993,7 @@ pgCursors = { schema: "pg_catalog", name: "pg_cursors", alias: "pc", - columns: \alias -> { + columns: |alias| { isScrollable: identifier alias "is_scrollable" (Sql.Types.nullable Sql.Types.bool), isBinary: identifier alias "is_binary" (Sql.Types.nullable Sql.Types.bool), isHoldable: identifier alias "is_holdable" (Sql.Types.nullable Sql.Types.bool), @@ -2007,7 +2007,7 @@ pgAvailableExtensions = { schema: "pg_catalog", name: "pg_available_extensions", alias: "pae", - columns: \alias -> { + columns: |alias| { name: identifier alias "name" (Sql.Types.nullable Sql.Types.str), comment: identifier alias "comment" (Sql.Types.nullable Sql.Types.str), installedVersion: identifier alias "installed_version" (Sql.Types.nullable Sql.Types.str), @@ -2019,7 +2019,7 @@ pgAvailableExtensionVersions = { schema: "pg_catalog", name: "pg_available_extension_versions", alias: "paev", - columns: \alias -> { + columns: |alias| { relocatable: identifier alias "relocatable" (Sql.Types.nullable Sql.Types.bool), trusted: identifier alias "trusted" (Sql.Types.nullable Sql.Types.bool), superuser: identifier alias "superuser" (Sql.Types.nullable Sql.Types.bool), @@ -2036,7 +2036,7 @@ pgPreparedXacts = { schema: "pg_catalog", name: "pg_prepared_xacts", alias: "ppx", - columns: \alias -> { + columns: |alias| { database: identifier alias "database" (Sql.Types.nullable Sql.Types.str), owner: identifier alias "owner" (Sql.Types.nullable Sql.Types.str), gid: identifier alias "gid" (Sql.Types.nullable Sql.Types.str), @@ -2049,7 +2049,7 @@ pgPreparedStatements = { schema: "pg_catalog", name: "pg_prepared_statements", alias: "pps", - columns: \alias -> { + columns: |alias| { fromSql: identifier alias "from_sql" (Sql.Types.nullable Sql.Types.bool), customPlans: identifier alias "custom_plans" (Sql.Types.nullable Sql.Types.i64), genericPlans: identifier alias "generic_plans" (Sql.Types.nullable Sql.Types.i64), @@ -2064,7 +2064,7 @@ pgSeclabels = { schema: "pg_catalog", name: "pg_seclabels", alias: "ps", - columns: \alias -> { + columns: |alias| { objsubid: identifier alias "objsubid" (Sql.Types.nullable Sql.Types.i32), label: identifier alias "label" (Sql.Types.nullable Sql.Types.str), provider: identifier alias "provider" (Sql.Types.nullable Sql.Types.str), @@ -2080,7 +2080,7 @@ pgTimezoneAbbrevs = { schema: "pg_catalog", name: "pg_timezone_abbrevs", alias: "pta", - columns: \alias -> { + columns: |alias| { isDst: identifier alias "is_dst" (Sql.Types.nullable Sql.Types.bool), abbrev: identifier alias "abbrev" (Sql.Types.nullable Sql.Types.str), utcOffset: identifier alias "utc_offset" (Sql.Types.nullable (Sql.Types.unsupported "interval")), @@ -2091,7 +2091,7 @@ pgTimezoneNames = { schema: "pg_catalog", name: "pg_timezone_names", alias: "ptn", - columns: \alias -> { + columns: |alias| { isDst: identifier alias "is_dst" (Sql.Types.nullable Sql.Types.bool), abbrev: identifier alias "abbrev" (Sql.Types.nullable Sql.Types.str), name: identifier alias "name" (Sql.Types.nullable Sql.Types.str), @@ -2103,7 +2103,7 @@ pgStatSysTables = { schema: "pg_catalog", name: "pg_stat_sys_tables", alias: "psst", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), autoanalyzeCount: identifier alias "autoanalyze_count" (Sql.Types.nullable Sql.Types.i64), @@ -2134,7 +2134,7 @@ pgStatXactSysTables = { schema: "pg_catalog", name: "pg_stat_xact_sys_tables", alias: "psxst", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), nTupHotUpd: identifier alias "n_tup_hot_upd" (Sql.Types.nullable Sql.Types.i64), @@ -2153,7 +2153,7 @@ pgStatUserTables = { schema: "pg_catalog", name: "pg_stat_user_tables", alias: "psut", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), autoanalyzeCount: identifier alias "autoanalyze_count" (Sql.Types.nullable Sql.Types.i64), @@ -2184,7 +2184,7 @@ pgStatAllTables = { schema: "pg_catalog", name: "pg_stat_all_tables", alias: "psat", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), autoanalyzeCount: identifier alias "autoanalyze_count" (Sql.Types.nullable Sql.Types.i64), @@ -2215,7 +2215,7 @@ pgStatXactAllTables = { schema: "pg_catalog", name: "pg_stat_xact_all_tables", alias: "psxat", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), nTupHotUpd: identifier alias "n_tup_hot_upd" (Sql.Types.nullable Sql.Types.i64), @@ -2234,7 +2234,7 @@ pgStatXactUserTables = { schema: "pg_catalog", name: "pg_stat_xact_user_tables", alias: "psxut", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), nTupHotUpd: identifier alias "n_tup_hot_upd" (Sql.Types.nullable Sql.Types.i64), @@ -2253,7 +2253,7 @@ pgStatioAllTables = { schema: "pg_catalog", name: "pg_statio_all_tables", alias: "psat", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), tidxBlksHit: identifier alias "tidx_blks_hit" (Sql.Types.nullable Sql.Types.i64), @@ -2272,7 +2272,7 @@ pgStatioSysTables = { schema: "pg_catalog", name: "pg_statio_sys_tables", alias: "psst", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), tidxBlksHit: identifier alias "tidx_blks_hit" (Sql.Types.nullable Sql.Types.i64), @@ -2291,7 +2291,7 @@ pgStatioUserTables = { schema: "pg_catalog", name: "pg_statio_user_tables", alias: "psut", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), tidxBlksHit: identifier alias "tidx_blks_hit" (Sql.Types.nullable Sql.Types.i64), @@ -2310,7 +2310,7 @@ pgStatAllIndexes = { schema: "pg_catalog", name: "pg_stat_all_indexes", alias: "psai", - columns: \alias -> { + columns: |alias| { indexrelname: identifier alias "indexrelname" (Sql.Types.nullable Sql.Types.str), relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), @@ -2326,7 +2326,7 @@ pgStatSysIndexes = { schema: "pg_catalog", name: "pg_stat_sys_indexes", alias: "pssi", - columns: \alias -> { + columns: |alias| { indexrelname: identifier alias "indexrelname" (Sql.Types.nullable Sql.Types.str), relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), @@ -2342,7 +2342,7 @@ pgStatUserIndexes = { schema: "pg_catalog", name: "pg_stat_user_indexes", alias: "psui", - columns: \alias -> { + columns: |alias| { indexrelname: identifier alias "indexrelname" (Sql.Types.nullable Sql.Types.str), relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), @@ -2358,7 +2358,7 @@ pgStatioAllIndexes = { schema: "pg_catalog", name: "pg_statio_all_indexes", alias: "psai", - columns: \alias -> { + columns: |alias| { indexrelname: identifier alias "indexrelname" (Sql.Types.nullable Sql.Types.str), relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), @@ -2373,7 +2373,7 @@ pgStatioSysIndexes = { schema: "pg_catalog", name: "pg_statio_sys_indexes", alias: "pssi", - columns: \alias -> { + columns: |alias| { indexrelname: identifier alias "indexrelname" (Sql.Types.nullable Sql.Types.str), relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), @@ -2388,7 +2388,7 @@ pgStatioUserIndexes = { schema: "pg_catalog", name: "pg_statio_user_indexes", alias: "psui", - columns: \alias -> { + columns: |alias| { indexrelname: identifier alias "indexrelname" (Sql.Types.nullable Sql.Types.str), relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), @@ -2403,7 +2403,7 @@ pgStatioAllSequences = { schema: "pg_catalog", name: "pg_statio_all_sequences", alias: "psas", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), blksHit: identifier alias "blks_hit" (Sql.Types.nullable Sql.Types.i64), @@ -2416,7 +2416,7 @@ pgStatioSysSequences = { schema: "pg_catalog", name: "pg_statio_sys_sequences", alias: "psss", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), blksHit: identifier alias "blks_hit" (Sql.Types.nullable Sql.Types.i64), @@ -2429,7 +2429,7 @@ pgStatioUserSequences = { schema: "pg_catalog", name: "pg_statio_user_sequences", alias: "psus", - columns: \alias -> { + columns: |alias| { relname: identifier alias "relname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), blksHit: identifier alias "blks_hit" (Sql.Types.nullable Sql.Types.i64), @@ -2442,7 +2442,7 @@ pgStatSubscription = { schema: "pg_catalog", name: "pg_stat_subscription", alias: "pss", - columns: \alias -> { + columns: |alias| { subname: identifier alias "subname" (Sql.Types.nullable Sql.Types.str), pid: identifier alias "pid" (Sql.Types.nullable Sql.Types.i32), relid: identifier alias "relid" (Sql.Types.nullable Sql.Types.i32), @@ -2459,7 +2459,7 @@ pgStatSsl = { schema: "pg_catalog", name: "pg_stat_ssl", alias: "pss", - columns: \alias -> { + columns: |alias| { ssl: identifier alias "ssl" (Sql.Types.nullable Sql.Types.bool), bits: identifier alias "bits" (Sql.Types.nullable Sql.Types.i32), pid: identifier alias "pid" (Sql.Types.nullable Sql.Types.i32), @@ -2475,7 +2475,7 @@ pgStatGssapi = { schema: "pg_catalog", name: "pg_stat_gssapi", alias: "psg", - columns: \alias -> { + columns: |alias| { encrypted: identifier alias "encrypted" (Sql.Types.nullable Sql.Types.bool), gssAuthenticated: identifier alias "gss_authenticated" (Sql.Types.nullable Sql.Types.bool), pid: identifier alias "pid" (Sql.Types.nullable Sql.Types.i32), @@ -2487,7 +2487,7 @@ pgReplicationSlots = { schema: "pg_catalog", name: "pg_replication_slots", alias: "prs", - columns: \alias -> { + columns: |alias| { twoPhase: identifier alias "two_phase" (Sql.Types.nullable Sql.Types.bool), active: identifier alias "active" (Sql.Types.nullable Sql.Types.bool), temporary: identifier alias "temporary" (Sql.Types.nullable Sql.Types.bool), @@ -2510,7 +2510,7 @@ pgStatReplicationSlots = { schema: "pg_catalog", name: "pg_stat_replication_slots", alias: "psrs", - columns: \alias -> { + columns: |alias| { totalBytes: identifier alias "total_bytes" (Sql.Types.nullable Sql.Types.i64), totalTxns: identifier alias "total_txns" (Sql.Types.nullable Sql.Types.i64), streamBytes: identifier alias "stream_bytes" (Sql.Types.nullable Sql.Types.i64), @@ -2528,7 +2528,7 @@ pgStatDatabase = { schema: "pg_catalog", name: "pg_stat_database", alias: "psd", - columns: \alias -> { + columns: |alias| { datname: identifier alias "datname" (Sql.Types.nullable Sql.Types.str), sessionsKilled: identifier alias "sessions_killed" (Sql.Types.nullable Sql.Types.i64), sessionsFatal: identifier alias "sessions_fatal" (Sql.Types.nullable Sql.Types.i64), @@ -2564,7 +2564,7 @@ pgStatDatabaseConflicts = { schema: "pg_catalog", name: "pg_stat_database_conflicts", alias: "psdc", - columns: \alias -> { + columns: |alias| { datname: identifier alias "datname" (Sql.Types.nullable Sql.Types.str), conflDeadlock: identifier alias "confl_deadlock" (Sql.Types.nullable Sql.Types.i64), conflBufferpin: identifier alias "confl_bufferpin" (Sql.Types.nullable Sql.Types.i64), @@ -2579,7 +2579,7 @@ pgStatUserFunctions = { schema: "pg_catalog", name: "pg_stat_user_functions", alias: "psuf", - columns: \alias -> { + columns: |alias| { funcname: identifier alias "funcname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), calls: identifier alias "calls" (Sql.Types.nullable Sql.Types.i64), @@ -2593,7 +2593,7 @@ pgStatXactUserFunctions = { schema: "pg_catalog", name: "pg_stat_xact_user_functions", alias: "psxuf", - columns: \alias -> { + columns: |alias| { funcname: identifier alias "funcname" (Sql.Types.nullable Sql.Types.str), schemaname: identifier alias "schemaname" (Sql.Types.nullable Sql.Types.str), calls: identifier alias "calls" (Sql.Types.nullable Sql.Types.i64), @@ -2607,7 +2607,7 @@ pgStatArchiver = { schema: "pg_catalog", name: "pg_stat_archiver", alias: "psa", - columns: \alias -> { + columns: |alias| { failedCount: identifier alias "failed_count" (Sql.Types.nullable Sql.Types.i64), archivedCount: identifier alias "archived_count" (Sql.Types.nullable Sql.Types.i64), lastFailedWal: identifier alias "last_failed_wal" (Sql.Types.nullable Sql.Types.str), @@ -2622,7 +2622,7 @@ pgStatBgwriter = { schema: "pg_catalog", name: "pg_stat_bgwriter", alias: "psb", - columns: \alias -> { + columns: |alias| { buffersAlloc: identifier alias "buffers_alloc" (Sql.Types.nullable Sql.Types.i64), buffersBackendFsync: identifier alias "buffers_backend_fsync" (Sql.Types.nullable Sql.Types.i64), buffersBackend: identifier alias "buffers_backend" (Sql.Types.nullable Sql.Types.i64), @@ -2641,7 +2641,7 @@ pgStatWal = { schema: "pg_catalog", name: "pg_stat_wal", alias: "psw", - columns: \alias -> { + columns: |alias| { walSync: identifier alias "wal_sync" (Sql.Types.nullable Sql.Types.i64), walWrite: identifier alias "wal_write" (Sql.Types.nullable Sql.Types.i64), walBuffersFull: identifier alias "wal_buffers_full" (Sql.Types.nullable Sql.Types.i64), @@ -2658,7 +2658,7 @@ pgStatProgressAnalyze = { schema: "pg_catalog", name: "pg_stat_progress_analyze", alias: "pspa", - columns: \alias -> { + columns: |alias| { datname: identifier alias "datname" (Sql.Types.nullable Sql.Types.str), childTablesDone: identifier alias "child_tables_done" (Sql.Types.nullable Sql.Types.i64), childTablesTotal: identifier alias "child_tables_total" (Sql.Types.nullable Sql.Types.i64), @@ -2678,7 +2678,7 @@ pgStatProgressVacuum = { schema: "pg_catalog", name: "pg_stat_progress_vacuum", alias: "pspv", - columns: \alias -> { + columns: |alias| { datname: identifier alias "datname" (Sql.Types.nullable Sql.Types.str), numDeadTuples: identifier alias "num_dead_tuples" (Sql.Types.nullable Sql.Types.i64), maxDeadTuples: identifier alias "max_dead_tuples" (Sql.Types.nullable Sql.Types.i64), @@ -2697,7 +2697,7 @@ pgStatProgressCluster = { schema: "pg_catalog", name: "pg_stat_progress_cluster", alias: "pspc", - columns: \alias -> { + columns: |alias| { datname: identifier alias "datname" (Sql.Types.nullable Sql.Types.str), indexRebuildCount: identifier alias "index_rebuild_count" (Sql.Types.nullable Sql.Types.i64), heapBlksScanned: identifier alias "heap_blks_scanned" (Sql.Types.nullable Sql.Types.i64), @@ -2717,7 +2717,7 @@ pgStatProgressCreateIndex = { schema: "pg_catalog", name: "pg_stat_progress_create_index", alias: "pspci", - columns: \alias -> { + columns: |alias| { datname: identifier alias "datname" (Sql.Types.nullable Sql.Types.str), partitionsDone: identifier alias "partitions_done" (Sql.Types.nullable Sql.Types.i64), partitionsTotal: identifier alias "partitions_total" (Sql.Types.nullable Sql.Types.i64), @@ -2741,7 +2741,7 @@ pgStatProgressBasebackup = { schema: "pg_catalog", name: "pg_stat_progress_basebackup", alias: "pspb", - columns: \alias -> { + columns: |alias| { tablespacesStreamed: identifier alias "tablespaces_streamed" (Sql.Types.nullable Sql.Types.i64), tablespacesTotal: identifier alias "tablespaces_total" (Sql.Types.nullable Sql.Types.i64), backupStreamed: identifier alias "backup_streamed" (Sql.Types.nullable Sql.Types.i64), @@ -2755,7 +2755,7 @@ pgStatProgressCopy = { schema: "pg_catalog", name: "pg_stat_progress_copy", alias: "pspc", - columns: \alias -> { + columns: |alias| { datname: identifier alias "datname" (Sql.Types.nullable Sql.Types.str), tuplesExcluded: identifier alias "tuples_excluded" (Sql.Types.nullable Sql.Types.i64), tuplesProcessed: identifier alias "tuples_processed" (Sql.Types.nullable Sql.Types.i64), @@ -2773,7 +2773,7 @@ pgUserMappings = { schema: "pg_catalog", name: "pg_user_mappings", alias: "pum", - columns: \alias -> { + columns: |alias| { usename: identifier alias "usename" (Sql.Types.nullable Sql.Types.str), srvname: identifier alias "srvname" (Sql.Types.nullable Sql.Types.str), umuser: identifier alias "umuser" (Sql.Types.nullable Sql.Types.i32), @@ -2787,7 +2787,7 @@ pgStatSubscriptionStats = { schema: "pg_catalog", name: "pg_stat_subscription_stats", alias: "psss", - columns: \alias -> { + columns: |alias| { subname: identifier alias "subname" (Sql.Types.nullable Sql.Types.str), syncErrorCount: identifier alias "sync_error_count" (Sql.Types.nullable Sql.Types.i64), applyErrorCount: identifier alias "apply_error_count" (Sql.Types.nullable Sql.Types.i64), @@ -2806,7 +2806,7 @@ pgLargeobject = { schema: "pg_catalog", name: "pg_largeobject", alias: "pl", - columns: \alias -> { + columns: |alias| { data: identifier alias "data" (Sql.Types.unsupported "bytea"), pageno: identifier alias "pageno" pgLargeobjectPageno, loid: identifier alias "loid" pgLargeobjectLoid, diff --git a/sql-cli/src/Schema.roc b/sql-cli/src/Schema.roc index 8b4466f..5ea91b4 100644 --- a/sql-cli/src/Schema.roc +++ b/sql-cli/src/Schema.roc @@ -4,9 +4,9 @@ module [ Table, Column, new, - getName, - getTables, - primaryColumn, + get_name, + get_tables, + primary_column, ] Nullable a : [Null, NotNull a] @@ -15,7 +15,7 @@ Schema := { name : Str, tables : List Table, references : Dict ColumnId ColumnId, - primaryColumns : Dict ColumnId { tableName : Str, columnName : Str }, + primary_columns : Dict ColumnId { table_name : Str, column_name : Str }, } ColumnId : (I32, I16) @@ -30,191 +30,210 @@ Table : { Column : { num : I16, name : Str, - dataType : Str, - typeCategory : Str, - elemDataType : Nullable Str, - isNullable : Bool, + data_type : Str, + type_category : Str, + elem_data_type : Nullable Str, + is_nullable : Bool, } Constraint : { type : Str, columns : List I16, - foreignTable : I32, # pg sets this to 0 if not foreign key - foreignColumns : List I16, + foreign_table : I32, # pg sets this to 0 if not foreign key + foreign_columns : List I16, } new : Str, List Table -> Schema -new = \schemaName, tables -> - tablesLen : U64 - tablesLen = List.len tables +new = |schema_name, tables| + tables_len : U64 + tables_len = List.len(tables) keys : { - primaryColumns : Dict ColumnId { tableName : Str, columnName : Str }, + primary_columns : Dict ColumnId { table_name : Str, column_name : Str }, references : Dict ColumnId ColumnId, } keys = - stateTable, table <- List.walk tables { - primaryColumns: Dict.withCapacity tablesLen, - references: Dict.withCapacity (tablesLen * 4), - } - state, constraint <- List.walk table.constraints stateTable - - when constraint.type is - "p" -> - newPrimaryColumns = - constraint.columns - |> List.map \colNum -> - - names = - when List.findFirst table.columns \col -> col.num == colNum is - Ok { name } -> - { - tableName: table.name, - columnName: name, - } - - Err NotFound -> - { - tableName: "$(Num.toStr table.id)", - columnName: "$(Num.toStr colNum)", - } - - ((table.id, colNum), names) - |> Dict.fromList - - { state & primaryColumns: Dict.insertAll state.primaryColumns newPrimaryColumns } - - "f" -> - newReferences = - constraint.columns - |> List.map2 constraint.foreignColumns \colNum, foreignColumn -> - ((table.id, colNum), (constraint.foreignTable, foreignColumn)) - |> Dict.fromList - - { state & references: Dict.insertAll state.references newReferences } - - _ -> - state - - @Schema { - name: schemaName, - tables, - primaryColumns: keys.primaryColumns, - references: keys.references, - } + List.walk( + tables, + { + primary_columns: Dict.with_capacity(tables_len), + references: Dict.with_capacity((tables_len * 4)), + }, + |state_table, table| + List.walk( + table.constraints, + state_table, + |state, constraint| + when constraint.type is + "p" -> + new_primary_columns = + constraint.columns + |> List.map( + |col_num| + + names = + when List.find_first(table.columns, |col| col.num == col_num) is + Ok({ name }) -> + { + table_name: table.name, + column_name: name, + } + + Err(NotFound) -> + { + table_name: "${Num.to_str(table.id)}", + column_name: "${Num.to_str(col_num)}", + } + + ((table.id, col_num), names), + ) + |> Dict.from_list + + { state & primary_columns: Dict.insert_all(state.primary_columns, new_primary_columns) } + + "f" -> + new_references = + constraint.columns + |> List.map2( + constraint.foreign_columns, + |col_num, foreign_column| + ((table.id, col_num), (constraint.foreign_table, foreign_column)), + ) + |> Dict.from_list + + { state & references: Dict.insert_all(state.references, new_references) } + + _ -> + state, + ), + ) + @Schema( + { + name: schema_name, + tables, + primary_columns: keys.primary_columns, + references: keys.references, + }, + ) -getTables : Schema -> List Table -getTables = \@Schema schema -> schema.tables +get_tables : Schema -> List Table +get_tables = |@Schema(schema)| schema.tables -getName : Schema -> Str -getName = \@Schema schema -> schema.name +get_name : Schema -> Str +get_name = |@Schema(schema)| schema.name ## Recursively find the final column referenced by another column. ## ## Returns itself if it's part of a primary key and no foreign key. -primaryColumn : Schema, +primary_column : + Schema, ColumnId -> Result { id : ColumnId, - tableName : Str, - columnName : Str, + table_name : Str, + column_name : Str, } [KeyNotFound] -primaryColumn = \@Schema schema, column -> - when Dict.get schema.references column is - Ok refColumn -> - primaryColumn (@Schema schema) refColumn - - Err KeyNotFound -> - Dict.get schema.primaryColumns column - |> Result.map \names -> { - id: column, - tableName: names.tableName, - columnName: names.columnName, - } - -expect primaryColumn testSchema (1, 1) == Ok { id: (1, 1), tableName: "users", columnName: "id" } -expect primaryColumn testSchema (1, 2) == Err KeyNotFound -expect primaryColumn testSchema (2, 1) == Ok { id: (2, 1), tableName: "posts", columnName: "id" } -expect primaryColumn testSchema (2, 2) == Ok { id: (1, 1), tableName: "users", columnName: "id" } -expect primaryColumn testSchema (2, 3) == Err KeyNotFound - -testSchema = - new "public" [ - { - id: 1, - name: "users", - columns: [ - { - num: 1, - name: "id", - dataType: "int4", - typeCategory: "N", - elemDataType: Null, - isNullable: Bool.false, - }, - { - num: 2, - name: "name", - dataType: "text", - typeCategory: "S", - elemDataType: Null, - isNullable: Bool.false, +primary_column = |@Schema(schema), column| + when Dict.get(schema.references, column) is + Ok(ref_column) -> + primary_column(@Schema(schema), ref_column) + + Err(KeyNotFound) -> + Dict.get(schema.primary_columns, column) + |> Result.map_ok( + |names| { + id: column, + table_name: names.table_name, + column_name: names.column_name, }, - ], - constraints: [ - { - type: "p", - columns: [1], - foreignTable: 0, - foreignColumns: [], - }, - ], - }, - { - id: 2, - name: "posts", - columns: [ - { - num: 1, - name: "id", - dataType: "int4", - typeCategory: "N", - elemDataType: Null, - isNullable: Bool.false, - }, - { - num: 2, - name: "user_id", - dataType: "int4", - typeCategory: "N", - elemDataType: Null, - isNullable: Bool.false, - }, - { - num: 3, - name: "title", - dataType: "text", - typeCategory: "S", - elemDataType: Null, - isNullable: Bool.false, - }, - ], - constraints: [ - { - type: "p", - columns: [1], - foreignTable: 0, - foreignColumns: [], - }, - { - type: "f", - columns: [2], - foreignTable: 1, - foreignColumns: [1], - }, - ], - }, - ] + ) + +expect primary_column(test_schema, (1, 1)) == Ok({ id: (1, 1), table_name: "users", column_name: "id" }) +expect primary_column(test_schema, (1, 2)) == Err(KeyNotFound) +expect primary_column(test_schema, (2, 1)) == Ok({ id: (2, 1), table_name: "posts", column_name: "id" }) +expect primary_column(test_schema, (2, 2)) == Ok({ id: (1, 1), table_name: "users", column_name: "id" }) +expect primary_column(test_schema, (2, 3)) == Err(KeyNotFound) + +test_schema = + new( + "public", + [ + { + id: 1, + name: "users", + columns: [ + { + num: 1, + name: "id", + data_type: "int4", + type_category: "N", + elem_data_type: Null, + is_nullable: Bool.false, + }, + { + num: 2, + name: "name", + data_type: "text", + type_category: "S", + elem_data_type: Null, + is_nullable: Bool.false, + }, + ], + constraints: [ + { + type: "p", + columns: [1], + foreign_table: 0, + foreign_columns: [], + }, + ], + }, + { + id: 2, + name: "posts", + columns: [ + { + num: 1, + name: "id", + data_type: "int4", + type_category: "N", + elem_data_type: Null, + is_nullable: Bool.false, + }, + { + num: 2, + name: "user_id", + data_type: "int4", + type_category: "N", + elem_data_type: Null, + is_nullable: Bool.false, + }, + { + num: 3, + name: "title", + data_type: "text", + type_category: "S", + elem_data_type: Null, + is_nullable: Bool.false, + }, + ], + constraints: [ + { + type: "p", + columns: [1], + foreign_table: 0, + foreign_columns: [], + }, + { + type: "f", + columns: [2], + foreign_table: 1, + foreign_columns: [1], + }, + ], + }, + ], + ) diff --git a/src/Batch.roc b/src/Batch.roc index fe0a430..306e617 100644 --- a/src/Batch.roc +++ b/src/Batch.roc @@ -4,14 +4,15 @@ module [ succeed, with, params, - reuseName, + reuse_name, sequence, ] import Cmd exposing [Cmd] import Pg.Result exposing [CmdResult] -Batch a err := Params { +Batch a err := + Params { decode : List CmdResult -> Result @@ -27,129 +28,141 @@ Batch a err := Params { Params a : { commands : List BatchedCmd, - seenSql : SeenSql, + seen_sql : SeenSql, }a SeenSql : Dict Str { index : U64, reused : Bool } BatchedCmd : Cmd.Params {} [ReuseSql U64] -reuseName : U64 -> Str -reuseName = \index -> - indexStr = Num.toStr index - "b[$(indexStr)]" - -succeed : a -> Batch a err -succeed = \value -> - @Batch { - commands: List.withCapacity 5, - seenSql: Dict.withCapacity 5, - decode: \rest -> Ok { value, rest }, - } +reuse_name : U64 -> Str +reuse_name = |index| + index_str = Num.to_str(index) + "b[${index_str}]" + +succeed : _ -> Batch _ _ +succeed = |value| + @Batch( + { + commands: List.with_capacity(5), + seen_sql: Dict.with_capacity(5), + decode: |rest| Ok({ value, rest }), + }, + ) with : Batch (a -> b) err, Cmd a err -> Batch b err -with = \@Batch batch, cmd -> - { seenSql, newCmd, newIndex } = addCmd batch cmd - commands = batch.commands |> List.append newCmd - - decode = \results -> - { value: fn, rest } <- batch.decode results |> Result.try - - when rest is - [next, ..] -> - a <- Cmd.decode next cmd - |> Result.mapErr ExpectErr - |> Result.try - - Ok { - value: fn a, - rest: List.dropFirst rest 1, - } - - _ -> - Err (MissingCmdResult newIndex) - - @Batch { commands, seenSql, decode } +with = |@Batch(batch), cmd| + { seen_sql, new_cmd, new_index } = add_cmd(batch, cmd) + commands = batch.commands |> List.append(new_cmd) + + decode = |results| + batch.decode(results) + |> Result.try( + |{ value: fn, rest }| + when rest is + [next, ..] -> + Cmd.decode(next, cmd) + |> Result.map_err(ExpectErr) + |> Result.try( + |a| + Ok( + { + value: fn(a), + rest: List.drop_first(rest, 1), + }, + ), + ) + + _ -> + Err(MissingCmdResult(new_index)), + ) + + @Batch({ commands, seen_sql, decode }) sequence : List (Cmd a err) -> Batch (List a) err -sequence = \cmds -> - count = List.len cmds +sequence = |cmds| + count = List.len(cmds) init = { - commands: List.withCapacity count, - seenSql: Dict.withCapacity (smallest 10 count), + commands: List.with_capacity(count), + seen_sql: Dict.with_capacity(smallest(10, count)), } batch = cmds - |> List.walk init \b, cmd -> - { seenSql, newCmd } = addCmd b cmd - - { - seenSql, - commands: b.commands |> List.append newCmd, - } - - decode = \results -> - List.map2 results cmds Cmd.decode - |> List.mapTry \r -> r - |> Result.map \value -> { value, rest: [] } - |> Result.mapErr ExpectErr - - @Batch { - commands: batch.commands, - seenSql: batch.seenSql, - decode, - } + |> List.walk( + init, + |b, cmd| + { seen_sql, new_cmd } = add_cmd(b, cmd) + + { + seen_sql, + commands: b.commands |> List.append(new_cmd), + }, + ) + + decode = |results| + List.map2(results, cmds, Cmd.decode) + |> List.map_try(|r| r) + |> Result.map_ok(|value| { value, rest: [] }) + |> Result.map_err(ExpectErr) + + @Batch( + { + commands: batch.commands, + seen_sql: batch.seen_sql, + decode, + }, + ) smallest : Num a, Num a -> Num a -smallest = \a, b -> +smallest = |a, b| if a < b then a else b -addCmd : Params *, Cmd * * -> { seenSql : SeenSql, newCmd : BatchedCmd, newIndex : U64 } -addCmd = \batch, cmd -> - cmdParams = Cmd.params cmd - newIndex = List.len batch.commands +add_cmd : Params *, Cmd * * -> { seen_sql : SeenSql, new_cmd : BatchedCmd, new_index : U64 } +add_cmd = |batch, cmd| + cmd_params = Cmd.params(cmd) + new_index = List.len(batch.commands) - when cmdParams.kind is - SqlCmd sql -> - when Dict.get batch.seenSql sql is - Err KeyNotFound -> + when cmd_params.kind is + SqlCmd(sql) -> + when Dict.get(batch.seen_sql, sql) is + Err(KeyNotFound) -> entry = { - index: newIndex, + index: new_index, reused: Bool.false, } - seenSql = batch.seenSql |> Dict.insert sql entry - newCmd = SqlCmd sql |> batchedCmd cmdParams + seen_sql = batch.seen_sql |> Dict.insert(sql, entry) + new_cmd = SqlCmd(sql) |> batched_cmd(cmd_params) - { seenSql, newCmd, newIndex } + { seen_sql, new_cmd, new_index } - Ok { index, reused } -> - seenSql = + Ok({ index, reused }) -> + seen_sql = if reused then - batch.seenSql + batch.seen_sql else entry = { index, reused: Bool.true } - batch.seenSql |> Dict.insert sql entry + batch.seen_sql |> Dict.insert(sql, entry) - newCmd = ReuseSql index |> batchedCmd cmdParams + new_cmd = ReuseSql(index) |> batched_cmd(cmd_params) - { seenSql, newCmd, newIndex } + { seen_sql, new_cmd, new_index } - PreparedCmd prep -> - newCmd = PreparedCmd prep |> batchedCmd cmdParams + PreparedCmd(prep) -> + new_cmd = PreparedCmd(prep) |> batched_cmd(cmd_params) - { seenSql: batch.seenSql, newCmd, newIndex } + { seen_sql: batch.seen_sql, new_cmd, new_index } -batchedCmd : Cmd.Kind [ReuseSql U64], Cmd.Params {} [] -> BatchedCmd -batchedCmd = \kind, cmdParams -> { +batched_cmd : Cmd.Kind [ReuseSql U64], Cmd.Params {} [] -> BatchedCmd +batched_cmd = |kind, cmd_params| { kind, - bindings: cmdParams.bindings, - limit: cmdParams.limit, + bindings: cmd_params.bindings, + limit: cmd_params.limit, } params : Batch a err -> Params _ -params = \@Batch batch -> batch +params = |@Batch(batch)| batch diff --git a/src/Bytes/Decode.roc b/src/Bytes/Decode.roc index a7aec51..d4e3673 100644 --- a/src/Bytes/Decode.roc +++ b/src/Bytes/Decode.roc @@ -1,28 +1,27 @@ module [ Decode, + Step, + await, + bool, + c_str, decode, - u8, - u16, - u32, - u64, - i8, + fail, i16, i32, i64, - cStr, - bool, - take, + i8, + loop, map, succeed, - fail, - await, - Step, - loop, + take, + u16, + u32, + u64, + u8, ] -import Bool exposing [Bool] - -Decode value err := List U8 +Decode value err := + List U8 -> Result { decoded : value, @@ -31,178 +30,203 @@ Decode value err := List U8 err decode : List U8, Decode value err -> Result value err -decode = \bytes, @Decode decoder -> - decoder bytes - |> Result.map .decoded +decode = |bytes, @Decode(decode_decoder)| + decode_decoder(bytes) + |> Result.map_ok(.decoded) # Unsigned Integers u8 : Decode U8 [UnexpectedEnd] u8 = - bytes <- @Decode - - when bytes is - [byte, ..] -> - Ok { decoded: byte, remaining: List.dropFirst bytes 1 } + @Decode( + |bytes| + when bytes is + [byte, ..] -> + Ok({ decoded: byte, remaining: List.drop_first(bytes, 1) }) - _ -> - Err UnexpectedEnd + _ -> + Err(UnexpectedEnd), + ) u16 : Decode U16 [UnexpectedEnd] u16 = - bytes <- @Decode + @Decode( + |bytes| + when bytes is + [b0, b1, ..] -> + value = + Num.shift_left_by(Num.to_u16(b0), 8) + |> Num.bitwise_or(Num.to_u16(b1)) - when bytes is - [b0, b1, ..] -> - value = - Num.shiftLeftBy (Num.toU16 b0) 8 - |> Num.bitwiseOr (Num.toU16 b1) + Ok({ decoded: value, remaining: List.drop_first(bytes, 2) }) - Ok { decoded: value, remaining: List.dropFirst bytes 2 } - - _ -> - Err UnexpectedEnd + _ -> + Err(UnexpectedEnd), + ) u32 : Decode U32 [UnexpectedEnd] u32 = - bytes <- @Decode - - when bytes is - [b0, b1, b2, b3, ..] -> - value = - Num.shiftLeftBy (Num.toU32 b0) 24 - |> Num.bitwiseOr (Num.shiftLeftBy (Num.toU32 b1) 16) - |> Num.bitwiseOr (Num.shiftLeftBy (Num.toU32 b2) 8) - |> Num.bitwiseOr (Num.toU32 b3) - - Ok { decoded: value, remaining: List.dropFirst bytes 4 } - - _ -> - Err UnexpectedEnd + @Decode( + |bytes| + when bytes is + [b0, b1, b2, b3, ..] -> + value = + Num.shift_left_by(Num.to_u32(b0), 24) + |> Num.bitwise_or(Num.shift_left_by(Num.to_u32(b1), 16)) + |> Num.bitwise_or(Num.shift_left_by(Num.to_u32(b2), 8)) + |> Num.bitwise_or(Num.to_u32(b3)) + + Ok({ decoded: value, remaining: List.drop_first(bytes, 4) }) + + _ -> + Err(UnexpectedEnd), + ) u64 : Decode U64 [UnexpectedEnd] u64 = - bytes <- @Decode - - when bytes is - [b0, b1, b2, b3, b4, b5, b6, b7, ..] -> - value = - Num.shiftLeftBy (Num.toU64 b0) 56 - |> Num.bitwiseOr (Num.shiftLeftBy (Num.toU64 b1) 48) - |> Num.bitwiseOr (Num.shiftLeftBy (Num.toU64 b2) 40) - |> Num.bitwiseOr (Num.shiftLeftBy (Num.toU64 b3) 32) - |> Num.bitwiseOr (Num.shiftLeftBy (Num.toU64 b4) 24) - |> Num.bitwiseOr (Num.shiftLeftBy (Num.toU64 b5) 16) - |> Num.bitwiseOr (Num.shiftLeftBy (Num.toU64 b6) 8) - |> Num.bitwiseOr (Num.toU64 b7) - - Ok { decoded: value, remaining: List.dropFirst bytes 8 } - - _ -> - Err UnexpectedEnd + @Decode( + |bytes| + when bytes is + [b0, b1, b2, b3, b4, b5, b6, b7, ..] -> + value = + Num.shift_left_by(Num.to_u64(b0), 56) + |> Num.bitwise_or(Num.shift_left_by(Num.to_u64(b1), 48)) + |> Num.bitwise_or(Num.shift_left_by(Num.to_u64(b2), 40)) + |> Num.bitwise_or(Num.shift_left_by(Num.to_u64(b3), 32)) + |> Num.bitwise_or(Num.shift_left_by(Num.to_u64(b4), 24)) + |> Num.bitwise_or(Num.shift_left_by(Num.to_u64(b5), 16)) + |> Num.bitwise_or(Num.shift_left_by(Num.to_u64(b6), 8)) + |> Num.bitwise_or(Num.to_u64(b7)) + + Ok({ decoded: value, remaining: List.drop_first(bytes, 8) }) + + _ -> + Err(UnexpectedEnd), + ) # Signed Integers i8 : Decode I8 [UnexpectedEnd] i8 = - map u8 Num.toI8 + map(u8, Num.to_i8) i16 : Decode I16 [UnexpectedEnd] i16 = - map u16 Num.toI16 + map(u16, Num.to_i16) i32 : Decode I32 [UnexpectedEnd] i32 = - map u32 Num.toI32 + map(u32, Num.to_i32) i64 : Decode I64 [UnexpectedEnd] i64 = - map u64 Num.toI64 + map(u64, Num.to_i64) take : U64, (List U8 -> value) -> Decode value [UnexpectedEnd] -take = \count, callback -> - bytes <- @Decode - { before, others } = List.split bytes count +take = |count, callback| + @Decode( + |bytes| + { before, others } = List.split_at(bytes, count) - if List.len before == count then - Ok { decoded: callback before, remaining: others } - else - Err UnexpectedEnd + if List.len(before) == count then + Ok({ decoded: callback(before), remaining: others }) + else + Err(UnexpectedEnd), + ) # Strings -cStr : Decode Str [TerminatorNotFound, Utf8DecodeError _] -cStr = - bytes <- @Decode - - when List.splitFirst bytes 0 is - Ok { before, after } -> - when Str.fromUtf8 before is - Ok value -> - Ok { decoded: value, remaining: after } +c_str : Decode Str [TerminatorNotFound, Utf8DecodeError _] +c_str = + @Decode( + |bytes| + when List.split_first(bytes, 0) is + Ok({ before, after }) -> + when Str.from_utf8(before) is + Ok(value) -> + Ok({ decoded: value, remaining: after }) - Err err -> - Err (Utf8DecodeError err) + Err(err) -> + Err(Utf8DecodeError(err)) - Err _ -> - Err TerminatorNotFound + Err(_) -> + Err(TerminatorNotFound), + ) # Bools bool : Decode Bool [UnexpectedEnd] bool = - bytes <- @Decode + @Decode( + |bytes| + when bytes is + [byte, ..] -> + Ok({ decoded: byte == 1, remaining: List.drop_first(bytes, 1) }) - when bytes is - [byte, ..] -> - Ok { decoded: byte == 1, remaining: List.dropFirst bytes 1 } - - _ -> - Err UnexpectedEnd + _ -> + Err(UnexpectedEnd), + ) # Mapping succeed : value -> Decode value err -succeed = \value -> - bytes <- @Decode - Ok { decoded: value, remaining: bytes } +succeed = |value| + @Decode( + |bytes| + Ok({ decoded: value, remaining: bytes }), + ) fail : err -> Decode value err -fail = \err -> - _ <- @Decode - Err err +fail = |err| + @Decode( + |_| + Err(err), + ) await : Decode a err, (a -> Decode b err) -> Decode b err -await = \@Decode decoderA, callback -> - bytes <- @Decode - a <- Result.try (decoderA bytes) - (@Decode decoderB) = callback a.decoded - decoderB a.remaining +await = |@Decode(decoder_a), callback| + @Decode( + |bytes| + Result.try( + decoder_a(bytes), + |a| + @Decode(decoder_b) = callback(a.decoded) + decoder_b(a.remaining), + ), + ) map : Decode a err, (a -> b) -> Decode b err -map = \@Decode decoder, mapFn -> - bytes <- @Decode - { decoded, remaining } <- Result.map (decoder bytes) - { decoded: mapFn decoded, remaining } +map = |@Decode(map_decoder), map_fn| + @Decode( + |bytes| + Result.map_ok( + map_decoder(bytes), + |{ decoded, remaining }| + { decoded: map_fn(decoded), remaining }, + ), + ) # Loop Step state a : [Loop state, Done a] loop : state, (state -> Decode (Step state a) err) -> Decode a err -loop = \state, step -> - bytes <- @Decode - - loopHelp step state bytes - -loopHelp = \step, state, bytes -> - (@Decode decoder) = step state - - { decoded, remaining } <- Result.try (decoder bytes) - - when decoded is - Loop newState -> - loopHelp step newState remaining - - Done result -> - Ok { decoded: result, remaining } +loop = |state, step| + @Decode( + |bytes| + loop_help(step, state, bytes), + ) + +loop_help = |step, state, bytes| + @Decode(loop_help_decoder) = step(state) + + Result.try( + loop_help_decoder(bytes), + |{ decoded, remaining }| + when decoded is + Loop(new_state) -> + loop_help(step, new_state, remaining) + + Done(result) -> + Ok({ decoded: result, remaining }), + ) diff --git a/src/Bytes/Encode.roc b/src/Bytes/Encode.roc index 69d3e84..7f5808e 100644 --- a/src/Bytes/Encode.roc +++ b/src/Bytes/Encode.roc @@ -8,8 +8,8 @@ module [ i16, i32, i64, - cStr, - nullTerminate, + c_str, + null_terminate, ] sequence : List (List U8) -> List U8 @@ -24,62 +24,62 @@ u8 = u16 : U16 -> List U8 u16 = - sized 16 + sized(16) u32 : U32 -> List U8 u32 = - sized 32 + sized(32) u64 : U64 -> List U8 u64 = - sized 64 + sized(64) # Signed integers i8 : I8 -> List U8 -i8 = \value -> - [Num.toU8 value] +i8 = |value| + [Num.to_u8(value)] i16 : I16 -> List U8 i16 = - sized 16 + sized(16) i32 : I32 -> List U8 i32 = - sized 32 + sized(32) i64 : I64 -> List U8 i64 = - sized 64 + sized(64) sized : U8 -> (Int * -> List U8) -sized = \size -> - \value -> - sizedHelp value (size - 8) [] +sized = |size| + |value| + sized_help(value, (size - 8), []) -sizedHelp : Int *, U8, List U8 -> List U8 -sizedHelp = \value, offset, collected -> +sized_help : Int *, U8, List U8 -> List U8 +sized_help = |value, offset, collected| part = value - |> Num.shiftRightBy offset - |> Num.toU8 + |> Num.shift_right_by(offset) + |> Num.to_u8 added = - List.append collected part + List.append(collected, part) if offset == 0 then added else - sizedHelp value (offset - 8) added + sized_help(value, (offset - 8), added) # Strings -cStr : Str -> List U8 -cStr = \value -> +c_str : Str -> List U8 +c_str = |value| value - |> Str.toUtf8 - |> nullTerminate + |> Str.to_utf8 + |> null_terminate -nullTerminate : List U8 -> List U8 -nullTerminate = \bytes -> - List.append bytes 0 +null_terminate : List U8 -> List U8 +null_terminate = |bytes| + List.append(bytes, 0) diff --git a/src/Cmd.roc b/src/Cmd.roc index feefcf8..622bd0b 100644 --- a/src/Cmd.roc +++ b/src/Cmd.roc @@ -3,20 +3,20 @@ module [ Params, Limit, Kind, - fromSql, + from_sql, prepared, params, - withLimit, + with_limit, decode, - withDecode, + with_decode, map, bind, Binding, - encodeBindings, + encode_bindings, ] import Protocol.Frontend exposing [FormatCode] -import Protocol.Backend exposing [RowField] +import Protocol.Backend exposing [RowField, ParameterField] import Pg.Result exposing [CmdResult] Cmd a err := Params { decode : CmdResult -> Result a err } [] @@ -35,59 +35,66 @@ Kind k : [ { name : Str, fields : List RowField, + parameters : List ParameterField, }, ]k -fromSql : Str -> Cmd CmdResult [] -fromSql = \sql -> - new (SqlCmd sql) +from_sql : Str -> Cmd CmdResult [] +from_sql = |sql| + new(SqlCmd(sql)) -prepared : { name : Str, fields : List RowField } -> Cmd CmdResult [] -prepared = \prep -> - new (PreparedCmd prep) +prepared : { name : Str, fields : List RowField, parameters : List ParameterField } -> Cmd CmdResult [] +prepared = |prep| + new(PreparedCmd(prep)) new : Kind [] -> Cmd CmdResult [] -new = \kind -> - @Cmd { - kind, - limit: None, - bindings: [], - decode: Ok, - } +new = |kind| + @Cmd( + { + kind, + limit: None, + bindings: [], + decode: Ok, + }, + ) params : Cmd a err -> Params {} [] -params = \@Cmd { kind, bindings, limit } -> +params = |@Cmd({ kind, bindings, limit })| { kind, bindings, limit } -withLimit : Cmd a err, I32 -> Cmd a err -withLimit = \@Cmd cmd, limit -> - @Cmd { cmd & limit: Limit limit } +with_limit : Cmd a err, I32 -> Cmd a err +with_limit = |@Cmd(cmd), limit| + @Cmd({ cmd & limit: Limit(limit) }) decode : CmdResult, Cmd a err -> Result a err -decode = \r, @Cmd cmd -> - cmd.decode r - -withDecode : Cmd * *, (CmdResult -> Result a err) -> Cmd a err -withDecode = \@Cmd cmd, fn -> - @Cmd { - kind: cmd.kind, - limit: cmd.limit, - bindings: cmd.bindings, - decode: fn, - } +decode = |r, @Cmd(cmd)| + cmd.decode(r) + +with_decode : Cmd * *, (CmdResult -> Result a err) -> Cmd a err +with_decode = |@Cmd(cmd), fn| + @Cmd( + { + kind: cmd.kind, + limit: cmd.limit, + bindings: cmd.bindings, + decode: fn, + }, + ) map : Cmd a err, (a -> b) -> Cmd b err -map = \@Cmd cmd, fn -> - @Cmd { - kind: cmd.kind, - limit: cmd.limit, - bindings: cmd.bindings, - decode: \r -> cmd.decode r |> Result.map fn, - } +map = |@Cmd(cmd), fn| + @Cmd( + { + kind: cmd.kind, + limit: cmd.limit, + bindings: cmd.bindings, + decode: |r| cmd.decode(r) |> Result.map_ok(fn), + }, + ) bind : Cmd a err, List Binding -> Cmd a err -bind = \@Cmd cmd, bindings -> - @Cmd { cmd & bindings } +bind = |@Cmd(cmd), bindings| + @Cmd({ cmd & bindings }) Binding : [ Null, @@ -95,28 +102,33 @@ Binding : [ Binary (List U8), ] -encodeBindings : List Binding +encode_bindings : + List Binding -> { - formatCodes : List FormatCode, - paramValues : List [Null, Value (List U8)], + format_codes : List FormatCode, + param_values : List [Null, Value (List U8)], } -encodeBindings = \bindings -> - count = List.len bindings +encode_bindings = |bindings| + count = List.len(bindings) empty = { - formatCodes: List.withCapacity count, - paramValues: List.withCapacity count, + format_codes: List.with_capacity(count), + param_values: List.with_capacity(count), } - List.walk bindings empty \state, binding -> - { format, value } = encodeSingle binding + List.walk( + bindings, + empty, + |state, binding| + { format, value } = encode_single(binding) - { - formatCodes: state.formatCodes |> List.append format, - paramValues: state.paramValues |> List.append value, - } + { + format_codes: state.format_codes |> List.append(format), + param_values: state.param_values |> List.append(value), + }, + ) -encodeSingle = \binding -> +encode_single = |binding| when binding is Null -> { @@ -124,15 +136,15 @@ encodeSingle = \binding -> format: Binary, } - Binary value -> + Binary(value) -> { - value: Value value, + value: Value(value), format: Binary, } - Text value -> + Text(value) -> { - value: Value (Str.toUtf8 value), + value: Value(Str.to_utf8(value)), format: Text, } diff --git a/src/Pg/BasicCliClient.roc b/src/Pg/BasicCliClient.roc deleted file mode 100644 index aba7a73..0000000 --- a/src/Pg/BasicCliClient.roc +++ /dev/null @@ -1,452 +0,0 @@ -## Unfortunately, the regular `Pg.Client` module runs into the -## infamous "Error during alias analysis" compiler bug when used from basic-cli. -## This version does not. -module [ - connect, - command, - batch, - prepare, - Error, - errorToStr, - Client, -] - -import Protocol.Backend -import Protocol.Frontend -import Bytes.Encode -import Bytes.Decode exposing [decode] -import Pg.Result exposing [CmdResult] -import Pg.Cmd exposing [Cmd] -import Pg.Batch exposing [Batch] -import pf.Tcp -import Cmd -import Batch - -Client := { - stream : Tcp.Stream, - backendKey : Result Protocol.Backend.KeyData [Pending], -} - -connect : - { - host : Str, - port : U16, - user : Str, - auth ? [None, Password Str], - database : Str, - } - -> Task Client _ -connect = \{ host, port, database, auth ? None, user } -> - stream = Tcp.connect! host port - - Tcp.write! stream (Protocol.Frontend.startup { user, database }) - - msg, state <- messageLoop stream { - parameters: Dict.empty {}, - backendKey: Err Pending, - } - - when msg is - AuthOk -> - next state - - AuthCleartextPassword -> - when auth is - None -> - Task.err PasswordRequired - - Password pwd -> - Tcp.write! stream (Protocol.Frontend.passwordMessage pwd) - - next state - - AuthUnsupported -> - Task.err UnsupportedAuth - - BackendKeyData backendKey -> - next { state & backendKey: Ok backendKey } - - ReadyForQuery _ -> - client = @Client { - stream, - backendKey: state.backendKey, - } - - return client - - _ -> - unexpected msg - -# Single command - -command : Cmd a err, - Client - -> Task - a - [ - PgExpectErr err, - PgErr Error, - PgProtoErr _, - TcpReadErr _, - TcpUnexpectedEOF, - TcpWriteErr _, - ] -command = \cmd, @Client { stream } -> - { kind, limit, bindings } = Cmd.params cmd - { formatCodes, paramValues } = Cmd.encodeBindings bindings - - init = - when kind is - SqlCmd sql -> - { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.parse { sql }, - Protocol.Frontend.bind { formatCodes, paramValues }, - Protocol.Frontend.describePortal {}, - Protocol.Frontend.execute { limit }, - ], - fields: [], - } - - PreparedCmd prepared -> - { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.bind { - formatCodes, - paramValues, - preparedStatement: prepared.name, - }, - Protocol.Frontend.execute { limit }, - ], - fields: prepared.fields, - } - - sendWithSync! stream init.messages - - result = readCmdResult! init.fields stream - - decoded = - Cmd.decode result cmd - |> Result.mapErr PgExpectErr - |> Task.fromResult! - - readReadyForQuery! stream - - Task.ok decoded - -# Batches - -batch : Batch a err, - Client - -> Task - a - [ - PgExpectErr err, - PgErr Error, - PgProtoErr _, - TcpReadErr _, - TcpUnexpectedEOF, - TcpWriteErr _, - ] -batch = \cmdBatch, @Client { stream } -> - { commands, seenSql, decode: batchDecode } = Batch.params cmdBatch - - reusedIndexes = - seenSql - |> Dict.walk (Set.empty {}) \set, _, { index, reused } -> - if reused then - set |> Set.insert index - else - set - - inits = - commands - |> List.mapWithIndex (\cmd, ix -> initBatchedCmd reusedIndexes cmd ix) - - commandMessages = - inits - |> List.map .messages - |> Bytes.Encode.sequence - - closeMessages = - reusedIndexes - |> Set.toList - |> List.map \ix -> - Protocol.Frontend.closeStatement { name: Batch.reuseName ix } - |> Bytes.Encode.sequence - - messages = commandMessages |> List.concat closeMessages - sendWithSync! stream messages - - Task.loop - { - remaining: inits, - results: List.withCapacity (List.len commands), - } - (\state -> batchReadStep batchDecode stream state) - -initBatchedCmd : Set U64, - Batch.BatchedCmd, - U64 - -> { - messages : List U8, - fields : [ - Describe, - ReuseFrom U64, - Known (List Pg.Result.RowField), - ], - } -initBatchedCmd = \reusedIndexes, cmd, cmdIndex -> - { formatCodes, paramValues } = Cmd.encodeBindings cmd.bindings - - when cmd.kind is - SqlCmd sql -> - name = - if Set.contains reusedIndexes cmdIndex then - Batch.reuseName cmdIndex - else - "" - - { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.parse { sql, name }, - Protocol.Frontend.bind { - formatCodes, - paramValues, - preparedStatement: name, - }, - Protocol.Frontend.describePortal {}, - Protocol.Frontend.execute { limit: cmd.limit }, - ], - fields: Describe, - } - - ReuseSql index -> - { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.bind { - formatCodes, - paramValues, - preparedStatement: Batch.reuseName index, - }, - Protocol.Frontend.execute { limit: cmd.limit }, - ], - fields: ReuseFrom index, - } - - PreparedCmd prepared -> - { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.bind { - formatCodes, - paramValues, - preparedStatement: prepared.name, - }, - Protocol.Frontend.execute { limit: cmd.limit }, - ], - fields: Known prepared.fields, - } - -batchReadStep = \batchDecode, stream, { remaining, results } -> - when remaining is - [] -> - when batchDecode results is - Ok { value } -> - readReadyForQuery! stream - return value - - Err (MissingCmdResult index) -> - Task.err (PgProtoErr (MissingBatchedCmdResult index)) - - Err (ExpectErr err) -> - Task.err (PgExpectErr err) - - [first, ..] -> - fields = batchedCmdFields! results first.fields - result = readCmdResult! fields stream - - next { - remaining: remaining |> List.dropFirst 1, - results: results |> List.append result, - } - -batchedCmdFields = \results, fieldsMethod -> - when fieldsMethod is - Describe -> - Task.ok [] - - ReuseFrom index -> - when List.get results index is - Ok result -> - Task.ok (Pg.Result.fields result) - - Err OutOfBounds -> - # TODO: better name - Task.err (PgProtoErr ResultOutOfBounds) - - Known fields -> - Task.ok fields - -# Execute helpers - -readCmdResult = \initFields, stream -> - msg, state <- messageLoop stream { - fields: initFields, - rows: [], - } - - when msg is - ParseComplete | BindComplete | ParameterDescription | NoData -> - next state - - RowDescription fields -> - next { state & fields: fields } - - DataRow row -> - next { state & rows: List.append state.rows row } - - CommandComplete _ | EmptyQueryResponse | PortalSuspended -> - return (Pg.Result.create state) - - _ -> - unexpected msg - -readReadyForQuery = \stream -> - msg, {} <- messageLoop stream {} - - when msg is - CloseComplete -> - next {} - - ReadyForQuery _ -> - return {} - - _ -> - unexpected msg - -# Prepared Statements - -prepare : Str,{ name : Str, client : Client } - -> Task - (Cmd CmdResult []) - [ - PgErr Error, - PgProtoErr _, - TcpReadErr _, - TcpUnexpectedEOF, - TcpWriteErr _, - ] -prepare = \sql, { name, client } -> - (@Client { stream }) = client - - parseAndDescribe = Bytes.Encode.sequence [ - Protocol.Frontend.parse { sql, name }, - Protocol.Frontend.describeStatement { name }, - Protocol.Frontend.sync, - ] - - Tcp.write! stream parseAndDescribe - - msg, state <- messageLoop stream [] - - when msg is - ParseComplete | ParameterDescription | NoData -> - next state - - RowDescription fields -> - next fields - - ReadyForQuery _ -> - return (Cmd.prepared { name, fields: state }) - - _ -> - unexpected msg - -# Errors - -Error : Protocol.Backend.Error - -errorToStr : Error -> Str -errorToStr = \err -> - addField = \str, name, result -> - when result is - Ok value -> - "$(str)\n$(name): $(value)" - - Err {} -> - str - - fieldsStr = - "" - |> addField "Detail" err.detail - |> addField "Hint" err.hint - |> addField "Position" (err.position |> Result.map Num.toStr) - |> addField "Internal Position" (err.internalPosition |> Result.map Num.toStr) - |> addField "Internal Query" err.internalQuery - |> addField "Where" err.ewhere - |> addField "Schema" err.schemaName - |> addField "Table" err.tableName - |> addField "Data type" err.dataTypeName - |> addField "Constraint" err.constraintName - |> addField "File" err.file - |> addField "Line" err.line - |> addField "Routine" err.line - - "$(err.localizedSeverity) ($(err.code)): $(err.message)\n$(fieldsStr)" - |> Str.trim - -# Helpers - -readMessage : Tcp.Stream -> Task Protocol.Backend.Message [PgProtoErr _, TcpReadErr _, TcpUnexpectedEOF] -readMessage = \stream -> - headerBytes = Tcp.readExactly! stream 5 - - protoDecode = \bytes, dec -> - decode bytes dec - |> Result.mapErr PgProtoErr - |> Task.fromResult - - meta = headerBytes |> protoDecode! Protocol.Backend.header - - if meta.len > 0 then - payload = Tcp.readExactly! stream (Num.toU64 meta.len) - protoDecode payload (Protocol.Backend.message meta.msgType) - else - protoDecode [] (Protocol.Backend.message meta.msgType) - -messageLoop : Tcp.Stream, state, (Protocol.Backend.Message, state -> Task [Done done, Step state] _) -> Task done _ -messageLoop = \stream, initState, stepFn -> - state <- Task.loop initState - - message = readMessage! stream - - when message is - ErrorResponse error -> - Task.err (PgErr error) - - ParameterStatus _ -> - Task.ok (Step state) - - _ -> - stepFn message state - -next : a -> Task [Step a] * -next = \state -> - Task.ok (Step state) - -return : a -> Task [Done a] * -return = \result -> - Task.ok (Done result) - -unexpected : a -> Task * [PgProtoErr [UnexpectedMsg a]] -unexpected = \msg -> - Task.err (PgProtoErr (UnexpectedMsg msg)) - -sendWithSync : Tcp.Stream, List U8 -> Task {} _ -sendWithSync = \stream, bytes -> - content = Bytes.Encode.sequence [ - bytes, - Protocol.Frontend.sync, - ] - - Tcp.write stream content diff --git a/src/Pg/Batch.roc b/src/Pg/Batch.roc index 866dac8..90a3c23 100644 --- a/src/Pg/Batch.roc +++ b/src/Pg/Batch.roc @@ -5,11 +5,14 @@ import Cmd exposing [Cmd] Batch a err : Batch.Batch a err -succeed : a -> Batch a err +# succeed : a -> Batch a err +succeed : _ -> Batch _ _ succeed = Batch.succeed -with : Batch (a -> b) err, Cmd a err -> Batch b err +# with : Batch (a -> b) err, Cmd a err -> Batch b err +with : Batch (_ -> _) _, Cmd _ _ -> Batch _ _ with = Batch.with -sequence : List (Cmd a err) -> Batch (List a) err +# sequence : List (Cmd a err) -> Batch (List a) err +sequence : List (Cmd _ _) -> Batch (List _) _ sequence = Batch.sequence diff --git a/src/Pg/Client.roc b/src/Pg/Client.roc index fb27136..4bf8f06 100644 --- a/src/Pg/Client.roc +++ b/src/Pg/Client.roc @@ -1,91 +1,89 @@ module [ - withConnect, - command, - batch, - prepare, - Error, - errorToStr, Client, + Error, + batch!, + command!, + connect!, + error_to_str, + prepare!, ] -import Protocol.Backend -import Protocol.Frontend -import Bytes.Encode +import Batch import Bytes.Decode exposing [decode] -import Pg.Result exposing [CmdResult] -import Pg.Cmd exposing [Cmd] +import Bytes.Encode +import Cmd import Pg.Batch exposing [Batch] -import pf.Task exposing [Task, await] +import Pg.Cmd exposing [Cmd] +import Pg.Result exposing [CmdResult] +import Protocol.Backend +import Protocol.Frontend +import pf.Stderr import pf.Tcp -import Cmd -import Batch Client := { stream : Tcp.Stream, - backendKey : Result Protocol.Backend.KeyData [Pending], + backend_key : Result Protocol.Backend.KeyData [Pending], } -withConnect : +connect! : { host : Str, port : U16, user : Str, - auth ? [None, Password Str], + auth ?? [None, Password Str], database : Str, - }, - (Client -> Task a _) - -> Task a _ -withConnect = \{ host, port, database, auth ? None, user }, callback -> - stream <- Tcp.withConnect host port - - _ <- Protocol.Frontend.startup { user, database } |> send stream - - msg, state <- messageLoop stream { - parameters: Dict.empty {}, - backendKey: Err Pending, - } - - when msg is - AuthOk -> - next state - - AuthCleartextPassword -> - when auth is - None -> - Task.err PasswordRequired - - Password pwd -> - _ <- Protocol.Frontend.passwordMessage pwd |> send stream - - next state - - AuthUnsupported -> - Task.err UnsupportedAuth - - BackendKeyData backendKey -> - next { state & backendKey: Ok backendKey } - - ReadyForQuery _ -> - result <- - @Client { - stream, - backendKey: state.backendKey, - } - |> callback - |> await - - _ <- Protocol.Frontend.terminate |> send stream - - return result + } + => Result Client _ +connect! = |{ host, port, database, auth ?? None, user }| + stream = Tcp.connect!(host, port)? + Tcp.write!(stream, Protocol.Frontend.startup({ user, database }))? - _ -> - unexpected msg + message_loop!( + stream, + { + parameters: Dict.empty({}), + backend_key: Err(Pending), + }, + |msg, state| + when msg is + AuthOk -> + next(state) + + AuthCleartextPassword -> + when auth is + None -> + Err(PasswordRequired) + + Password(pwd) -> + Tcp.write!(stream, Protocol.Frontend.password_message(pwd))? + next(state) + + AuthUnsupported -> + Err(UnsupportedAuth) + + BackendKeyData(backend_key) -> + next({ state & backend_key: Ok(backend_key) }) + + ReadyForQuery(_) -> + client = + @Client( + { + stream, + backend_key: state.backend_key, + }, + ) + return Ok(Done(client)) + + _ -> + unexpected(msg), + ) # Single command -command : Cmd a err, +command! : + Cmd a err, Client - -> Task + => Result a [ PgExpectErr err, @@ -95,54 +93,58 @@ command : Cmd a err, TcpUnexpectedEOF, TcpWriteErr _, ] -command = \cmd, @Client { stream } -> - { kind, limit, bindings } = Cmd.params cmd - { formatCodes, paramValues } = Cmd.encodeBindings bindings +command! = |cmd, @Client({ stream })| + { kind, limit, bindings } = Cmd.params(cmd) + { format_codes, param_values } = Cmd.encode_bindings(bindings) init = when kind is - SqlCmd sql -> + SqlCmd(sql) -> { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.parse { sql }, - Protocol.Frontend.bind { formatCodes, paramValues }, - Protocol.Frontend.describePortal {}, - Protocol.Frontend.execute { limit }, - ], + messages: Bytes.Encode.sequence( + [ + Protocol.Frontend.parse({ sql }), + Protocol.Frontend.bind({ format_codes, param_values }), + Protocol.Frontend.describe_portal({}), + Protocol.Frontend.execute({ limit }), + ], + ), fields: [], } - PreparedCmd prepared -> + PreparedCmd(prepared) -> { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.bind { - formatCodes, - paramValues, - preparedStatement: prepared.name, - }, - Protocol.Frontend.execute { limit }, - ], + messages: Bytes.Encode.sequence( + [ + Protocol.Frontend.bind( + { + format_codes, + param_values, + prepared_statement: prepared.name, + }, + ), + Protocol.Frontend.execute({ limit }), + ], + ), fields: prepared.fields, } - _ <- sendWithSync init.messages stream + send_with_sync!(stream, init.messages)? - result <- readCmdResult init.fields stream |> await + result = read_cmd_result!(init.fields, stream)? - decoded <- Cmd.decode result cmd - |> Result.mapErr PgExpectErr - |> Task.fromResult - |> await + decoded = Cmd.decode(result, cmd) ? PgExpectErr - _ <- readReadyForQuery stream |> await + read_ready_for_query!(stream)? - Task.ok decoded + Ok(decoded) # Batches -batch : Batch a err, +batch! : + Batch a err, Client - -> Task + => Result a [ PgExpectErr err, @@ -152,44 +154,51 @@ batch : Batch a err, TcpUnexpectedEOF, TcpWriteErr _, ] -batch = \cmdBatch, @Client { stream } -> - { commands, seenSql, decode: batchDecode } = Batch.params cmdBatch - - reusedIndexes = - seenSql - |> Dict.walk (Set.empty {}) \set, _, { index, reused } -> - if reused then - set |> Set.insert index - else - set +batch! = |cmd_batch, @Client({ stream })| + { commands, seen_sql, decode: batch_decode } = Batch.params(cmd_batch) + + reused_indexes = + seen_sql + |> Dict.walk( + Set.empty({}), + |set, _, { index, reused }| + if reused then + set |> Set.insert(index) + else + set, + ) inits = commands - |> List.mapWithIndex (\cmd, ix -> initBatchedCmd reusedIndexes cmd ix) + |> List.map_with_index(|cmd, ix| init_batched_cmd(reused_indexes, cmd, ix)) - commandMessages = + command_messages = inits - |> List.map .messages + |> List.map(.messages) |> Bytes.Encode.sequence - closeMessages = - reusedIndexes - |> Set.toList - |> List.map \ix -> - Protocol.Frontend.closeStatement { name: Batch.reuseName ix } + close_messages = + reused_indexes + |> Set.to_list + |> List.map( + |ix| + Protocol.Frontend.close_statement({ name: Batch.reuse_name(ix) }), + ) |> Bytes.Encode.sequence - messages = commandMessages |> List.concat closeMessages - _ <- sendWithSync messages stream + messages = command_messages |> List.concat(close_messages) + send_with_sync!(stream, messages)? - Task.loop + loop!( { remaining: inits, - results: List.withCapacity (List.len commands), - } - (\state -> batchReadStep batchDecode stream state) + results: List.with_capacity(List.len(commands)), + }, + |state| batch_read_step!(batch_decode, stream, state), + ) -initBatchedCmd : Set U64, +init_batched_cmd : + Set U64, Batch.BatchedCmd, U64 -> { @@ -200,139 +209,164 @@ initBatchedCmd : Set U64, Known (List Pg.Result.RowField), ], } -initBatchedCmd = \reusedIndexes, cmd, cmdIndex -> - { formatCodes, paramValues } = Cmd.encodeBindings cmd.bindings +init_batched_cmd = |reused_indexes, cmd, cmd_index| + { format_codes, param_values } = Cmd.encode_bindings(cmd.bindings) when cmd.kind is - SqlCmd sql -> + SqlCmd(sql) -> name = - if Set.contains reusedIndexes cmdIndex then - Batch.reuseName cmdIndex + if Set.contains(reused_indexes, cmd_index) then + Batch.reuse_name(cmd_index) else "" { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.parse { sql, name }, - Protocol.Frontend.bind { - formatCodes, - paramValues, - preparedStatement: name, - }, - Protocol.Frontend.describePortal {}, - Protocol.Frontend.execute { limit: cmd.limit }, - ], + messages: Bytes.Encode.sequence( + [ + Protocol.Frontend.parse({ sql, name }), + Protocol.Frontend.bind( + { + format_codes, + param_values, + prepared_statement: name, + }, + ), + Protocol.Frontend.describe_portal({}), + Protocol.Frontend.execute({ limit: cmd.limit }), + ], + ), fields: Describe, } - ReuseSql index -> + ReuseSql(index) -> { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.bind { - formatCodes, - paramValues, - preparedStatement: Batch.reuseName index, - }, - Protocol.Frontend.execute { limit: cmd.limit }, - ], - fields: ReuseFrom index, + messages: Bytes.Encode.sequence( + [ + Protocol.Frontend.bind( + { + format_codes, + param_values, + prepared_statement: Batch.reuse_name(index), + }, + ), + Protocol.Frontend.execute({ limit: cmd.limit }), + ], + ), + fields: ReuseFrom(index), } - PreparedCmd prepared -> + PreparedCmd(prepared) -> { - messages: Bytes.Encode.sequence [ - Protocol.Frontend.bind { - formatCodes, - paramValues, - preparedStatement: prepared.name, - }, - Protocol.Frontend.execute { limit: cmd.limit }, - ], - fields: Known prepared.fields, + messages: Bytes.Encode.sequence( + [ + Protocol.Frontend.bind( + { + format_codes, + param_values, + prepared_statement: prepared.name, + }, + ), + Protocol.Frontend.execute({ limit: cmd.limit }), + ], + ), + fields: Known(prepared.fields), } -batchReadStep = \batchDecode, stream, { remaining, results } -> +batch_read_step! = |batch_decode, stream, { remaining, results }| when remaining is [] -> - when batchDecode results is - Ok { value } -> - _ <- readReadyForQuery stream |> await - - return value + when batch_decode(results) is + Ok({ value }) -> + read_ready_for_query!(stream)? + return_(value) - Err (MissingCmdResult index) -> - Task.err (PgProtoErr (MissingBatchedCmdResult index)) + Err(MissingCmdResult(index)) -> + Err(PgProtoErr(MissingBatchedCmdResult(index))) - Err (ExpectErr err) -> - Task.err (PgExpectErr err) + Err(ExpectErr(err)) -> + Err(PgExpectErr(err)) [first, ..] -> - fields <- await (batchedCmdFields results first.fields) - result <- readCmdResult fields stream |> await + fields = batched_cmd_fields(results, first.fields)? + result = read_cmd_result!(fields, stream)? - next { - remaining: remaining |> List.dropFirst 1, - results: results |> List.append result, - } + next( + { + remaining: remaining |> List.drop_first(1), + results: results |> List.append(result), + }, + ) -batchedCmdFields = \results, fieldsMethod -> - when fieldsMethod is +batched_cmd_fields = |results, fields_method| + when fields_method is Describe -> - Task.ok [] + Ok([]) - ReuseFrom index -> - when List.get results index is - Ok result -> - Task.ok (Pg.Result.fields result) + ReuseFrom(index) -> + when List.get(results, index) is + Ok(result) -> + Ok(Pg.Result.fields(result)) - Err OutOfBounds -> + Err(OutOfBounds) -> # TODO: better name - Task.err (PgProtoErr ResultOutOfBounds) + Err(PgProtoErr(ResultOutOfBounds)) - Known fields -> - Task.ok fields + Known(fields) -> + Ok(fields) # Execute helpers -readCmdResult = \initFields, stream -> - msg, state <- messageLoop stream { - fields: initFields, +read_cmd_result! = |init_fields, stream| + message_loop!( + stream, + { + fields: init_fields, rows: [], - } - - when msg is - ParseComplete | BindComplete | ParameterDescription | NoData -> - next state + parameters: [], + }, + |msg, state| + when msg is + ParseComplete | BindComplete | NoData | NoticeResponse _ -> + next(state) - RowDescription fields -> - next { state & fields: fields } + ParameterDescription(parameters) -> + next({ state & parameters: parameters }) - DataRow row -> - next { state & rows: List.append state.rows row } + RowDescription(fields) -> + next({ state & fields: fields }) - CommandComplete _ | EmptyQueryResponse | PortalSuspended -> - return (Pg.Result.create state) + DataRow(row) -> + next({ state & rows: List.append(state.rows, row) }) - _ -> - unexpected msg + CommandComplete(_) | EmptyQueryResponse | PortalSuspended -> + return_(Pg.Result.create(state)) -readReadyForQuery = \stream -> - msg, {} <- messageLoop stream {} + _ -> + unexpected(msg), + ) - when msg is - CloseComplete -> - next {} +read_ready_for_query! = |stream| + message_loop!( + stream, + {}, + |msg, {}| + when msg is + CloseComplete -> + next({}) - ReadyForQuery _ -> - return {} + ReadyForQuery(_) -> + return_({}) - _ -> - unexpected msg + _ -> + unexpected(msg), + ) # Prepared Statements -prepare : Str,{ name : Str, client : Client } - -> Task +prepare! : + Str, + { name : Str, client : Client } + => Result (Cmd CmdResult []) [ PgErr Error, @@ -341,119 +375,136 @@ prepare : Str,{ name : Str, client : Client } TcpUnexpectedEOF, TcpWriteErr _, ] -prepare = \sql, { name, client } -> - (@Client { stream }) = client - - _ <- Bytes.Encode.sequence [ - Protocol.Frontend.parse { sql, name }, - Protocol.Frontend.describeStatement { name }, - Protocol.Frontend.sync, - ] - |> send stream - - msg, state <- messageLoop stream [] - - when msg is - ParseComplete | ParameterDescription | NoData -> - next state - - RowDescription fields -> - next fields - - ReadyForQuery _ -> - return (Cmd.prepared { name, fields: state }) - - _ -> - unexpected msg +prepare! = |sql, { name, client }| + @Client({ stream }) = client + + parse_and_describe = + Bytes.Encode.sequence( + [ + Protocol.Frontend.parse({ sql, name }), + Protocol.Frontend.describe_statement({ name }), + Protocol.Frontend.sync, + ], + ) + Tcp.write!(stream, parse_and_describe)? + + message_loop!( + stream, + { fields: [], parameters: [] }, + |msg, state| + when msg is + ParseComplete | NoData -> + next(state) + + ParameterDescription parameters -> + next({ state & parameters: parameters }) + + RowDescription(fields) -> + next({ state & fields: fields }) + + ReadyForQuery(_) -> + return_(Cmd.prepared({ name, fields: state.fields, parameters: state.parameters })) + + _ -> + unexpected(msg), + ) # Errors Error : Protocol.Backend.Error -errorToStr : Error -> Str -errorToStr = \err -> - addField = \str, name, result -> +error_to_str : Error -> Str +error_to_str = |err| + add_field = |str, name, result| when result is - Ok value -> - "$(str)\n$(name): $(value)" + Ok(value) -> + "${str}\n${name}: ${value}" - Err {} -> + Err({}) -> str - fieldsStr = + fields_str = "" - |> addField "Detail" err.detail - |> addField "Hint" err.hint - |> addField "Position" (err.position |> Result.map Num.toStr) - |> addField "Internal Position" (err.internalPosition |> Result.map Num.toStr) - |> addField "Internal Query" err.internalQuery - |> addField "Where" err.ewhere - |> addField "Schema" err.schemaName - |> addField "Table" err.tableName - |> addField "Data type" err.dataTypeName - |> addField "Constraint" err.constraintName - |> addField "File" err.file - |> addField "Line" err.line - |> addField "Routine" err.line - - "$(err.localizedSeverity) ($(err.code)): $(err.message)\n$(fieldsStr)" + |> add_field("Detail", err.detail) + |> add_field("Hint", err.hint) + |> add_field("Position", (err.position |> Result.map_ok(Num.to_str))) + |> add_field("Internal Position", (err.internal_position |> Result.map_ok(Num.to_str))) + |> add_field("Internal Query", err.internal_query) + |> add_field("Where", err.ewhere) + |> add_field("Schema", err.schema_name) + |> add_field("Table", err.table_name) + |> add_field("Data type", err.data_type_name) + |> add_field("Constraint", err.constraint_name) + |> add_field("File", err.file) + |> add_field("Line", err.line) + |> add_field("Routine", err.line) + + "${err.localized_severity} (${err.code}): ${err.message}\n${fields_str}" |> Str.trim # Helpers -readMessage : Tcp.Stream -> Task Protocol.Backend.Message [PgProtoErr _, TcpReadErr _, TcpUnexpectedEOF] -readMessage = \stream -> - headerBytes <- Tcp.readExactly 5 stream |> await +read_message! : Tcp.Stream => Result Protocol.Backend.Message [PgProtoErr _, TcpReadErr _, TcpUnexpectedEOF] +read_message! = |stream| + header_bytes = Tcp.read_exactly!(stream, 5)? - protoDecode = \bytes, dec -> - decode bytes dec - |> Result.mapErr PgProtoErr - |> Task.fromResult + proto_decode = |bytes, dec| + decode(bytes, dec) + |> Result.map_err(PgProtoErr) - meta <- headerBytes |> protoDecode Protocol.Backend.header |> await + meta = header_bytes |> proto_decode(Protocol.Backend.header)? if meta.len > 0 then - payload <- Tcp.readExactly (Num.toU64 meta.len) stream |> await - protoDecode payload (Protocol.Backend.message meta.msgType) + payload = Tcp.read_exactly!(stream, Num.to_u64(meta.len))? + proto_decode(payload, Protocol.Backend.message(meta.msg_type)) else - protoDecode [] (Protocol.Backend.message meta.msgType) - -messageLoop : Tcp.Stream, state, (Protocol.Backend.Message, state -> Task [Done done, Step state] _) -> Task done _ -messageLoop = \stream, initState, stepFn -> - state <- Task.loop initState - - message <- readMessage stream |> await - - when message is - ErrorResponse error -> - Task.err (PgErr error) - - ParameterStatus _ -> - Task.ok (Step state) - - _ -> - stepFn message state - -next : a -> Task [Step a] * -next = \state -> - Task.ok (Step state) - -return : a -> Task [Done a] * -return = \result -> - Task.ok (Done result) - -unexpected : a -> Task * [PgProtoErr [UnexpectedMsg a]] -unexpected = \msg -> - Task.err (PgProtoErr (UnexpectedMsg msg)) - -send : List U8, Tcp.Stream, ({} -> Task a _) -> Task a _ -send = \bytes, stream, callback -> - Tcp.write bytes stream |> await callback - -sendWithSync : List U8, Tcp.Stream, ({} -> Task a _) -> Task a _ -sendWithSync = \bytes, stream, callback -> - Bytes.Encode.sequence [ - bytes, - Protocol.Frontend.sync, - ] - |> send stream callback + proto_decode([], Protocol.Backend.message(meta.msg_type)) + +loop! : state, (state => Result [Step state, Done done] err) => Result done err +loop! = |state, fn!| + when fn!(state) is + Err(err) -> Err(err) + Ok(Done(done)) -> + Ok(done) + + Ok(Step(next_)) -> loop!(next_, fn!) + +message_loop! : Tcp.Stream, state, (Protocol.Backend.Message, state => Result [Done done, Step state] _) => Result done _ +message_loop! = |stream, init_state, step_fn!| + loop!( + init_state, + |state| + message = read_message!(stream)? + when message is + ErrorResponse(error) -> + _ = Stderr.line!("ERROR from roc-pg: ${Inspect.to_str(error)}") + Err(PgErr(error)) + + ParameterStatus(_) -> + Ok(Step(state)) + + _ -> + step_fn!(message, state), + ) + +next : a -> Result [Step a] * +next = |state| + Ok(Step(state)) + +return_ : a -> Result [Done a] * +return_ = |result| + Ok(Done(result)) + +unexpected : a -> Result * [PgProtoErr [UnexpectedMsg a]] +unexpected = |msg| + Err(PgProtoErr(UnexpectedMsg(msg))) + +send_with_sync! : Tcp.Stream, List U8 => Result {} _ +send_with_sync! = |stream, bytes| + content = Bytes.Encode.sequence( + [ + bytes, + Protocol.Frontend.sync, + ], + ) + Tcp.write!(stream, content) diff --git a/src/Pg/Cmd.roc b/src/Pg/Cmd.roc index e2fe90a..334e763 100644 --- a/src/Pg/Cmd.roc +++ b/src/Pg/Cmd.roc @@ -1,29 +1,29 @@ module [ + Binding, Cmd, - new, - expectN, + bind, + bool, + bytes, expect1, + expect_n, + f32, + f64, + i128, + i16, + i32, + i64, + i8, + inspect, map, - bind, - Binding, + new, null, str, - u8, + u128, u16, u32, u64, - u128, - i8, - i16, - i32, - i64, - i128, - f32, - f64, - bytes, - bool, - withCustomDecode, - inspect, + u8, + with_custom_decode, ] import Cmd @@ -32,82 +32,89 @@ import Pg.Result exposing [CmdResult] Cmd a err : Cmd.Cmd a err new : Str -> Cmd CmdResult [] -new = Cmd.fromSql +new = Cmd.from_sql # Result -expectN : Cmd CmdResult [], Pg.Result.Decode a err -> Cmd (List a) [FieldNotFound Str]err -expectN = \cmd, decoder -> - Cmd.withDecode cmd \r -> Pg.Result.decode r decoder +expect_n : Cmd CmdResult [], Pg.Result.Decode a err -> Cmd (List a) [FieldNotFound Str]err +expect_n = |cmd, decoder| + Cmd.with_decode(cmd, |r| Pg.Result.decode(r, decoder)) expect1 : Cmd CmdResult [], Pg.Result.Decode a [EmptyResult]err -> Cmd a [EmptyResult, FieldNotFound Str]err -expect1 = \cmd, decoder -> - cmdResult <- cmd |> Cmd.withLimit 1 |> Cmd.withDecode - rows <- Pg.Result.decode cmdResult decoder |> Result.try - - when rows is - [row] -> - Ok row - - _ -> - Err EmptyResult - -map : Cmd a err, (a -> b) -> Cmd b err +expect1 = |cmd, decoder| + cmd + |> Cmd.with_limit(1) + |> Cmd.with_decode( + |cmd_result| + Pg.Result.decode(cmd_result, decoder) + |> Result.try( + |rows| + when rows is + [row] -> + Ok(row) + + _ -> + Err(EmptyResult), + ), + ) + +map : Cmd _ _, (_ -> _) -> Cmd _ _ map = Cmd.map -withCustomDecode : Cmd * *, (CmdResult -> Result a err) -> Cmd a err -withCustomDecode = Cmd.withDecode +with_custom_decode : Cmd _ _, (CmdResult -> Result _ _) -> Cmd _ _ +with_custom_decode = Cmd.with_decode inspect : Cmd a err -> Str -inspect = \cmd -> - { kind, bindings } = Cmd.params cmd +inspect = |cmd| + { kind, bindings } = Cmd.params(cmd) - kindStr = inspectKind kind - bindingsStr = + kind_str = inspect_kind(kind) + bindings_str = bindings - |> List.mapWithIndex - \val, index -> + |> List.map_with_index( + |val, index| n = index + 1 - "$$(Num.toStr n) = $(inspectBinding val)" - |> Str.joinWith "\n" + "$${Num.to_str(n)} = ${inspect_binding(val)}", + ) + |> Str.join_with("\n") - "$(kindStr)\n$(bindingsStr)" + "${kind_str}\n${bindings_str}" -inspectKind = \kind -> +inspect_kind = |kind| when kind is - SqlCmd sql -> - "SQL: $(sql)" + SqlCmd(sql) -> + "SQL: ${sql}" - PreparedCmd { name } -> - "Prepared: $(name)" + PreparedCmd({ name }) -> + "Prepared: ${name}" -inspectBinding = \binding -> +inspect_binding = |binding| when binding is Null -> "NULL" - Text text -> + Text(text) -> text - Binary bin -> + Binary(bin) -> bin - |> List.map Num.toStr - |> Str.joinWith "," + |> List.map(Num.to_str) + |> Str.join_with(",") # Bindings Binding := Cmd.Binding implements [Eq] bind : Cmd a err, List Binding -> Cmd a err -bind = \cmd, bindings -> - Cmd.bind cmd (bindings |> List.map \@Binding binding -> binding) +bind = |cmd, bindings| + Cmd.bind(cmd, (bindings |> List.map(|@Binding(binding)| binding))) null : Binding -null = @Binding Null +null = @Binding(Null) str : Str -> Binding -str = \value -> - @Binding (Text value) +str = |value| + @Binding(Text(value)) u8 = num u16 = num @@ -123,13 +130,13 @@ f32 = num f64 = num num : Num * -> Binding -num = \value -> - @Binding (Text (Num.toStr value)) +num = |value| + @Binding(Text(Num.to_str(value))) bool : Bool -> Binding -bool = \value -> - @Binding (Binary [(if value then 1 else 0)]) +bool = |value| + @Binding(Binary([(if value then 1 else 0)])) bytes : List U8 -> Binding -bytes = \value -> - @Binding (Binary value) +bytes = |value| + @Binding(Binary(value)) diff --git a/src/Pg/Result.roc b/src/Pg/Result.roc index f6720a8..fc41dee 100644 --- a/src/Pg/Result.roc +++ b/src/Pg/Result.roc @@ -1,143 +1,173 @@ module [ CmdResult, + Decode, + ParameterField, RowField, + apply, + bool, create, - len, + dec, + decode, + f32, + f64, fields, + i128, + i16, + i32, + i64, + i8, + len, + combine, rows, - decode, - Decode, str, - u8, + succeed, + u128, u16, u32, u64, - u128, - i8, - i16, - i32, - i64, - i128, - f32, - f64, - dec, - bool, + u8, with, - apply, - succeed, ] import Protocol.Backend RowField : Protocol.Backend.RowField +ParameterField : Protocol.Backend.ParameterField CmdResult := { fields : List RowField, rows : List (List (List U8)), + parameters : List ParameterField, } create = @CmdResult fields : CmdResult -> List RowField -fields = \@CmdResult result -> result.fields +fields = |@CmdResult(result)| result.fields rows : CmdResult -> List (List (List U8)) -rows = \@CmdResult result -> result.rows +rows = |@CmdResult(result)| result.rows len : CmdResult -> U64 -len = \@CmdResult result -> - List.len result.rows +len = |@CmdResult(result)| + List.len(result.rows) Decode a err := List RowField -> Result - (List (List U8) - -> - Result a [FieldNotFound Str]err) + (List (List U8) -> Result a [FieldNotFound Str]err) [FieldNotFound Str] decode : CmdResult, Decode a err -> Result (List a) [FieldNotFound Str]err -decode = \@CmdResult r, @Decode getDecode -> - when getDecode r.fields is - Ok fn -> - List.mapTry r.rows fn - - Err (FieldNotFound name) -> - Err (FieldNotFound name) - -str = decoder Ok - -u8 = decoder Str.toU8 - -u16 = decoder Str.toU16 - -u32 = decoder Str.toU32 - -u64 = decoder Str.toU64 - -u128 = decoder Str.toU128 - -i8 = decoder Str.toI8 - -i16 = decoder Str.toI8 - -i32 = decoder Str.toI32 - -i64 = decoder Str.toI64 - -i128 = decoder Str.toI128 - -f32 = decoder Str.toF32 - -f64 = decoder Str.toF64 - -dec = decoder Str.toDec - -bool = decoder \v -> - when v is - "t" -> Ok Bool.true - "f" -> Ok Bool.false - _ -> Err InvalidBoolStr - -decoder = \fn -> \name -> - rowFields <- @Decode - - when List.findFirstIndex rowFields \f -> f.name == name is - Ok index -> - row <- Ok - - when List.get row index is - Ok bytes -> - when Str.fromUtf8 bytes is - Ok strValue -> - fn strValue - - Err err -> - Err err - - Err OutOfBounds -> - Err (FieldNotFound name) - - Err NotFound -> - Err (FieldNotFound name) - -map2 = \@Decode a, @Decode b, cb -> - rowFields <- @Decode - - decodeA <- Result.try (a rowFields) - decodeB <- Result.try (b rowFields) - - row <- Ok - - valueA <- Result.try (decodeA row) - valueB <- Result.try (decodeB row) - - Ok (cb valueA valueB) - -succeed = \value -> - @Decode \_ -> Ok \_ -> Ok value - -with = \a, b -> map2 a b (\fn, val -> fn val) - -apply = \a -> \fn -> with fn a +decode = |@CmdResult(r), @Decode(get_decode)| + when get_decode(r.fields) is + Ok(fn) -> + List.map_try(r.rows, fn) + + Err(FieldNotFound(name)) -> + Err(FieldNotFound(name)) + +str = decoder(Ok) + +u8 = decoder(Str.to_u8) + +u16 = decoder(Str.to_u16) + +u32 = decoder(Str.to_u32) + +u64 = decoder(Str.to_u64) + +u128 = decoder(Str.to_u128) + +i8 = decoder(Str.to_i8) + +i16 = decoder(Str.to_i16) + +i32 = decoder(Str.to_i32) + +i64 = decoder(Str.to_i64) + +i128 = decoder(Str.to_i128) + +f32 = decoder(Str.to_f32) + +f64 = decoder(Str.to_f64) + +dec = decoder(Str.to_dec) + +bool = decoder( + |v| + when v is + "t" -> Ok(Bool.true) + "f" -> Ok(Bool.false) + _ -> Err(InvalidBoolStr), +) + +decoder = |fn| + |name| + @Decode( + |row_fields| + when List.find_first_index(row_fields, |f| f.name == name) is + Ok(index) -> + Ok( + |row| + when List.get(row, index) is + Ok(bytes) -> + str_value = Str.from_utf8(bytes)? + fn(str_value) + + Err(OutOfBounds) -> + Err(FieldNotFound(name)), + ) + + Err(NotFound) -> + Err(FieldNotFound(name)), + ) + +map2 = |@Decode(a), @Decode(b), cb| + @Decode( + |row_fields| + Result.try( + a(row_fields), + |decode_a| + Result.try( + b(row_fields), + |decode_b| + Ok( + |row| + Result.try( + decode_a(row), + |value_a| + Result.try( + decode_b(row), + |value_b| + Ok(cb(value_a, value_b)), + ), + ), + ), + ), + ), + ) + +succeed = |value| + @Decode(|_| Ok(|_| Ok(value))) + +with = |a, b| map2(a, b, |fn, val| fn(val)) + +apply = |a| |fn| with(fn, a) + +## Use with Roc's [Record Builder](https://www.roc-lang.org/tutorial#record-builder) +## syntax to build records of your returned rows: +## +## ``` +## Pg.Cmd.expect_n( +## { Pg.Result.combine <- +## name: Pg.Result.str("name"), +## age: Pg.Result.u8("age"), +## }, +## ) +## ``` +# NOTE: `combine` is an alias of `map2` simply to increase its +# discoverability and user-friendliness for its intended use-case. +combine = map2 diff --git a/src/Protocol/Backend.roc b/src/Protocol/Backend.roc index 931ec33..7cb70ae 100644 --- a/src/Protocol/Backend.roc +++ b/src/Protocol/Backend.roc @@ -1,11 +1,12 @@ module [ - header, - message, - Message, + Error, KeyData, - Status, + Message, + ParameterField, RowField, - Error, + Status, + header, + message, ] import Bytes.Decode exposing [ @@ -18,7 +19,7 @@ import Bytes.Decode exposing [ u8, i16, i32, - cStr, + c_str, take, ] @@ -32,9 +33,10 @@ Message : [ ErrorResponse Error, ParseComplete, BindComplete, + NoticeResponse (List { code : U8, value : Str }), NoData, RowDescription (List RowField), - ParameterDescription, + ParameterDescription (List ParameterField), DataRow (List (List U8)), PortalSuspended, CommandComplete Str, @@ -42,197 +44,277 @@ Message : [ CloseComplete, ] -header : Decode { msgType : U8, len : I32 } _ +header : Decode { msg_type : U8, len : I32 } _ header = - msgType <- await u8 - len <- map i32 - - { msgType, len: len - 4 } + await( + u8, + |msg_type| + map( + i32, + |len| + { msg_type, len: len - 4 }, + ), + ) message : U8 -> Decode Message _ -message = \msgType -> - when msgType is +message = |msg_type| + when msg_type is 'R' -> - authRequest + auth_request 'S' -> - paramStatus + param_status 'K' -> - backendKeyData + backend_key_data 'Z' -> - readyForQuery + ready_for_query 'E' -> - errorResponse + error_response '1' -> - succeed ParseComplete + succeed(ParseComplete) '2' -> - succeed BindComplete + succeed(BindComplete) + + 'N' -> + notice_response 'n' -> - succeed NoData + succeed(NoData) 'T' -> - rowDescription + row_description 't' -> - succeed ParameterDescription + parameter_description 'D' -> - dataRow + data_row 's' -> - succeed PortalSuspended + succeed(PortalSuspended) 'C' -> - commandComplete + command_complete 'I' -> - succeed EmptyQueryResponse + succeed(EmptyQueryResponse) '3' -> - succeed CloseComplete - - _ -> - fail (UnrecognizedBackendMessage msgType) - -authRequest : Decode Message _ -authRequest = - authType <- map i32 - - when authType is - 0 -> - AuthOk - - 3 -> - AuthCleartextPassword + succeed(CloseComplete) _ -> - AuthUnsupported - -paramStatus : Decode Message _ -paramStatus = - name <- await cStr - value <- await cStr - succeed (ParameterStatus { name, value }) - -KeyData : { processId : I32, secretKey : I32 } - -backendKeyData : Decode Message _ -backendKeyData = - processId <- await i32 - secretKey <- await i32 - succeed (BackendKeyData { processId, secretKey }) + fail(UnrecognizedBackendMessage(msg_type)) + +auth_request : Decode Message _ +auth_request = + map( + i32, + |auth_type| + when auth_type is + 0 -> + AuthOk + + 3 -> + AuthCleartextPassword + + _ -> + AuthUnsupported, + ) + +param_status : Decode Message _ +param_status = + await( + c_str, + |name| + await( + c_str, + |value| + succeed(ParameterStatus({ name, value })), + ), + ) + +KeyData : { process_id : I32, secret_key : I32 } + +backend_key_data : Decode Message _ +backend_key_data = + await( + i32, + |process_id| + await( + i32, + |secret_key| + succeed(BackendKeyData({ process_id, secret_key })), + ), + ) Status : [Idle, TransactionBlock, FailedTransactionBlock] -readyForQuery : Decode Message _ -readyForQuery = - status <- await u8 - - when status is - 'I' -> - succeed (ReadyForQuery Idle) - - 'T' -> - succeed (ReadyForQuery TransactionBlock) - - 'E' -> - succeed (ReadyForQuery FailedTransactionBlock) - - _ -> - fail (UnrecognizedBackendStatus status) +ready_for_query : Decode Message _ +ready_for_query = + await( + u8, + |status| + when status is + 'I' -> + succeed(ReadyForQuery(Idle)) + + 'T' -> + succeed(ReadyForQuery(TransactionBlock)) + + 'E' -> + succeed(ReadyForQuery(FailedTransactionBlock)) + + _ -> + fail(UnrecognizedBackendStatus(status)), + ) + +read_notice_responses : Decode (List { code : U8, value : Str }) _ +read_notice_responses = + loop( + [], + |collected| + await( + u8, + |code| + if code == 0 then + succeed(Done(collected)) + else + map( + c_str, + |value| + Loop(List.append(collected, { code, value })), + ), + ), + ) + +notice_response = + await( + read_notice_responses, + |notices| + succeed(NoticeResponse(notices)), + ) Error : { - localizedSeverity : Str, + localized_severity : Str, severity : Result ErrorSeverity {}, code : Str, message : Str, detail : Result Str {}, hint : Result Str {}, position : Result U32 {}, - internalPosition : Result U32 {}, - internalQuery : Result Str {}, + internal_position : Result U32 {}, + internal_query : Result Str {}, ewhere : Result Str {}, - schemaName : Result Str {}, - tableName : Result Str {}, - columnName : Result Str {}, - dataTypeName : Result Str {}, - constraintName : Result Str {}, + schema_name : Result Str {}, + table_name : Result Str {}, + column_name : Result Str {}, + data_type_name : Result Str {}, + constraint_name : Result Str {}, file : Result Str {}, line : Result Str {}, routine : Result Str {}, } -errorResponse : Decode Message _ -errorResponse = - dict <- await knownStrFields - - localizedSeverity <- 'S' |> requiredField dict - severity <- 'V' |> optionalFieldWith dict decodeSeverity - code <- 'C' |> requiredField dict - msg <- 'M' |> requiredField dict - position <- 'P' |> optionalFieldWith dict Str.toU32 - internalPosition <- 'p' |> optionalFieldWith dict Str.toU32 - - ErrorResponse { - localizedSeverity, - severity, - code, - message: msg, - detail: 'D' |> optionalField dict, - hint: 'H' |> optionalField dict, - position, - internalPosition, - internalQuery: 'q' |> optionalField dict, - ewhere: 'W' |> optionalField dict, - schemaName: 's' |> optionalField dict, - tableName: 't' |> optionalField dict, - columnName: 'c' |> optionalField dict, - dataTypeName: 'd' |> optionalField dict, - constraintName: 'n' |> optionalField dict, - file: 'F' |> optionalField dict, - line: 'L' |> optionalField dict, - routine: 'R' |> optionalField dict, - } - |> succeed - -optionalField = \fieldId, dict -> - when Dict.get dict fieldId is - Ok value -> - Ok value - - Err _ -> - Err {} - -optionalFieldWith = \fieldId, dict, validate, callback -> - result = Dict.get dict fieldId +error_response : Decode Message _ +error_response = + await( + known_str_fields, + |dict| + 'S' + |> required_field( + dict, + |localized_severity| + 'V' + |> optional_field_with( + dict, + decode_severity, + |severity| + 'C' + |> required_field( + dict, + |code| + 'M' + |> required_field( + dict, + |msg| + 'P' + |> optional_field_with( + dict, + Str.to_u32, + |position| + 'p' + |> optional_field_with( + dict, + Str.to_u32, + |internal_position| + ErrorResponse( + { + localized_severity, + severity, + code, + message: msg, + detail: 'D' |> optional_field(dict), + hint: 'H' |> optional_field(dict), + position, + internal_position, + internal_query: 'q' |> optional_field(dict), + ewhere: 'W' |> optional_field(dict), + schema_name: 's' |> optional_field(dict), + table_name: 't' |> optional_field(dict), + column_name: 'c' |> optional_field(dict), + data_type_name: 'd' |> optional_field(dict), + constraint_name: 'n' |> optional_field(dict), + file: 'F' |> optional_field(dict), + line: 'L' |> optional_field(dict), + routine: 'R' |> optional_field(dict), + }, + ) + |> succeed, + ), + ), + ), + ), + ), + ), + ) + +optional_field = |field_id, dict| + when Dict.get(dict, field_id) is + Ok(value) -> + Ok(value) + + Err(_) -> + Err({}) + +optional_field_with = |field_id, dict, validate, callback| + result = Dict.get(dict, field_id) when result is - Ok value -> - when validate value is - Ok validated -> - callback (Ok validated) + Ok(value) -> + when validate(value) is + Ok(validated) -> + callback(Ok(validated)) - Err err -> - fail err + Err(err) -> + fail(err) - Err _ -> - callback (Err {}) + Err(_) -> + callback(Err({})) -requiredField = \fieldId, dict, callback -> - result = Dict.get dict fieldId +required_field = |field_id, dict, callback| + result = Dict.get(dict, field_id) when result is - Ok value -> - callback value + Ok(value) -> + callback(value) - Err _ -> - fail (MissingField fieldId) + Err(_) -> + fail(MissingField(field_id)) ErrorSeverity : [ Error, @@ -245,119 +327,179 @@ ErrorSeverity : [ Log, ] -decodeSeverity = \str -> +decode_severity : Str -> Result ErrorSeverity [InvalidSeverity Str] +decode_severity = |str| when str is "ERROR" -> - Ok Error + Ok(Error) "FATAL" -> - Ok Fatal + Ok(Fatal) "PANIC" -> - Ok Panic + Ok(Panic) "WARNING" -> - Ok Warning + Ok(Warning) "NOTICE" -> - Ok Notice + Ok(Notice) "DEBUG" -> - Ok Debug + Ok(Debug) "INFO" -> - Ok Info + Ok(Info) "LOG" -> - Ok Log + Ok(Log) _ -> - Err (InvalidSeverity str) - -knownStrFields : Decode (Dict U8 Str) _ -knownStrFields = - collected <- loop (Dict.empty {}) - - fieldId <- await u8 - - if fieldId == 0 then - succeed (Done collected) - else - value <- map cStr - - collected - |> Dict.insert fieldId value - |> Loop + Err(InvalidSeverity(str)) + +known_str_fields : Decode (Dict U8 Str) _ +known_str_fields = + loop( + Dict.empty({}), + |collected| + await( + u8, + |field_id| + if field_id == 0 then + succeed(Done(collected)) + else + map( + c_str, + |value| + collected + |> Dict.insert(field_id, value) + |> Loop, + ), + ), + ) RowField : { name : Str, - column : Result { tableOid : I32, attributeNumber : I16 } [NotAColumn], - dataTypeOid : I32, - dataTypeSize : I16, - typeModifier : I32, - formatCode : I16, + column : Result { table_oid : I32, attribute_number : I16 } [NotAColumn], + data_type_oid : I32, + data_type_size : I16, + type_modifier : I32, + format_code : I16, } -rowDescription : Decode Message _ -rowDescription = - fieldCount <- await i16 - - fixedList fieldCount rowField - |> map RowDescription - -rowField : Decode RowField _ -rowField = - name <- await cStr - tableOid <- await i32 - attributeNumber <- await i16 - dataTypeOid <- await i32 - dataTypeSize <- await i16 - typeModifier <- await i32 - formatCode <- map i16 - - column = - if tableOid != 0 && attributeNumber != 0 then - Ok { tableOid, attributeNumber } - else - Err NotAColumn - - { - name, - column, - dataTypeOid, - dataTypeSize, - typeModifier, - formatCode, - } - -dataRow : Decode Message _ -dataRow = - columnCount <- await i16 - - fixedList - columnCount - ( - valueLen <- await i32 - - if valueLen == -1 then - succeed [] - else - take (Num.toU64 valueLen) \x -> x - ) - |> map DataRow - -fixedList = \count, itemDecode -> - collected <- loop (List.withCapacity (Num.toU64 count)) - - item <- map itemDecode - - added = List.append collected item - - if List.len added == Num.toU64 count then - Done added - else - Loop added +ParameterField : { + data_type_oid : I32, +} -commandComplete : Decode Message _ -commandComplete = - map cStr CommandComplete +parameter_description : Decode Message _ +parameter_description = + await( + i16, + |field_count| + if field_count == 0 then + succeed(ParameterDescription([])) + else + fixed_list(field_count, parameter_field) + |> map(ParameterDescription), + ) + +parameter_field : Decode ParameterField _ +parameter_field = + await( + i32, + |data_type_oid| + succeed({ data_type_oid }), + ) + +row_description : Decode Message _ +row_description = + await( + i16, + |field_count| + fixed_list(field_count, row_field) + |> map(RowDescription), + ) + +row_field : Decode RowField _ +row_field = + await( + c_str, + |name| + await( + i32, + |table_oid| + await( + i16, + |attribute_number| + await( + i32, + |data_type_oid| + await( + i16, + |data_type_size| + await( + i32, + |type_modifier| + map( + i16, + |format_code| + column = + if table_oid != 0 and attribute_number != 0 then + Ok({ table_oid, attribute_number }) + else + Err(NotAColumn) + + { + name, + column, + data_type_oid, + data_type_size, + type_modifier, + format_code, + }, + ), + ), + ), + ), + ), + ), + ) + +data_row : Decode Message _ +data_row = + await( + i16, + |column_count| + fixed_list( + column_count, + await( + i32, + |value_len| + if value_len == -1 then + succeed([]) + else + take(Num.to_u64(value_len), |x| x), + ), + ) + |> map(DataRow), + ) + +fixed_list = |count, item_decode| + loop( + List.with_capacity(Num.to_u64(count)), + |collected| + map( + item_decode, + |item| + added = List.append(collected, item) + + if List.len(added) == Num.to_u64(count) then + Done(added) + else + Loop(added), + ), + ) + +command_complete : Decode Message _ +command_complete = + map(c_str, CommandComplete) diff --git a/src/Protocol/Frontend.roc b/src/Protocol/Frontend.roc index 91bcea6..7a0c943 100644 --- a/src/Protocol/Frontend.roc +++ b/src/Protocol/Frontend.roc @@ -1,171 +1,204 @@ module [ startup, - passwordMessage, + password_message, terminate, parse, bind, FormatCode, - describePortal, - describeStatement, + describe_portal, + describe_statement, execute, - closeStatement, + close_statement, sync, ] import Bytes.Encode exposing [ sequence, - nullTerminate, + null_terminate, u8, i16, i32, - cStr, + c_str, ] startup : { user : Str, database : Str } -> List U8 -startup = \{ user, database } -> - sequence [ - # Version number - i16 3, - i16 0, - # Encoding - sequence [ - param "client_encoding" "utf_8", - param "user" user, - param "database" database, - ] - |> nullTerminate, - ] - |> prependLength +startup = |{ user, database }| + sequence( + [ + # Version number + i16(3), + i16(0), + # Encoding + sequence( + [ + param("client_encoding", "utf_8"), + param("user", user), + param("database", database), + ], + ) + |> null_terminate, + ], + ) + |> prepend_length param : Str, Str -> List U8 -param = \key, value -> - sequence [ - cStr key, - cStr value, - ] - -passwordMessage : Str -> List U8 -passwordMessage = \pwd -> - message 'p' [ - cStr pwd, - ] +param = |key, value| + sequence( + [ + c_str(key), + c_str(value), + ], + ) + +password_message : Str -> List U8 +password_message = |pwd| + message( + 'p', + [ + c_str(pwd), + ], + ) terminate : List U8 terminate = - message 'X' [] - -parse : { sql : Str, name ? Str, paramTypeIds ? List I32 } -> List U8 -parse = \{ sql, name ? "", paramTypeIds ? [] } -> - message 'P' [ - cStr name, - cStr sql, - array paramTypeIds i32, - ] + message('X', []) + +parse : { sql : Str, name ?? Str, param_type_ids ?? List I32 } -> List U8 +parse = |{ sql, name ?? "", param_type_ids ?? [] }| + message( + 'P', + [ + c_str(name), + c_str(sql), + array(param_type_ids, i32), + ], + ) bind : { - portal ? Str, - preparedStatement ? Str, - formatCodes ? List FormatCode, - paramValues : List [Null, Value (List U8)], - columnFormatCodes ? List FormatCode, + portal ?? Str, + prepared_statement ?? Str, + format_codes ?? List FormatCode, + param_values : List [Null, Value (List U8)], + column_format_codes ?? List FormatCode, } -> List U8 -bind = \{ portal ? "", preparedStatement ? "", formatCodes ? [], paramValues, columnFormatCodes ? [] } -> - message 'B' [ - cStr portal, - cStr preparedStatement, - array formatCodes formatCode, - array - paramValues - (\value -> - when value is - Null -> - i32 -1 - - Value b -> - bytes b +bind = |{ portal ?? "", prepared_statement ?? "", format_codes ?? [], param_values, column_format_codes ?? [] }| + message( + 'B', + [ + c_str(portal), + c_str(prepared_statement), + array(format_codes, format_code), + array( + param_values, + |value| + when value is + Null -> + i32(-1) + + Value(b) -> + bytes(b), ), - array columnFormatCodes formatCode, - ] + array(column_format_codes, format_code), + ], + ) FormatCode : [Text, Binary] -formatCode : FormatCode -> List U8 -formatCode = \code -> +format_code : FormatCode -> List U8 +format_code = |code| when code is Text -> - i16 0 + i16(0) Binary -> - i16 1 - -describePortal : { name ? Str } -> List U8 -describePortal = \{ name ? "" } -> - message 'D' [ - u8 'P', - cStr name, - ] - -describeStatement : { name ? Str } -> List U8 -describeStatement = \{ name ? "" } -> - message 'D' [ - u8 'S', - cStr name, - ] - -execute : { portal ? Str, limit ? [None, Limit I32] } -> List U8 -execute = \{ portal ? "", limit ? None } -> - limitOrZero = + i16(1) + +describe_portal : { name ?? Str } -> List U8 +describe_portal = |{ name ?? "" }| + message( + 'D', + [ + u8('P'), + c_str(name), + ], + ) + +describe_statement : { name ?? Str } -> List U8 +describe_statement = |{ name ?? "" }| + message( + 'D', + [ + u8('S'), + c_str(name), + ], + ) + +execute : { portal ?? Str, limit ?? [None, Limit I32] } -> List U8 +execute = |{ portal ?? "", limit ?? None }| + limit_or_zero = when limit is None -> 0 - Limit lim -> + Limit(lim) -> lim - message 'E' [ - cStr portal, - i32 limitOrZero, - ] - -closeStatement : { name : Str } -> List U8 -closeStatement = \{ name } -> - message 'C' [ - u8 'S', - cStr name, - ] + message( + 'E', + [ + c_str(portal), + i32(limit_or_zero), + ], + ) + +close_statement : { name : Str } -> List U8 +close_statement = |{ name }| + message( + 'C', + [ + u8('S'), + c_str(name), + ], + ) sync : List U8 sync = - message 'S' [] + message('S', []) array : List item, (item -> List U8) -> List U8 -array = \items, itemEncode -> - sequence [ - i16 (List.len items |> Num.toI16), - sequence (List.map items itemEncode), - ] +array = |items, item_encode| + sequence( + [ + i16((List.len(items) |> Num.to_i16)), + sequence(List.map(items, item_encode)), + ], + ) bytes : List U8 -> List U8 -bytes = \value -> - sequence [ - i32 (List.len value |> Num.toI32), - value, - ] +bytes = |value| + sequence( + [ + i32((List.len(value) |> Num.to_i32)), + value, + ], + ) message : U8, List (List U8) -> List U8 -message = \msgType, content -> - sequence [ - u8 msgType, - prependLength (sequence content), - ] - -prependLength : List U8 -> List U8 -prependLength = \msg -> - totalLength = - List.len msg +message = |msg_type, content| + sequence( + [ + u8(msg_type), + prepend_length(sequence(content)), + ], + ) + +prepend_length : List U8 -> List U8 +prepend_length = |msg| + total_length = + List.len(msg) + 4 - |> Num.toI32 + |> Num.to_i32 - List.concat (i32 totalLength) msg + List.concat(i32(total_length), msg) diff --git a/src/Sql.roc b/src/Sql.roc index dd8d94f..5522dab 100644 --- a/src/Sql.roc +++ b/src/Sql.roc @@ -4,37 +4,37 @@ module [ NullableExpr, Select, Query, - queryAll, - queryOne, - querySelection, - compileQuery, + query_all, + query_one, + query_selection, + compile_query, from, select, where, join, on, - leftJoin, - useOuter, + left_join, + use_outer, limit, offset, - orderBy, + order_by, asc, desc, Order, identifier, - discardPhantom, + discard_phantom, i16, i32, i64, str, null, - asNullable, - isNull, - isNotNull, - isDistinctFrom, - isNotDistinctFrom, - nullableEq, - nullableNeq, + as_nullable, + is_null, + is_not_null, + is_distinct_from, + is_not_distinct_from, + nullable_eq, + nullable_neq, lt, lte, eq, @@ -45,11 +45,11 @@ module [ like, ilike, concat, - and, - or, - andList, - orList, - not, + and_, + or_, + and_list, + or_list, + not_, true, false, add, @@ -61,10 +61,10 @@ module [ with, into, map, - selectionList, + selection_list, row, - rowArray, - tryMapQuery, + row_array, + try_map_query, ] import pg.Pg.Cmd exposing [Binding] @@ -86,9 +86,9 @@ Env : { aliases : Dict Str U8, } -emptyEnv : Env -emptyEnv = { - aliases: Dict.withCapacity 4, +empty_env : Env +empty_env = { + aliases: Dict.with_capacity(4), } Query a err := State Env { from : Sql, options : SelectOptions a } err @@ -101,187 +101,217 @@ Table table : { } from : Table table, (table -> Select a err) -> Query a err -from = \table, next -> @Query (fromHelp table next) - -fromHelp = \table, next -> - alias <- addAlias table.alias |> State.bind - - options <- table.columns alias - |> next - |> unwrapSelect - |> State.map - - { - from: [Raw " from $(table.schema).$(table.name) as $(alias)"], - options, - } - -addAlias : Str -> State Env Str err -addAlias = \wanted -> - env <- State.get |> State.bind - - when Dict.get env.aliases wanted is - Ok count -> - strCount = Num.toStr count - - newEnv = { env & - aliases: env.aliases |> Dict.insert wanted (count + 1), - } - - _ <- State.put newEnv |> State.map - - "$(wanted)_$(strCount)" - - Err KeyNotFound -> - newEnv = { env & - aliases: env.aliases |> Dict.insert wanted 1, - } - - _ <- State.put newEnv |> State.map - - wanted - -queryAll : Query (Selection a []) [] -> Pg.Cmd.Cmd _ _ -queryAll = \qs -> - query = buildBareQuery qs emptyEnv - { sql, params } = compileSql query.sql - - Pg.Cmd.new sql - |> Pg.Cmd.bind params - |> Pg.Cmd.withCustomDecode \result -> - cells <- Pg.Result.rows result |> List.mapTry - - cells - |> query.decode - |> Result.mapErr DecodeErr - -queryOne : Query (Selection a []) [] -> Pg.Cmd.Cmd _ _ -queryOne = \qs -> - query = buildBareQuery qs emptyEnv - { sql, params } = compileSql query.sql - - Pg.Cmd.new sql - |> Pg.Cmd.bind params - |> Pg.Cmd.withCustomDecode \result -> - rows = Pg.Result.rows result - - when rows |> List.takeFirst 1 is - [cells] -> - cells - |> query.decode - |> Result.mapErr DecodeErr - - _ -> - Err EmptyResult - -querySelection : Selection a err -> Result (Pg.Cmd.Cmd _ _) err -querySelection = \@Selection ss -> - (sel, _) <- State.attempt ss emptyEnv |> Result.map - - { sql, params } = - # TODO: Simplify to no-op cmd - if List.isEmpty sel.columns then - [Raw "select 1"] - |> compileSql - else - commaJoin sel.columns - |> List.prepend (Raw "select ") - |> compileSql - - Pg.Cmd.new sql - |> Pg.Cmd.bind params - |> Pg.Cmd.withCustomDecode \result -> - rows = Pg.Result.rows result - - when rows |> List.takeFirst 1 is - [cells] -> - cells - |> sel.decode - |> Result.mapErr DecodeErr - - _ -> - Err EmptyResult - -buildBareQuery : Query (Selection a []) [], +from = |table, next| @Query(from_help(table, next)) + +from_help = |table, next| + add_alias(table.alias) + |> State.bind( + |alias| + table.columns(alias) + |> next + |> unwrap_select + |> State.map( + |options| { + from: [Raw(" from ${table.schema}.${table.name} as ${alias}")], + options, + }, + ), + ) + +add_alias : Str -> State Env Str err +add_alias = |wanted| + State.get + |> State.bind( + |env| + when Dict.get(env.aliases, wanted) is + Ok(count) -> + str_count = Num.to_str(count) + + new_env = { env & + aliases: env.aliases |> Dict.insert(wanted, (count + 1)), + } + + State.put(new_env) + |> State.map( + |_| + "${wanted}_${str_count}", + ) + + Err(KeyNotFound) -> + new_env = { env & + aliases: env.aliases |> Dict.insert(wanted, 1), + } + + State.put(new_env) + |> State.map( + |_| + wanted, + ), + ) + +query_all : Query (Selection a []) [] -> Pg.Cmd.Cmd _ _ +query_all = |qs| + query = build_bare_query(qs, empty_env) + { sql, params } = compile_sql(query.sql) + + Pg.Cmd.new(sql) + |> Pg.Cmd.bind(params) + |> Pg.Cmd.with_custom_decode( + |result| + Pg.Result.rows(result) + |> List.map_try( + |cells| + cells + |> query.decode + |> Result.map_err(DecodeErr), + ), + ) + +query_one : Query (Selection a []) [] -> Pg.Cmd.Cmd _ _ +query_one = |qs| + query = build_bare_query(qs, empty_env) + { sql, params } = compile_sql(query.sql) + + Pg.Cmd.new(sql) + |> Pg.Cmd.bind(params) + |> Pg.Cmd.with_custom_decode( + |result| + rows = Pg.Result.rows(result) + + when rows |> List.take_first(1) is + [cells] -> + cells + |> query.decode + |> Result.map_err(DecodeErr) + + _ -> + Err(EmptyResult), + ) + +query_selection : Selection a err -> Result (Pg.Cmd.Cmd _ _) err +query_selection = |@Selection(ss)| + State.attempt(ss, empty_env) + |> Result.map_ok( + |(sel, _)| + { sql, params } = + # TODO: Simplify to no-op cmd + if List.is_empty(sel.columns) then + [Raw("select 1")] + |> compile_sql + else + comma_join(sel.columns) + |> List.prepend(Raw("select ")) + |> compile_sql + + Pg.Cmd.new(sql) + |> Pg.Cmd.bind(params) + |> Pg.Cmd.with_custom_decode( + |result| + rows = Pg.Result.rows(result) + + when rows |> List.take_first(1) is + [cells] -> + cells + |> sel.decode + |> Result.map_err(DecodeErr) + + _ -> + Err(EmptyResult), + ), + ) + +build_bare_query : + Query (Selection a []) [], Env -> { sql : Sql, decode : List (List U8) -> Result a Sql.Types.DecodeErr, } -buildBareQuery = \@Query qs, initEnv -> +build_bare_query = |@Query(qs), init_env| combined = - query <- State.bind qs - sel <- query.options.value |> unwrapSelection |> State.map - - { query, sel } + State.bind( + qs, + |query| + query.options.value + |> unwrap_selection + |> State.map( + |sel| + { query, sel }, + ), + ) - (result, _) = State.perform combined initEnv + (result, _) = State.perform(combined, init_env) { - sql: querySql result.query result.sel.columns Bare, + sql: query_sql(result.query, result.sel.columns, Bare), decode: result.sel.decode, } -querySql = \query, columns, columnWrapper -> - columnsSql = - when columnWrapper is +query_sql = |query, columns, column_wrapper| + columns_sql = + when column_wrapper is Bare -> - commaJoin columns + comma_join(columns) Row -> - [Raw "row("] - |> List.concat (commaJoin columns) - |> List.append (Raw ")") + [Raw("row(")] + |> List.concat(comma_join(columns)) + |> List.append(Raw(")")) - joinsSql = - List.join query.options.joins + joins_sql = + List.join(query.options.joins) - [Raw "select "] - |> List.reserve + [Raw("select ")] + |> List.reserve( ( - List.len columnsSql - + List.len query.from - + List.len joinsSql - + List.len query.options.where - + List.len query.options.orderBy - + List.len query.options.limit - + List.len query.options.offset - ) - |> List.concat columnsSql - |> List.concat query.from - |> List.concat joinsSql - |> List.concat query.options.where - |> List.concat query.options.orderBy - |> List.concat query.options.limit - |> List.concat query.options.offset - -compileQuery = \qs -> - buildBareQuery qs emptyEnv + List.len(columns_sql) + + List.len(query.from) + + List.len(joins_sql) + + List.len(query.options.where) + + List.len(query.options.order_by) + + List.len(query.options.limit) + + List.len(query.options.offset) + ), + ) + |> List.concat(columns_sql) + |> List.concat(query.from) + |> List.concat(joins_sql) + |> List.concat(query.options.where) + |> List.concat(query.options.order_by) + |> List.concat(query.options.limit) + |> List.concat(query.options.offset) + +compile_query = |qs| + build_bare_query(qs, empty_env) |> .sql - |> compileSql + |> compile_sql # Advanced: Failable query building -tryMapQuery : Query a err, (a -> Result b err) -> Query b err -tryMapQuery = \@Query qs, fn -> - @Query (tryMapQueryHelp qs fn) - -tryMapQueryHelp = \qs, fn -> - query <- State.bind qs - - fn query.options.value - |> Result.map \new -> { - from: query.from, - options: { - joins: query.options.joins, - where: query.options.where, - orderBy: query.options.orderBy, - limit: query.options.limit, - offset: query.options.offset, - value: new, - }, - } - |> State.fromResult +try_map_query : Query a err, (a -> Result b err) -> Query b err +try_map_query = |@Query(qs), fn| + @Query(try_map_query_help(qs, fn)) + +try_map_query_help = |qs, fn| + State.bind( + qs, + |query| + fn(query.options.value) + |> Result.map_ok( + |new| { + from: query.from, + options: { + joins: query.options.joins, + where: query.options.where, + order_by: query.options.order_by, + limit: query.options.limit, + offset: query.options.offset, + value: new, + }, + }, + ) + |> State.from_result, + ) # Options @@ -290,138 +320,153 @@ Select a err := State Env (SelectOptions a) err SelectOptions a : { joins : List Sql, where : Sql, - orderBy : Sql, + order_by : Sql, limit : Sql, offset : Sql, value : a, } -unwrapSelect = \@Select state -> state +unwrap_select = |@Select(state)| state join : Table table, (table -> Expr (PgBool *) *), (table -> Select a err) -> Select a err -join = \table, onExpr, next -> - @Select (joinHelp table onExpr next) - -joinHelp = \table, onExpr, next -> - alias <- addAlias table.alias |> State.bind - columns = table.columns alias +join = |table, on_expr, next| + @Select(join_help(table, on_expr, next)) - (@Expr { sql: onSql }) = onExpr columns +join_help = |table, on_expr, next| + add_alias(table.alias) + |> State.bind( + |alias| + columns = table.columns(alias) - options <- - next columns - |> unwrapSelect - |> State.map + @Expr({ sql: on_sql }) = on_expr(columns) - joinSql = - List.prepend onSql (Raw " join $(table.schema).$(table.name) as $(alias) on ") + next(columns) + |> unwrap_select + |> State.map( + |options| + join_sql = + List.prepend(on_sql, Raw(" join ${table.schema}.${table.name} as ${alias} on ")) - { options & joins: options.joins |> List.prepend joinSql } + { options & joins: options.joins |> List.prepend(join_sql) }, + ), + ) Outer table := table -leftJoin : Table table, (table -> Expr (PgBool *) *), (Outer table -> Select a err) -> Select a err -leftJoin = \table, onExpr, next -> - @Select (leftJoinHelp table onExpr next) - -leftJoinHelp = \table, onExpr, next -> - alias <- addAlias table.alias |> State.bind - columns = table.columns alias +left_join : Table table, (table -> Expr (PgBool *) *), (Outer table -> Select a err) -> Select a err +left_join = |table, on_expr, next| + @Select(left_join_help(table, on_expr, next)) - (@Expr { sql: onSql }) = onExpr columns +left_join_help = |table, on_expr, next| + add_alias(table.alias) + |> State.bind( + |alias| + columns = table.columns(alias) - options <- - next (@Outer columns) - |> unwrapSelect - |> State.map + @Expr({ sql: on_sql }) = on_expr(columns) - joinSql = - List.prepend onSql (Raw " left join $(table.schema).$(table.name) as $(alias) on ") + next(@Outer(columns)) + |> unwrap_select + |> State.map( + |options| + join_sql = + List.prepend(on_sql, Raw(" left join ${table.schema}.${table.name} as ${alias} on ")) - { options & joins: options.joins |> List.prepend joinSql } + { options & joins: options.joins |> List.prepend(join_sql) }, + ), + ) -useOuter : Outer table, (table -> Expr pg a) -> NullableExpr pg a -useOuter = \@Outer table, expr -> +use_outer : Outer table, (table -> Expr pg a) -> NullableExpr pg a +use_outer = |@Outer(table), expr| # DESIGN: break into its own module? might help combine multiple outer joins - asNullable (expr table) + as_nullable(expr(table)) -on = \toA, b -> \table -> toA table |> eq b +on = |to_a, b| |table| to_a(table) |> eq(b) select : a -> Select a * -select = \a -> +select = |a| { - joins: List.withCapacity 4, + joins: List.with_capacity(4), where: [], limit: [], offset: [], - orderBy: [], + order_by: [], value: a, } |> State.ok |> @Select -where : Select a err, Expr (PgBool *) * -> Select a err +where : Select _ _, Expr (PgBool _) _ -> Select _ _ where = - options, (@Expr expr) <- updateOptions - - newWhere = - if List.isEmpty options.where then - List.prepend expr.sql (Raw " where ") - else - options.where - |> List.reserve (List.len expr.sql + 1) - |> List.append (Raw " and ") - |> List.concat expr.sql - - { options & where: newWhere } - -limit : Select a err, U64 -> Select a err + update_options( + |options, @Expr(expr)| + new_where = + if List.is_empty(options.where) then + List.prepend(expr.sql, Raw(" where ")) + else + options.where + |> List.reserve((List.len(expr.sql) + 1)) + |> List.append(Raw(" and ")) + |> List.concat(expr.sql) + + { options & where: new_where }, + ) + +limit : Select _ _, U64 -> Select _ _ limit = - options, max <- updateOptions - { options & limit: [Raw " limit ", Param (Pg.Cmd.u64 max)] } + update_options( + |options, max| + { options & limit: [Raw(" limit "), Param(Pg.Cmd.u64(max))] }, + ) -offset : Select a err, U64 -> Select a err +offset : Select _ _, U64 -> Select _ _ offset = - options, start <- updateOptions - { options & offset: [Raw " offset ", Param (Pg.Cmd.u64 start)] } + update_options( + |options, start| + { options & offset: [Raw(" offset "), Param(Pg.Cmd.u64(start))] }, + ) Order := { sql : Sql, direction : [Asc, Desc] } asc : Expr (PgCmp *) * -> Order -asc = \@Expr expr -> @Order { sql: expr.sql, direction: Asc } +asc = |@Expr(expr)| @Order({ sql: expr.sql, direction: Asc }) desc : Expr (PgCmp *) * -> Order -desc = \@Expr expr -> @Order { sql: expr.sql, direction: Desc } - -orderBy : Select a err, List Order -> Select a err -orderBy = - options, order <- updateOptions - - sql = - order - |> List.map \@Order level -> - direction = - when level.direction is - Asc -> - " asc" - - Desc -> - " desc" - - level.sql |> List.append (Raw direction) - |> commaJoin - |> List.prepend (Raw " order by ") - - { options & orderBy: sql } - -updateOptions : (SelectOptions a, arg -> SelectOptions b) -> (Select a err, arg -> Select b err) -updateOptions = \fn -> - \@Select next, arg -> - @Select (State.map next \options -> fn options arg) +desc = |@Expr(expr)| @Order({ sql: expr.sql, direction: Desc }) + +order_by : Select _ _, List Order -> Select _ _ +order_by = + update_options( + |options, order| + sql = + order + |> List.map( + |@Order(level)| + direction = + when level.direction is + Asc -> + " asc" + + Desc -> + " desc" + + level.sql |> List.append(Raw(direction)), + ) + |> comma_join + |> List.prepend(Raw(" order by ")) + + { options & order_by: sql }, + ) + +update_options : (SelectOptions a, arg -> SelectOptions b) -> (Select a err, arg -> Select b err) +update_options = |fn| + |@Select(next), arg| + @Select(State.map(next, |options| fn(options, arg))) # Selection -Selection a err := State +Selection a err := + State Env { columns : List Sql, @@ -429,168 +474,225 @@ Selection a err := State } err -unwrapSelection = \@Selection state -> state +unwrap_selection = |@Selection(state)| state -updateSelection = \@Selection sel, fn -> @Selection (State.map sel \options -> fn options) +update_selection = |@Selection(sel), fn| @Selection(State.map(sel, |options| fn(options))) into : a -> Selection a err -into = \value -> +into = |value| { columns: [], - decode: \_ -> Ok value, + decode: |_| Ok(value), } |> State.ok |> @Selection column : Expr * a -> (Selection (a -> b) err -> Selection b err) -column = \@Expr expr -> \ss -> - sel <- updateSelection ss - apExprSel expr sel - -apExprSel = \expr, sel -> - index = List.len sel.columns - - decode = \cells -> - fn <- sel.decode cells |> Result.try - value <- cells - |> List.get index - |> Result.mapErr \OutOfBounds -> MissingColumn index - |> Result.try - a <- value - |> Sql.Types.decode expr.decode - |> Result.map - fn a +column = |@Expr(expr)| + |ss| + update_selection( + ss, + |sel| + ap_expr_sel(expr, sel), + ) + +ap_expr_sel = |expr, sel| + index = List.len(sel.columns) + + decode = |cells| + sel.decode(cells) + |> Result.try( + |fn| + cells + |> List.get(index) + |> Result.map_err(|OutOfBounds| MissingColumn(index)) + |> Result.try( + |value| + value + |> Sql.Types.decode(expr.decode) + |> Result.map_ok( + |a| + fn(a), + ), + ), + ) { - columns: sel.columns |> List.append expr.sql, + columns: sel.columns |> List.append(expr.sql), decode, } with : Selection a err -> (Selection (a -> b) err -> Selection b err) -with = \@Selection toASel -> \@Selection toFnSel -> - @Selection (withHelp toASel toFnSel) - -withHelp = \toASel, toFnSel -> - env <- State.get |> State.bind - - result = - (aSel, _) <- State.attempt toASel env |> Result.try - (fnSel, _) <- State.attempt toFnSel env |> Result.map - - count = List.len fnSel.columns - - decode = \cells -> - fn <- fnSel.decode cells |> Result.try - a <- cells - |> List.dropFirst count - |> aSel.decode - |> Result.map - fn a - - { - columns: fnSel.columns |> List.concat aSel.columns, - decode, - } - - State.fromResult result +with = |@Selection(to_a_sel)| + |@Selection(to_fn_sel)| + @Selection(with_help(to_a_sel, to_fn_sel)) + +with_help = |to_a_sel, to_fn_sel| + State.get + |> State.bind( + |env| + result = + State.attempt(to_a_sel, env) + |> Result.try( + |(a_sel, _)| + State.attempt(to_fn_sel, env) + |> Result.map_ok( + |(fn_sel, _)| + count = List.len(fn_sel.columns) + + decode = |cells| + fn_sel.decode(cells) + |> Result.try( + |fn| + cells + |> List.drop_first(count) + |> a_sel.decode + |> Result.map_ok( + |a| + fn(a), + ), + ) + + { + columns: fn_sel.columns |> List.concat(a_sel.columns), + decode, + }, + ), + ) + + State.from_result(result), + ) row : Query (Selection a err) err -> (Selection (a -> b) err -> Selection b err) -row = \@Query qs -> \@Selection ps -> - @Selection (rowHelp qs ps) - -rowHelp = \qs, ps -> - sel <- State.bind ps - env <- State.get |> State.bind - - rowState = - query <- State.bind qs - rowSel <- query.options.value |> unwrapSelection |> State.map - - limited = - options = query.options - { query & options: { options & limit: [Raw " limit 1"] } } - - { query: limited, sel: rowSel } - - State.attempt rowState env - |> Result.map \(rowQ, _) -> - sql = - [Raw "("] - |> List.concat (querySql rowQ.query rowQ.sel.columns Row) - |> List.append (Raw ")") - - { - sql, - decode: Sql.Types.row rowQ.sel.decode, - } - |> apExprSel sel - |> State.fromResult - -rowArray : Query (Selection a err) err -> (Selection (List a -> b) err -> Selection b err) -rowArray = \@Query qs -> \@Selection ps -> @Selection (rowArrayHelp qs ps) - -rowArrayHelp = \qs, ps -> - sel <- State.bind ps - env <- State.get |> State.bind - - rowState = - query <- State.bind qs - rowSel <- query.options.value |> unwrapSelection |> State.map - - { - sql: querySql query rowSel.columns Row, - decode: rowSel.decode, - } - - State.attempt rowState env - |> Result.map \(rowQ, _) -> - sql = - [Raw "(select array("] - |> List.concat rowQ.sql - |> List.append (Raw "))") - - decode = Sql.Types.array (Sql.Types.row rowQ.decode) - - expr = { sql, decode } - - apExprSel expr sel - |> State.fromResult - -selectionList : List (Selection a err) -> Selection (List a) err -selectionList = \sels -> @Selection (selectionListHelp sels) - -selectionListHelp = \sels -> - env <- State.get |> State.bind - - sels - |> List.mapTry \@Selection sel -> State.attempt sel env |> Result.map .0 - |> Result.map \allSels -> - decode = \cells -> - allSels - |> List.walkTry (cells, List.withCapacity (List.len sels)) decodeSel - |> Result.map .1 - - decodeSel = \(remainingCells, decodedSels), sel -> - { before, others } = List.split remainingCells (List.len sel.columns) - - decoded <- sel.decode before |> Result.map - - (others, List.append decodedSels decoded) - - { - columns: allSels |> List.joinMap .columns, - decode, - } - |> State.fromResult +row = |@Query(qs)| + |@Selection(ps)| + @Selection(row_help(qs, ps)) + +row_help = |qs, ps| + State.bind( + ps, + |sel| + State.get + |> State.bind( + |env| + row_state = + State.bind( + qs, + |query| + query.options.value + |> unwrap_selection + |> State.map( + |row_sel| + limited = + options = query.options + { query & options: { options & limit: [Raw(" limit 1")] } } + + { query: limited, sel: row_sel }, + ), + ) + + State.attempt(row_state, env) + |> Result.map_ok( + |(row_q, _)| + sql = + [Raw("(")] + |> List.concat(query_sql(row_q.query, row_q.sel.columns, Row)) + |> List.append(Raw(")")) + + { + sql, + decode: Sql.Types.row(row_q.sel.decode), + } + |> ap_expr_sel(sel), + ) + |> State.from_result, + ), + ) + +row_array : Query (Selection a err) err -> (Selection (List a -> b) err -> Selection b err) +row_array = |@Query(qs)| |@Selection(ps)| @Selection(row_array_help(qs, ps)) + +row_array_help = |qs, ps| + State.bind( + ps, + |sel| + State.get + |> State.bind( + |env| + row_state = + State.bind( + qs, + |query| + query.options.value + |> unwrap_selection + |> State.map( + |row_sel| { + sql: query_sql(query, row_sel.columns, Row), + decode: row_sel.decode, + }, + ), + ) + + State.attempt(row_state, env) + |> Result.map_ok( + |(row_q, _)| + sql = + [Raw("(select array(")] + |> List.concat(row_q.sql) + |> List.append(Raw("))")) + + decode = Sql.Types.array(Sql.Types.row(row_q.decode)) + + expr = { sql, decode } + + ap_expr_sel(expr, sel), + ) + |> State.from_result, + ), + ) + +selection_list : List (Selection a err) -> Selection (List a) err +selection_list = |sels| @Selection(selection_list_help(sels)) + +selection_list_help = |sels| + State.get + |> State.bind( + |env| + sels + |> List.map_try(|@Selection(sel)| State.attempt(sel, env) |> Result.map_ok(.0)) + |> Result.map_ok( + |all_sels| + decode = |cells| + all_sels + |> List.walk_try((cells, List.with_capacity(List.len(sels))), decode_sel) + |> Result.map_ok(.1) + + decode_sel = |(remaining_cells, decoded_sels), sel| + { before, others } = List.split_at(remaining_cells, List.len(sel.columns)) + + sel.decode(before) + |> Result.map_ok( + |decoded| + (others, List.append(decoded_sels, decoded)), + ) + + { + columns: all_sels |> List.join_map(.columns), + decode, + }, + ) + |> State.from_result, + ) map : Selection a err, (a -> b) -> Selection b err -map = \ss, fn -> - sel <- updateSelection ss - - { - columns: sel.columns, - decode: \cells -> Result.map (sel.decode cells) fn, - } +map = |ss, fn| + update_selection( + ss, + |sel| { + columns: sel.columns, + decode: |cells| Result.map_ok(sel.decode(cells), fn), + }, + ) # Expr @@ -602,10 +704,13 @@ Expr pg roc := { NullableExpr pg roc : Expr (Nullable pg) (Nullable roc) identifier : Str, Str, Decode pg roc -> Expr pg roc -identifier = \table, col, decode -> @Expr { - sql: [Raw "$(table).$(col)"], - decode, - } +identifier = |table, col, decode| + @Expr( + { + sql: [Raw("${table}.${col}")], + decode, + }, + ) ## Discards the phantom type variable that represents the SQL-level type. ## @@ -613,248 +718,300 @@ identifier = \table, col, decode -> @Expr { ## from writing an expression that you know it's valid. ## ## IMPORTANT: The compiled expression won't include a SQL cast! -discardPhantom : Expr * roc -> Expr * roc -discardPhantom = \@Expr expr -> @Expr { - sql: expr.sql, - decode: Sql.Types.discardPhantom expr.decode, - } +discard_phantom : Expr * roc -> Expr * roc +discard_phantom = |@Expr(expr)| + @Expr( + { + sql: expr.sql, + decode: Sql.Types.discard_phantom(expr.decode), + }, + ) # Expr: Literals i16 : I16 -> Expr (PgI16 *) I16 -i16 = \value -> @Expr { - sql: [Param (Pg.Cmd.i16 value)], - decode: Sql.Types.i16, - } +i16 = |value| + @Expr( + { + sql: [Param(Pg.Cmd.i16(value))], + decode: Sql.Types.i16, + }, + ) i32 : I32 -> Expr (PgI32 *) I32 -i32 = \value -> @Expr { - sql: [Param (Pg.Cmd.i32 value)], - decode: Sql.Types.i32, - } +i32 = |value| + @Expr( + { + sql: [Param(Pg.Cmd.i32(value))], + decode: Sql.Types.i32, + }, + ) i64 : I64 -> Expr (PgI64 *) I64 -i64 = \value -> @Expr { - sql: [Param (Pg.Cmd.i64 value)], - decode: Sql.Types.i64, - } +i64 = |value| + @Expr( + { + sql: [Param(Pg.Cmd.i64(value))], + decode: Sql.Types.i64, + }, + ) str : Str -> Expr (PgText *) Str -str = \value -> - @Expr { - sql: [Param (Pg.Cmd.str value)], - decode: Sql.Types.str, - } - -null : NullableExpr * [] -null = @Expr { - sql: [Raw "null"], - decode: Sql.Types.nullable (Sql.Types.fail "not null"), -} +str = |value| + @Expr( + { + sql: [Param(Pg.Cmd.str(value))], + decode: Sql.Types.str, + }, + ) -asNullable : Expr pg roc -> NullableExpr pg roc -asNullable = \@Expr a -> - @Expr { - sql: a.sql, - decode: Sql.Types.nullable a.decode, - } +null : NullableExpr _ [] +null = @Expr( + { + sql: [Raw("null")], + decode: Sql.Types.nullable(Sql.Types.fail("not null")), + }, +) + +as_nullable : Expr pg roc -> NullableExpr pg roc +as_nullable = |@Expr(a)| + @Expr( + { + sql: a.sql, + decode: Sql.Types.nullable(a.decode), + }, + ) # Expr: Comparison -isNull : NullableExpr * * -> Expr (PgBool *) Bool -isNull = \@Expr a -> - @Expr { - sql: a.sql |> List.append (Raw " is null"), - decode: Sql.Types.bool, - } +is_null : NullableExpr _ _ -> Expr (PgBool _) Bool +is_null = |@Expr(a)| + @Expr( + { + sql: a.sql |> List.append(Raw(" is null")), + decode: Sql.Types.bool, + }, + ) -isNotNull : NullableExpr * * -> Expr (PgBool *) Bool -isNotNull = \@Expr a -> - @Expr { - sql: a.sql |> List.append (Raw " is not null"), - decode: Sql.Types.bool, - } +is_not_null : NullableExpr _ _ -> Expr (PgBool _) Bool +is_not_null = |@Expr(a)| + @Expr( + { + sql: a.sql |> List.append(Raw(" is not null")), + decode: Sql.Types.bool, + }, + ) -isDistinctFrom : NullableExpr (PgCmp a) *, NullableExpr (PgCmp a) * -> Expr (PgBool *) Bool -isDistinctFrom = \a, b -> boolOp a "is distinct from" b +is_distinct_from : NullableExpr (PgCmp a) *, NullableExpr (PgCmp a) * -> Expr (PgBool *) Bool +is_distinct_from = |a, b| bool_op(a, "is distinct from", b) -isNotDistinctFrom : NullableExpr (PgCmp a) *, NullableExpr (PgCmp a) * -> Expr (PgBool *) Bool -isNotDistinctFrom = \a, b -> boolOp a "is not distinct from" b +is_not_distinct_from : NullableExpr (PgCmp a) *, NullableExpr (PgCmp a) * -> Expr (PgBool *) Bool +is_not_distinct_from = |a, b| bool_op(a, "is not distinct from", b) -nullableEq = isNotDistinctFrom +nullable_eq = is_not_distinct_from -nullableNeq = isDistinctFrom +nullable_neq = is_distinct_from concat : Expr (PgText *) *, Expr (PgText *) * -> Expr (PgText *) Str -concat = \@Expr a, @Expr b -> - @Expr { - sql: binOp a.sql "||" b.sql, - decode: Sql.Types.str, - } +concat = |@Expr(a), @Expr(b)| + @Expr( + { + sql: bin_op(a.sql, "||", b.sql), + decode: Sql.Types.str, + }, + ) lt : Expr (PgCmp a) *, Expr (PgCmp a) * -> Expr (PgBool *) Bool -lt = \a, b -> boolOp a "<" b +lt = |a, b| bool_op(a, "<", b) lte : Expr (PgCmp a) *, Expr (PgCmp a) * -> Expr (PgBool *) Bool -lte = \a, b -> boolOp a "<=" b +lte = |a, b| bool_op(a, "<=", b) eq : Expr (PgCmp a) *, Expr (PgCmp a) * -> Expr (PgBool *) Bool -eq = \a, b -> boolOp a "=" b +eq = |a, b| bool_op(a, "=", b) neq : Expr (PgCmp a) *, Expr (PgCmp a) * -> Expr (PgBool *) Bool -neq = \a, b -> boolOp a "<>" b +neq = |a, b| bool_op(a, "<>", b) gte : Expr (PgCmp a) *, Expr (PgCmp a) * -> Expr (PgBool *) Bool -gte = \a, b -> boolOp a ">=" b +gte = |a, b| bool_op(a, ">=", b) gt : Expr (PgCmp a) *, Expr (PgCmp a) * -> Expr (PgBool *) Bool -gt = \a, b -> boolOp a ">" b +gt = |a, b| bool_op(a, ">", b) in : Expr (PgCmp a) *, (item -> Expr (PgCmp a) *), List item -> Expr (PgBool *) Bool -in = \@Expr needle, toExpr, haystack -> - itemToSql = \item -> - (@Expr { sql }) = toExpr item +in = |@Expr(needle), to_expr, haystack| + item_to_sql = |item| + @Expr({ sql }) = to_expr(item) sql - listItems = + list_items = haystack - |> List.map itemToSql - |> commaJoin + |> List.map(item_to_sql) + |> comma_join - inSql = + in_sql = needle.sql - |> List.reserve (List.len listItems + 2) - |> List.append (Raw " in (") - |> List.concat listItems - |> List.append (Raw ")") + |> List.reserve((List.len(list_items) + 2)) + |> List.append(Raw(" in (")) + |> List.concat(list_items) + |> List.append(Raw(")")) - @Expr { - sql: inSql, - decode: Sql.Types.bool, - } + @Expr( + { + sql: in_sql, + decode: Sql.Types.bool, + }, + ) like : Expr (PgText *) *, Expr (PgText *) * -> Expr (PgBool *) Bool -like = \a, b -> - boolOp a "like" b +like = |a, b| + bool_op(a, "like", b) ilike : Expr (PgText *) *, Expr (PgText *) * -> Expr (PgBool *) Bool -ilike = \a, b -> - boolOp a "ilike" b +ilike = |a, b| + bool_op(a, "ilike", b) -boolOp : Expr * *, Str, Expr * * -> Expr (PgBool *) Bool -boolOp = \@Expr a, op, @Expr b -> - @Expr { - sql: binOp a.sql op b.sql, - decode: Sql.Types.bool, - } +bool_op : Expr * *, Str, Expr * * -> Expr (PgBool *) Bool +bool_op = |@Expr(a), op, @Expr(b)| + @Expr( + { + sql: bin_op(a.sql, op, b.sql), + decode: Sql.Types.bool, + }, + ) # Expr: Logical # TODO: Test precendence is ok! -and : Expr (PgBool *) *, Expr (PgBool *) * -> Expr (PgBool *) Bool -and = \a, b -> boolOp a "and" b +and_ : Expr (PgBool *) *, Expr (PgBool *) * -> Expr (PgBool *) Bool +and_ = |a, b| bool_op(a, "and", b) -or : Expr (PgBool *) *, Expr (PgBool a) * -> Expr (PgBool *) Bool -or = \@Expr a, @Expr b -> - @Expr { - sql: binOpParens a.sql "or" b.sql, - decode: Sql.Types.bool, - } +or_ : Expr (PgBool *) *, Expr (PgBool a) * -> Expr (PgBool *) Bool +or_ = |@Expr(a), @Expr(b)| + @Expr( + { + sql: bin_op_parens(a.sql, "or", b.sql), + decode: Sql.Types.bool, + }, + ) -not : Expr (PgBool *) * -> Expr (PgBool *) Bool -not = \@Expr a -> - @Expr { - sql: [Raw "not ("] - |> List.reserve (List.len a.sql + 1) - |> List.concat a.sql - |> List.append (Raw ")"), - decode: Sql.Types.bool, - } +not_ : Expr (PgBool *) * -> Expr (PgBool *) Bool +not_ = |@Expr(a)| + @Expr( + { + sql: [Raw("not (")] + |> List.reserve((List.len(a.sql) + 1)) + |> List.concat(a.sql) + |> List.append(Raw(")")), + decode: Sql.Types.bool, + }, + ) -andList : List (Expr (PgBool a) Bool) -> Expr (PgBool a) Bool -andList = \exprs -> - joinBool openTrue and exprs +and_list : List (Expr (PgBool _) Bool) -> Expr (PgBool _) Bool +and_list = |exprs| + join_bool(open_true, and_, exprs) -orList : List (Expr (PgBool a) Bool) -> Expr (PgBool a) Bool -orList = \exprs -> - joinBool openFalse or exprs +or_list : List (Expr (PgBool _) Bool) -> Expr (PgBool _) Bool +or_list = |exprs| + join_bool(open_false, or_, exprs) BoolOp a : Expr (PgBool a) Bool, Expr (PgBool a) Bool -> Expr (PgBool a) Bool -joinBool : Expr (PgBool a) Bool, BoolOp a, List (Expr (PgBool a) Bool) -> Expr (PgBool a) Bool -joinBool = \default, operator, exprs -> +join_bool : Expr (PgBool _) Bool, BoolOp _, List (Expr (PgBool _) Bool) -> Expr (PgBool _) Bool +join_bool = |default, operator, exprs| # We could simplify this by using default as the initial value, # but in most cases we would produce something like (true and ...), # which is not very nice to read. result = - List.walk exprs Empty \acc, rhs -> - when acc is - Empty -> - NotEmpty rhs - - NotEmpty lhs -> - NotEmpty (operator lhs rhs) + List.walk( + exprs, + Empty, + |acc, rhs| + when acc is + Empty -> + NotEmpty(rhs) + + NotEmpty(lhs) -> + NotEmpty(operator(lhs, rhs)), + ) when result is Empty -> default - NotEmpty expr -> + NotEmpty(expr) -> expr -true : Expr (PgBool a) Bool -true = @Expr { - sql: [Raw "true"], - decode: Sql.Types.bool, -} +true : Expr (PgBool _) Bool +true = @Expr( + { + sql: [Raw("true")], + decode: Sql.Types.bool, + }, +) -false : Expr (PgBool *) Bool -false = @Expr { - sql: [Raw "false"], - decode: Sql.Types.bool, -} +false : Expr (PgBool _) Bool +false = @Expr( + { + sql: [Raw("false")], + decode: Sql.Types.bool, + }, +) # These are needed for `andList` and `orList` -openTrue = @Expr { - sql: [Raw "true"], - decode: Sql.Types.bool, -} +open_true = @Expr( + { + sql: [Raw("true")], + decode: Sql.Types.bool, + }, +) -openFalse = @Expr { - sql: [Raw "false"], - decode: Sql.Types.bool, -} +open_false = @Expr( + { + sql: [Raw("false")], + decode: Sql.Types.bool, + }, +) # Expr: Num add : Expr (PgNum pg) roc, Expr (PgNum pg) roc -> Expr (PgNum pg) roc -add = \@Expr a, @Expr b -> - @Expr { - sql: binOpParens a.sql "+" b.sql, - decode: a.decode, - } +add = |@Expr(a), @Expr(b)| + @Expr( + { + sql: bin_op_parens(a.sql, "+", b.sql), + decode: a.decode, + }, + ) sub : Expr (PgNum pg) roc, Expr (PgNum pg) roc -> Expr (PgNum pg) roc -sub = \@Expr a, @Expr b -> - @Expr { - sql: binOpParens a.sql "-" b.sql, - decode: a.decode, - } +sub = |@Expr(a), @Expr(b)| + @Expr( + { + sql: bin_op_parens(a.sql, "-", b.sql), + decode: a.decode, + }, + ) mul : Expr (PgNum pg) roc, Expr (PgNum pg) roc -> Expr (PgNum pg) roc -mul = \@Expr a, @Expr b -> - @Expr { - sql: binOp a.sql "*" b.sql, - decode: a.decode, - } +mul = |@Expr(a), @Expr(b)| + @Expr( + { + sql: bin_op(a.sql, "*", b.sql), + decode: a.decode, + }, + ) div : Expr (PgNum pg) roc, Expr (PgNum pg) roc -> Expr (PgNum pg) roc -div = \@Expr a, @Expr b -> - @Expr { - sql: binOp a.sql "/" b.sql, - decode: a.decode, - } +div = |@Expr(a), @Expr(b)| + @Expr( + { + sql: bin_op(a.sql, "/", b.sql), + decode: a.decode, + }, + ) # Sql helpers @@ -870,69 +1027,70 @@ Compiled : { params : List Binding, } -joinWith : List Sql, Sql -> Sql -joinWith = \items, sep -> +join_with : List Sql, Sql -> Sql +join_with = |items, sep| items - |> List.intersperse sep + |> List.intersperse(sep) |> List.join -commaJoin : List Sql -> Sql -commaJoin = \items -> - joinWith items [Raw ", "] +comma_join : List Sql -> Sql +comma_join = |items| + join_with(items, [Raw(", ")]) -binOp = \a, op, b -> +bin_op = |a, op, b| a - |> List.reserve (List.len b + 1) - |> List.append (Raw " $(op) ") - |> List.concat b - -binOpParens = \a, op, b -> - [Raw "("] - |> List.reserve (List.len a + List.len b + 2) - |> List.concat a - |> List.append (Raw " $(op) ") - |> List.concat b - |> List.append (Raw ")") - -compileSql : Sql -> Compiled -compileSql = \sql -> + |> List.reserve((List.len(b) + 1)) + |> List.append(Raw(" ${op} ")) + |> List.concat(b) + +bin_op_parens = |a, op, b| + [Raw("(")] + |> List.reserve((List.len(a) + List.len(b) + 2)) + |> List.concat(a) + |> List.append(Raw(" ${op} ")) + |> List.concat(b) + |> List.append(Raw(")")) + +compile_sql : Sql -> Compiled +compile_sql = |sql| sql - |> List.walk + |> List.walk( { - params: List.withCapacity 8, - sql: Str.withCapacity (List.len sql * 12), - } - addPart + params: List.with_capacity(8), + sql: Str.with_capacity((List.len(sql) * 12)), + }, + add_part, + ) -addPart : Compiled, Part -> Compiled -addPart = \{ params, sql }, part -> +add_part : Compiled, Part -> Compiled +add_part = |{ params, sql }, part| when part is - Param param -> - paramCount = List.len params + Param(param) -> + param_count = List.len(params) - { newParams, index } = + { new_params, index } = # TODO: Use a dict - indexResult = params |> List.findFirstIndex \p -> p == param + index_result = params |> List.find_first_index(|p| p == param) - when indexResult is - Ok existingIndex -> + when index_result is + Ok(existing_index) -> { - newParams: params, - index: existingIndex, + new_params: params, + index: existing_index, } - Err NotFound -> + Err(NotFound) -> { - newParams: params |> List.append param, - index: paramCount, + new_params: params |> List.append(param), + index: param_count, } - binding = Num.toStr (index + 1) + binding = Num.to_str((index + 1)) - { sql: "$(sql)$$(binding)", params: newParams } + { sql: "${sql}$${binding}", params: new_params } - Raw raw -> - { sql: Str.concat sql raw, params } + Raw(raw) -> + { sql: Str.concat(sql, raw), params } # Tests diff --git a/src/Sql/Nullable.roc b/src/Sql/Nullable.roc index 31bdbce..390e5b7 100644 --- a/src/Sql/Nullable.roc +++ b/src/Sql/Nullable.roc @@ -1,15 +1,15 @@ -module [Nullable, map, withDefault] +module [Nullable, map, with_default] Nullable a : [Null, NotNull a] map : Nullable a, (a -> b) -> Nullable b -map = \x, fn -> +map = |x, fn| when x is Null -> Null - NotNull a -> NotNull (fn a) + NotNull(a) -> NotNull(fn(a)) -withDefault : Nullable a, a -> a -withDefault = \x, def -> +with_default : Nullable a, a -> a +with_default = |x, def| when x is Null -> def - NotNull a -> a + NotNull(a) -> a diff --git a/src/Sql/Types.roc b/src/Sql/Types.roc index ec69c8e..53540c4 100644 --- a/src/Sql/Types.roc +++ b/src/Sql/Types.roc @@ -6,7 +6,7 @@ module [ succeed, fail, nullable, - discardPhantom, + discard_phantom, i16, i32, i64, @@ -45,134 +45,146 @@ DecodeErr : [ ] decode : List U8, Decode pg a -> Result a DecodeErr -decode = \bytes, @Decode fn -> - fn bytes +decode = |bytes, @Decode(fn)| + fn(bytes) map : Decode pg a, (a -> b) -> Decode pg b -map = \@Decode a, toB -> @Decode \bytes -> a bytes |> Result.map toB +map = |@Decode(a), to_b| @Decode(|bytes| a(bytes) |> Result.map_ok(to_b)) succeed : a -> Decode pg a -succeed = \value -> @Decode \_ -> Ok value +succeed = |value| @Decode(|_| Ok(value)) fail : Str -> Decode pg a -fail = \message -> @Decode \_ -> Err (Error message) +fail = |message| @Decode(|_| Err(Error(message))) nullable : Decode pg a -> Decode (Nullable pg) (Nullable a) -nullable = \@Decode sub -> - bytes <- @Decode +nullable = |@Decode(sub)| + @Decode( + |bytes| + if List.is_empty(bytes) then + # TODO: Use Null tag instead of empty list + Ok(Null) + else + sub(bytes) + |> Result.map_ok(NotNull), + ) - if List.isEmpty bytes then - # TODO: Use Null tag instead of empty list - Ok Null - else - sub bytes - |> Result.map NotNull +discard_phantom : Decode * a -> Decode * a +discard_phantom = |@Decode(sub)| @Decode(sub) -discardPhantom : Decode * a -> Decode * a -discardPhantom = \@Decode sub -> @Decode sub +i16 : Decode (PgI16 _) I16 +i16 = text_format(Str.to_i16) -i16 : Decode (PgI16 *) I16 -i16 = textFormat Str.toI16 +i32 : Decode (PgI32 _) I32 +i32 = text_format(Str.to_i32) -i32 : Decode (PgI32 *) I32 -i32 = textFormat Str.toI32 +i64 : Decode (PgI64 _) I64 +i64 = text_format(Str.to_i64) -i64 : Decode (PgI64 *) I64 -i64 = textFormat Str.toI64 +f32 : Decode (PgF32 _) F32 +f32 = text_format(Str.to_f32) -f32 : Decode (PgF32 *) F32 -f32 = textFormat Str.toF32 +f64 : Decode (PgF64 _) F64 +f64 = text_format(Str.to_f64) -f64 : Decode (PgF64 *) F64 -f64 = textFormat Str.toF64 +dec : Decode (PgDec _) Dec +dec = text_format(Str.to_dec) -dec : Decode (PgDec *) Dec -dec = textFormat Str.toDec - -str : Decode (PgText *) Str -str = textFormat Ok +str : Decode (PgText _) Str +str = text_format(Ok) # TODO: Make a proper uuid type -uuid : Decode (PgUuid *) Str -uuid = textFormat Ok +uuid : Decode (PgUuid _) Str +uuid = text_format(Ok) bool : Decode (PgBool *) Bool bool = - bytes <- @Decode - - when bytes is - ['t'] -> - Ok Bool.true + @Decode( + |bytes| + when bytes is + ['t'] -> + Ok(Bool.true) - ['f'] -> - Ok Bool.false + ['f'] -> + Ok(Bool.false) - _ -> - Err (InvalidBool bytes) + _ -> + Err(InvalidBool(bytes)), + ) Raw : { bytes : List U8, - typeName : Str, + type_name : Str, } unsupported : Str -> Decode * Raw -unsupported = \typeName -> - bytes <- @Decode - - Ok { - bytes, - typeName, - } +unsupported = |type_name| + @Decode( + |bytes| + Ok( + { + bytes, + type_name, + }, + ), + ) array : Decode pg a -> Decode (PgArray pg *) (List a) -array = \@Decode decodeItem -> - arrayBytes <- @Decode - - arrayBytes - |> parseArray - |> List.mapTry \item -> - when item is - Null -> - decodeItem [] - - NotNull val -> - decodeItem val +array = |@Decode(decode_item)| + @Decode( + |array_bytes| + array_bytes + |> parse_array + |> List.map_try( + |item| + when item is + Null -> + decode_item([]) + + NotNull(val) -> + decode_item(val), + ), + ) row : (List (List U8) -> Result a DecodeErr) -> Decode pg a -row = \decodeRow -> - rowBytes <- @Decode - - rowBytes - |> parseRow - |> List.map \field -> - when field is - Null -> - [] - - NotNull val -> - val - |> decodeRow - -parseRow = \bytes -> +row = |decode_row| + @Decode( + |row_bytes| + row_bytes + |> parse_row + |> List.map( + |field| + when field is + Null -> + [] + + NotNull(val) -> + val, + ) + |> decode_row, + ) + +parse_row = |bytes| bytes - |> List.dropFirst 1 - |> List.walk + |> List.drop_first(1) + |> List.walk( { - items: List.withCapacity 6, - curr: List.withCapacity 32, + items: List.with_capacity(6), + curr: List.with_capacity(32), quote: Pending, escaped: Bool.false, - } - rowChar + }, + row_char, + ) |> .items -rowChar = \state, byte -> +row_char = |state, byte| # This parser assumes postgres won't respond with malformed syntax when byte is _ if state.escaped -> { state & escaped: Bool.false, - curr: state.curr |> List.append byte, + curr: state.curr |> List.append(byte), } '"' -> @@ -187,7 +199,7 @@ rowChar = \state, byte -> { state & quote: Open, # reopening means the string contains a literal quote - curr: state.curr |> List.append byte, + curr: state.curr |> List.append(byte), } '\\' -> @@ -195,74 +207,77 @@ rowChar = \state, byte -> ',' | ')' if state.quote != Open -> item = - if List.isEmpty state.curr && state.quote == Pending then + if List.is_empty(state.curr) and state.quote == Pending then Null else - NotNull state.curr + NotNull(state.curr) { state & - items: state.items |> List.append item, - curr: List.withCapacity 32, + items: state.items |> List.append(item), + curr: List.with_capacity(32), quote: Pending, } _ -> - { state & curr: state.curr |> List.append byte } + { state & curr: state.curr |> List.append(byte) } -prs = \input -> +prs = |input| input - |> Str.toUtf8 - |> parseRow - |> List.map \item -> - when item is - Null -> Null - NotNull present -> - present - |> Str.fromUtf8 - |> Result.withDefault "" - |> NotNull - -expect prs "(42)" == [NotNull "42"] -expect prs "(42,hi)" == [NotNull "42", NotNull "hi"] -expect prs "(42,\" hi\")" == [NotNull "42", NotNull " hi"] -expect prs "(\"hello world\",hi)" == [NotNull "hello world", NotNull "hi"] -expect prs "(\"hello \\\"Agus\\\"\",21)" == [NotNull "hello \"Agus\"", NotNull "21"] -expect prs "(\"hello \"\"Agus\"\"\",21)" == [NotNull "hello \"Agus\"", NotNull "21"] -expect prs "(\"a\"\"\")" == [NotNull "a\""] -expect prs "(\"a\\\\b\")" == [NotNull "a\\b"] -expect prs "(\"a\nb\")" == [NotNull "a\nb"] -expect prs "(\"hi, world!\",\"hello, agus\")" == [NotNull "hi, world!", NotNull "hello, agus"] -expect prs "(spaces,no quotes)" == [NotNull "spaces", NotNull "no quotes"] -expect prs "(,prev is null)" == [Null, NotNull "prev is null"] -expect prs "(next is null,)" == [NotNull "next is null", Null] -expect prs "(next is null,,prev is null)" == [NotNull "next is null", Null, NotNull "prev is null"] -expect prs "()" == [Null] -expect prs "(,)" == [Null, Null] -expect prs "(,,)" == [Null, Null, Null] -expect prs "(\"\")" == [NotNull ""] -expect prs "(\"\",)" == [NotNull "", Null] - -parseArray = \bytes -> + |> Str.to_utf8 + |> parse_row + |> List.map( + |item| + when item is + Null -> Null + NotNull(present) -> + present + |> Str.from_utf8 + |> Result.with_default("") + |> NotNull, + ) + +expect prs("(42)") == [NotNull("42")] +expect prs("(42,hi)") == [NotNull("42"), NotNull("hi")] +expect prs("(42,\" hi\")") == [NotNull("42"), NotNull(" hi")] +expect prs("(\"hello world\",hi)") == [NotNull("hello world"), NotNull("hi")] +expect prs("(\"hello \\\"Agus\\\"\",21)") == [NotNull("hello \"Agus\""), NotNull("21")] +expect prs("(\"hello \"\"Agus\"\"\",21)") == [NotNull("hello \"Agus\""), NotNull("21")] +expect prs("(\"a\"\"\")") == [NotNull("a\"")] +expect prs("(\"a\\\\b\")") == [NotNull("a\\b")] +expect prs("(\"a\nb\")") == [NotNull("a\nb")] +expect prs("(\"hi, world!\",\"hello, agus\")") == [NotNull("hi, world!"), NotNull("hello, agus")] +expect prs("(spaces,no quotes)") == [NotNull("spaces"), NotNull("no quotes")] +expect prs("(,prev is null)") == [Null, NotNull("prev is null")] +expect prs("(next is null,)") == [NotNull("next is null"), Null] +expect prs("(next is null,,prev is null)") == [NotNull("next is null"), Null, NotNull("prev is null")] +expect prs("()") == [Null] +expect prs("(,)") == [Null, Null] +expect prs("(,,)") == [Null, Null, Null] +expect prs("(\"\")") == [NotNull("")] +expect prs("(\"\",)") == [NotNull(""), Null] + +parse_array = |bytes| bytes - |> List.dropFirst 1 - |> List.walk + |> List.drop_first(1) + |> List.walk( { - items: List.withCapacity 16, - curr: List.withCapacity 32, + items: List.with_capacity(16), + curr: List.with_capacity(32), quote: Pending, escaped: Bool.false, - } - arrayChar + }, + array_char, + ) |> .items -arrayChar = \state, byte -> +array_char = |state, byte| # This parser assumes postgres won't respond with malformed syntax ## TODO: Handle null when byte is _ if state.escaped -> { state & escaped: Bool.false, - curr: state.curr |> List.append byte, + curr: state.curr |> List.append(byte), } '"' -> @@ -277,7 +292,7 @@ arrayChar = \state, byte -> { state & quote: Open, # reopening means the string contains a literal quote - curr: state.curr |> List.append byte, + curr: state.curr |> List.append(byte), } '\\' -> @@ -285,58 +300,63 @@ arrayChar = \state, byte -> ',' | '}' if state.quote != Open -> items = - if List.isEmpty state.curr && state.quote == Pending then + if List.is_empty(state.curr) and state.quote == Pending then state.items else - state.items |> List.append (NotNull state.curr) + state.items |> List.append(NotNull(state.curr)) { state & items, - curr: List.withCapacity 32, + curr: List.with_capacity(32), quote: Pending, } _ -> - { state & curr: state.curr |> List.append byte } + { state & curr: state.curr |> List.append(byte) } -pas = \input -> +pas = |input| input - |> Str.toUtf8 - |> parseArray - |> List.map \item -> - when item is - Null -> Null - NotNull present -> - present - |> Str.fromUtf8 - |> Result.withDefault "" - |> NotNull - -expect pas "{}" == [] -expect pas "{42}" == [NotNull "42"] + |> Str.to_utf8 + |> parse_array + |> List.map( + |item| + when item is + Null -> Null + NotNull(present) -> + present + |> Str.from_utf8 + |> Result.with_default("") + |> NotNull, + ) + +expect pas("{}") == [] +expect pas("{42}") == [NotNull("42")] expect result = - pas "{\"(hi,\\\"lala\\\"\\\"\\\")\"}" - |> List.map \item -> - when item is - NotNull p -> - NotNull (prs p) - - Null -> - Null - - result == [NotNull [NotNull "hi", NotNull "lala\""]] - -textFormat : (Str -> Result a DecodeErr) -> Decode pg a -textFormat = \fn -> - bytes <- @Decode - - when Str.fromUtf8 bytes is - Ok text -> - fn text - - Err (BadUtf8 _ _) -> - Err InvalidUtf8 + pas("{\"(hi,\\\"lala\\\"\\\"\\\")\"}") + |> List.map( + |item| + when item is + NotNull(p) -> + NotNull(prs(p)) + + Null -> + Null, + ) + + result == [NotNull([NotNull("hi"), NotNull("lala\"")])] + +text_format : (Str -> Result a DecodeErr) -> Decode pg a +text_format = |fn| + @Decode( + |bytes| + when Str.from_utf8(bytes) is + Ok(text) -> + fn(text) + + Err(BadUtf8(_)) -> + Err(InvalidUtf8), + ) # Types diff --git a/src/State.roc b/src/State.roc index 9095439..56cf582 100644 --- a/src/State.roc +++ b/src/State.roc @@ -7,69 +7,97 @@ module [ put, ok, err, - fromResult, + from_result, State, ] State state ok err := state -> Result (ok, state) err attempt : State state ok err, state -> Result (ok, state) err -attempt = \@State r, init -> - r init +attempt = |@State(r), init| + r(init) perform : State state ok [], state -> (ok, state) -perform = \state, init -> - (Ok result) = attempt state init +perform = |state, init| + Ok(result) = attempt(state, init) result bind : State state ok err, (ok -> State state ok2 err) -> State state ok2 err -bind = \first, next -> - s1 <- @State - (o, s2) <- attempt first s1 |> Result.try - attempt (next o) s2 +bind = |first, next| + @State( + |s1| + attempt(first, s1) + |> Result.try( + |(o, s2)| + attempt(next(o), s2), + ), + ) map : State state ok err, (ok -> ok2) -> State state ok2 err -map = \first, next -> - s1 <- @State - (o, s2) <- attempt first s1 |> Result.map - (next o, s2) +map = |first, next| + @State( + |s1| + attempt(first, s1) + |> Result.map_ok( + |(o, s2)| + (next(o), s2), + ), + ) get : State state state err -get = @State \state -> Ok (state, state) +get = @State(|state| Ok((state, state))) put : state -> State state {} err -put = \state -> @State \_ -> Ok ({}, state) +put = |state| @State(|_| Ok(({}, state))) ok : ok -> State * ok * -ok = \o -> @State \state -> Ok (o, state) +ok = |o| @State(|state| Ok((o, state))) err : err -> State * * err -err = \e -> @State \_ -> Err e +err = |e| @State(|_| Err(e)) -fromResult : Result ok err -> State * ok err -fromResult = \result -> - state <- @State - Result.map result \o -> (o, state) +from_result : Result ok err -> State * ok err +from_result = |result| + @State( + |state| + Result.map_ok(result, |o| (o, state)), + ) expect next = - curr <- get |> bind - _ <- put (curr + 1) |> bind - - ok curr + get + |> bind( + |curr| + put((curr + 1)) + |> bind( + |_| + ok(curr), + ), + ) seq = - one <- next |> bind - two <- next |> bind - three <- next |> bind - - ok (one, two, three) - - perform seq 1 == ((1, 2, 3), 3) + next + |> bind( + |one| + next + |> bind( + |two| + next + |> bind( + |three| + ok((one, two, three)), + ), + ), + ) + + perform(seq, 1) == ((1, 2, 3), 3) expect seq = - one <- get |> bind - err one + get + |> bind( + |one| + err(one), + ) - attempt seq 1 == Err 1 + attempt(seq, 1) == Err(1)