From bb938f1e9948e99e9e332c738f409166ade0878b Mon Sep 17 00:00:00 2001 From: Riccardo Binetti Date: Fri, 8 May 2026 10:47:21 +0200 Subject: [PATCH 1/3] chore: bump CI and deps --- .github/workflows/main.yml | 10 +++++----- mix.lock | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d516ec3..30ddbde 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,12 +13,12 @@ jobs: strategy: fail-fast: true matrix: - otp: ["28.1.1"] - elixir: ["1.19.2-otp-28"] + otp: ["28.5"] + elixir: ["1.19.4-otp-28"] zig: ["0.14.1"] steps: - name: Clone the repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install OTP and Elixir uses: erlef/setup-beam@v1 @@ -94,8 +94,8 @@ jobs: otp: "27.3.4.5" elixir: "1.19.2-otp-27" - os: ubuntu-24.04 - otp: "28.1.1" - elixir: "1.19.2-otp-28" + otp: "28.5" + elixir: "1.19.4-otp-28" - os: macos-14 # These actually are not relevant since brew just pulls the latest version otp: "latest" diff --git a/mix.lock b/mix.lock index d638181..1ee76ff 100644 --- a/mix.lock +++ b/mix.lock @@ -1,20 +1,20 @@ %{ "build_dot_zig": {:hex, :build_dot_zig, "0.6.2", "90730b02a47de7c2e128e5a899e0501234c6dfcc2e2dbcc05ecd5503f7910347", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f1d18d8ae9ef54b649f2f266b7b6137ef7d31ef5c69c3c8aac8a1bc4d783c02b"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, - "credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"}, + "castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"}, + "credo": {:hex, :credo, "1.7.18", "5c5596bf7aedf9c8c227f13272ac499fe8eae6237bd326f2f07dfc173786f042", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a189d164685fd945809e862fe76a7420c4398fa288d76257662aecb909d6b3e5"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, - "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, - "styler": {:hex, :styler, "1.10.1", "9229050c978bfaaab1d94e8673843576d0127d48fe64824a30babde3d6342475", [:mix], [], "hexpm", "d86cbcc70e8ab424393af313d1d885931ba9dc7c383d7dd30f4ab255a8d39f73"}, + "stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"}, + "styler": {:hex, :styler, "1.11.0", "35010d970689a23c2bcc8e97bd8bf7d20e3561d60c49be84654df5c37d051a9c", [:mix], [], "hexpm", "70f36165d0cf238a32b7a456fdef6a9c72e77e657d7ac4a0ace33aeba3f2b8c0"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, } From 39a143036cf747857ee5a965bb7d6897c919e89c Mon Sep 17 00:00:00 2001 From: Riccardo Binetti Date: Mon, 18 May 2026 23:18:31 +0200 Subject: [PATCH 2/3] fix: don't execute TigerBeetle's build.zig The build.zig of TigerBeetle 0.17.0 introduces a(nother) dependency on it being executed in a git repository. Unfortunately, this doesn't have an easy workaround like the previous `git_commit` option. To keep TigerBeetlex working and still pull TigerBeetle using the Zig package manager, we just use the TigerBeetle source files, without calling b.dependency, preventing TigerBeetle's build.zig to be executed. --- build.zig | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/build.zig b/build.zig index 3f4c442..dcd1349 100644 --- a/build.zig +++ b/build.zig @@ -36,6 +36,21 @@ fn resolve_target(b: *std.Build, target_requested: ?[]const u8) !std.Build.Resol return b.resolveTargetQuery(query); } +fn dependency_root_path(b: *std.Build, name: []const u8) std.Build.LazyPath { + const deps = @import("root").dependencies; + const pkg_hash = for (b.available_deps) |dep| { + if (std.mem.eql(u8, dep[0], name)) break dep[1]; + } else std.debug.panic("no dependency named '{s}' in build.zig.zon", .{name}); + + inline for (@typeInfo(deps.packages).@"struct".decls) |decl| { + if (std.mem.eql(u8, decl.name, pkg_hash)) { + return .{ .cwd_relative = @field(deps.packages, decl.name).build_root }; + } + } + + unreachable; +} + // Although this function looks imperative, note that its job is to // declaratively construct a build graph that will be executed by an external // runner. @@ -64,18 +79,13 @@ pub fn build(b: *std.Build) !void { break :blk b.run(&argv); }; - // In its build.zig, TigerBeetle accepts a git commit hash that gets passed around to different modules (CI, VSR etc). - // If no one is explicitly passed, it falls back to reading it by shelling out to git. - // This is a problem because it means that it's a compile-time requirement to build inside a git repo, which could be - // false if we're using TigerBeetlex, e.g., in an .exs script. - // To avoid this, we just pass a fake git commit hash, since the git commit hash doesn't change the client behavior - // in any way. - const fake_git_commit_hash = "bee71e0000000000000000000000000000bee71e"; // Beetle-hash! - const tigerbeetle_dep = b.dependency("tigerbeetle", .{ .@"git-commit" = @as([]const u8, fake_git_commit_hash) }); - - const stdx_mod = b.createModule(.{ .root_source_file = tigerbeetle_dep.path("src/stdx/stdx.zig") }); + // We only need TigerBeetle's source files. Running its build.zig would shell out to git while + // configuring unrelated release/test steps, which fails when TigerBeetlex is built from a + // non-git directory such as a Hex dependency checkout. + const tigerbeetle_root = dependency_root_path(b, "tigerbeetle"); + const stdx_mod = b.createModule(.{ .root_source_file = tigerbeetle_root.path(b, "src/stdx/stdx.zig") }); const vsr_mod = b.createModule(.{ - .root_source_file = tigerbeetle_dep.path("src/vsr.zig"), + .root_source_file = tigerbeetle_root.path(b, "src/vsr.zig"), }); vsr_mod.addImport("stdx", stdx_mod); From 4cfc84e97cac78ed3e675ab5744fb5bc2e24709d Mon Sep 17 00:00:00 2001 From: Riccardo Binetti Date: Fri, 8 May 2026 10:32:51 +0200 Subject: [PATCH 3/3] feat!: prepare release 0.17.0 Update TigerBeetle client to 0.17.0, which introduces some breaking changes in the API --- CHANGELOG.md | 13 ++ README.md | 2 +- build.zig.zon | 4 +- guides/change_data_capture.livemd | 6 +- guides/walkthrough.livemd | 118 +++++++----- .../bindings/create_account_result.ex | 97 ++++------ .../bindings/create_account_status.ex | 71 +++++++ .../bindings/create_accounts_result.ex | 51 ----- .../bindings/create_transfer_result.ex | 179 ++++-------------- .../bindings/create_transfer_status.ex | 153 +++++++++++++++ .../bindings/create_transfers_result.ex | 51 ----- lib/tigerbeetlex/bindings/operation.ex | 10 +- lib/tigerbeetlex/bindings/response.ex | 28 +-- lib/tigerbeetlex/client.ex | 26 +-- lib/tigerbeetlex/connection.ex | 34 ++-- mix.exs | 8 +- test/concurrency_test.exs | 8 +- test/integration_test.exs | 104 +++++----- test/tigerbeetlex/client_test.exs | 8 +- test/tigerbeetlex/response_test.exs | 20 +- tools/elixir_bindings.zig | 22 ++- 21 files changed, 537 insertions(+), 476 deletions(-) create mode 100644 lib/tigerbeetlex/bindings/create_account_status.ex delete mode 100644 lib/tigerbeetlex/bindings/create_accounts_result.ex create mode 100644 lib/tigerbeetlex/bindings/create_transfer_status.ex delete mode 100644 lib/tigerbeetlex/bindings/create_transfers_result.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index c1826fd..5013b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.17.0] - 2026-05-08 + +### Changed + +- Update TigerBeetle client to 0.17.0 + +### Breaking Changes + +- `create_accounts/2` and `create_transfers/2` now return one result per submitted event, using + `CreateAccountResult` and `CreateTransferResult` with `status` and `timestamp` fields. +- Remove the old sparse `CreateAccountsResult` and `CreateTransfersResult` structs in favor of the + new per-event result API and `CreateAccountStatus` / `CreateTransferStatus` enums. + ## [0.16.78] - 2026-03-25 ### Changed diff --git a/README.md b/README.md index 4c58736..b9de7be 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The package can be installed by adding `tigerbeetlex` to your list of dependenci ```elixir def deps do [ - {:tigerbeetlex, "~> 0.16.78"} + {:tigerbeetlex, "~> 0.17.0"} ] end ``` diff --git a/build.zig.zon b/build.zig.zon index 10f0262..e0576d8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -24,8 +24,8 @@ // internet connectivity. .dependencies = .{ .tigerbeetle = .{ - .url = "https://github.com/tigerbeetle/tigerbeetle/archive/refs/tags/0.16.78.tar.gz", - .hash = "N-V-__8AAA44hQBJEzstrDbqwqyEfsRaqLUt2Dn1iCPUdAe0", + .url = "https://github.com/tigerbeetle/tigerbeetle/archive/refs/tags/0.17.0.tar.gz", + .hash = "N-V-__8AAFAChwA75k5_3tJ2WE-plkwNlyHjSj2KAtEGpwkt", }, }, .paths = .{ diff --git a/guides/change_data_capture.livemd b/guides/change_data_capture.livemd index ca96cce..623a62d 100644 --- a/guides/change_data_capture.livemd +++ b/guides/change_data_capture.livemd @@ -2,7 +2,7 @@ ```elixir Mix.install([ - {:tigerbeetlex, "~> 0.16.78"}, + {:tigerbeetlex, "~> 0.17.0"}, {:broadway, "~> 1.2"}, {:broadway_rabbitmq, "~> 0.7"}, {:jason, "~> 1.2"} @@ -35,7 +35,7 @@ In your `mix.exs` add TigerBeetlex, Broadway and Jason as dependencies ```elixir [ - {:tigerbeetlex, "~> 0.16.78"}, + {:tigerbeetlex, "~> 0.17.0"}, {:broadway, "~> 1.2"}, {:broadway_rabbitmq, "~> 0.7"}, {:jason, "~> 1.2"} @@ -171,7 +171,7 @@ account_2 = code: 3 } -{:ok, _account_errors} = Connection.create_accounts(:tb, [account_1, account_2]) +{:ok, _account_results} = Connection.create_accounts(:tb, [account_1, account_2]) # Error handling omitted ``` diff --git a/guides/walkthrough.livemd b/guides/walkthrough.livemd index 4c0b70d..0f4aa77 100644 --- a/guides/walkthrough.livemd +++ b/guides/walkthrough.livemd @@ -2,7 +2,7 @@ ```elixir Mix.install([ - {:tigerbeetlex, "~> 0.16.78"} + {:tigerbeetlex, "~> 0.17.0"} ]) ``` @@ -17,7 +17,7 @@ The walkthrough can also be executed in Livebook. In your `mix.exs` add TigerBeetlex as a dependency ```elixir -{:tigerbeetlex, "~> 0.16.78"} +{:tigerbeetlex, "~> 0.17.0"} ``` ## Creating a Client @@ -25,7 +25,7 @@ In your `mix.exs` add TigerBeetlex as a dependency A client is created with a cluster ID and replica addresses for all replicas in the cluster. The cluster ID and replica addresses are both chosen by the system that starts the TigerBeetle cluster. -Clients are thread-safe and a single instance should be shared between multiple concurrent tasks. In the case of `TigerBeetlex.Connection`, the client is stored in the state of the underlying processes, so it's already shared across callers. +Clients are thread-safe and a single instance should be shared between multiple concurrent tasks. This allows events to be automatically batched. In the case of `TigerBeetlex.Connection`, the client is stored in the state of the underlying processes, so it's already shared across callers. Multiple clients are useful when connecting to more than one TigerBeetle cluster. @@ -63,7 +63,7 @@ account = code: 718 } -{:ok, _account_errors} = Connection.create_accounts(:tb, [account]) +{:ok, _account_results} = Connection.create_accounts(:tb, [account]) # Error handling omitted. ``` @@ -73,13 +73,13 @@ See details for the recommended ID scheme in [time-based identifiers](https://do ### Account Flags -The account `flags` value is a struct with binary fields. See details for these flags in the [Accounts reference](https://docs.tigerbeetle.com/reference/account#flags). +The account `flags` value is a struct with boolean fields. See details for these flags in the [Accounts reference](https://docs.tigerbeetle.com/reference/account#flags). -To toggle behavior for an account, set the relative field to `true`: +To toggle behavior for an account, set the relevant field to `true`: * `AccountFlags.linked` * `AccountFlags.debits_must_not_exceed_credits` -* `AccountFlags.credits_must_not_exceed_credits` +* `AccountFlags.credits_must_not_exceed_debits` * `AccountFlags.history` For example, to link two accounts where the first account additionally has the `debits_must_not_exceed_credits` constraint: @@ -103,18 +103,24 @@ account1 = flags: %AccountFlags{history: true} } -{:ok, _account_errors} = Connection.create_accounts(:tb, [account0, account1]) +{:ok, _account_results} = Connection.create_accounts(:tb, [account0, account1]) # Error handling omitted. ``` ### Response and Errors -The response is an empty list if all accounts were created successfully. If the response is non-empty, each struct in the response list contains error information for an account that failed. The error struct contains an error result as atom and the index of the account in the request batch. +The response contains one `%TigerBeetlex.CreateAccountResult{}` for each account in the request batch, in the same order as the submitted accounts: + +* Successfully created accounts with status `:created` return the timestamp assigned to the `Account`. +* Already existing accounts with status `:exists` return the timestamp of the original existing `Account`. +* Failed accounts return the status along with the timestamp when validation occurred. + +Treat `:created` and `:exists` as successful outcomes according to your application's idempotency policy. See all error conditions in the [create_accounts reference](https://docs.tigerbeetle.com/reference/requests/create_accounts/). ```elixir -alias TigerBeetlex.CreateAccountsResult +alias TigerBeetlex.CreateAccountResult account0 = %Account{ @@ -137,18 +143,21 @@ account2 = code: 1 } -{:ok, account_errors} = Connection.create_accounts(:tb, [account0, account1, account2]) +{:ok, account_results} = Connection.create_accounts(:tb, [account0, account1, account2]) -Enum.each(account_errors, fn - %CreateAccountsResult{result: :exists} = error -> - IO.puts("Batch account at index #{error.index} already exists") +account_results +|> Enum.with_index() +|> Enum.each(fn + {%CreateAccountResult{status: status, timestamp: timestamp}, index} + when status in [:created, :exists] -> + IO.puts("Batch account at index #{index} was accepted with status #{status} at #{timestamp}") - error -> - IO.puts("Batch account at index #{error.index} failed to create: #{error.result}") + {%CreateAccountResult{status: status}, index} -> + IO.puts("Batch account at index #{index} failed to create: #{status}") end) ``` -To handle errors you can compare the result atom contained in the struct with the ones present in the `create_accounts` reference above. +To handle errors you can compare the `status` atom contained in the struct with the ones present in the `create_accounts` reference above. ## Account Lookup @@ -181,7 +190,7 @@ transfers = [ } ] -{:ok, _transfer_errors} = Connection.create_transfers(:tb, transfers) +{:ok, _transfer_results} = Connection.create_transfers(:tb, transfers) # Error handling omitted. ``` @@ -191,12 +200,18 @@ See details for the recommended ID scheme in [time-based identifiers](https://do ### Response and Errors -The response is an empty list if all transfers were created successfully. If the response is non-empty, each struct in the response list contains error information for a transfer that failed. The error struct contains an error result as atom and the index of the transfer in the request batch. +The response contains one `%TigerBeetlex.CreateTransferResult{}` for each transfer in the request batch, in the same order as the submitted transfers: + +* Successfully created transfers with status `:created` return the timestamp assigned to the `Transfer`. +* Already existing transfers with status `:exists` return the timestamp of the original existing `Transfer`. +* Failed transfers return the status along with the timestamp when validation occurred. + +Treat `:created` and `:exists` as successful outcomes according to your application's idempotency policy. See all error conditions in the [create_transfers reference](https://docs.tigerbeetle.com/reference/requests/create_transfers/). ```elixir -alias TigerBeetlex.CreateTransfersResult +alias TigerBeetlex.CreateTransferResult transfers = [ %Transfer{ @@ -225,18 +240,21 @@ transfers = [ } ] -{:ok, transfer_errors} = Connection.create_transfers(:tb, transfers) +{:ok, transfer_results} = Connection.create_transfers(:tb, transfers) -Enum.each(transfer_errors, fn - %CreateTransfersResult{result: :exists} = error -> - IO.puts("Batch transfer at index #{error.index} already exists") +transfer_results +|> Enum.with_index() +|> Enum.each(fn + {%CreateTransferResult{status: status, timestamp: timestamp}, index} + when status in [:created, :exists] -> + IO.puts("Batch transfer at index #{index} was accepted with status #{status} at #{timestamp}") - error -> - IO.puts("Batch transfer at index #{error.index} failed to create: #{error.result}") + {%CreateTransferResult{status: status}, index} -> + IO.puts("Batch transfer at index #{index} failed to create: #{status}") end) ``` -To handle errors you can compare the result atom contained in the struct with the ones present in the `create_transfers` reference above. +To handle errors you can compare the `status` atom contained in the struct with the ones present in the `create_transfers` reference above. ## Batching @@ -247,7 +265,7 @@ TigerBeetle performance is maximized when you batch API requests. A client insta batch = [] Enum.each(batch, fn transfer -> - {:ok, _transfer_errors} = Connection.create_transfers(:tb, [transfer]) + {:ok, _transfer_results} = Connection.create_transfers(:tb, [transfer]) # Error handling omitted end) ``` @@ -263,7 +281,7 @@ batch_size = 8189 batch |> Enum.chunk_every(batch_size) |> Enum.each(fn transfers -> - {:ok, _transfer_errors} = Connection.create_transfers(:tb, transfers) + {:ok, _transfer_results} = Connection.create_transfers(:tb, transfers) # Error handling omitted end) ``` @@ -274,14 +292,14 @@ If you are making requests to TigerBeetle from workers pulling jobs from a queue ## Transfer Flags -The transfer `flags` value is a struct with boolean fields. See details for these flags in the [Accounts reference](https://docs.tigerbeetle.com/reference/transfer#flags). +The transfer `flags` value is a struct with boolean fields. See details for these flags in the [Transfers reference](https://docs.tigerbeetle.com/reference/transfer#flags). -To toggle behavior for an account, set the relative field to `true`: +To toggle behavior for a transfer, set the relevant field to `true`: -* `AccountFlags.linked` -* `AccountFlags.pending` -* `AccountFlags.post_pending_transfer` -* `AccountFlags.void_pending_transfer` +* `TransferFlags.linked` +* `TransferFlags.pending` +* `TransferFlags.post_pending_transfer` +* `TransferFlags.void_pending_transfer` For example, to link `transfer0` and `transfer1`: @@ -311,7 +329,7 @@ transfer1 = code: 720 } -{:ok, _transfer_errors} = Connection.create_transfers(:tb, [transfer0, transfer1]) +{:ok, _transfer_results} = Connection.create_transfers(:tb, [transfer0, transfer1]) # Error handling omitted ``` @@ -336,7 +354,7 @@ transfer0 = flags: %TransferFlags{pending: true} } -{:ok, _transfer_errors} = Connection.create_transfers(:tb, [transfer0]) +{:ok, _transfer_results} = Connection.create_transfers(:tb, [transfer0]) # Error handling omitted transfer1 = @@ -353,7 +371,7 @@ transfer1 = flags: %TransferFlags{post_pending_transfer: true} } -{:ok, _transfer_errors} = Connection.create_transfers(:tb, [transfer1]) +{:ok, _transfer_results} = Connection.create_transfers(:tb, [transfer1]) # Error handling omitted ``` @@ -374,7 +392,7 @@ transfer0 = flags: %TransferFlags{pending: true} } -{:ok, _transfer_errors} = Connection.create_transfers(:tb, [transfer0]) +{:ok, _transfer_results} = Connection.create_transfers(:tb, [transfer0]) # Error handling omitted transfer1 = @@ -383,14 +401,14 @@ transfer1 = id: ID.from_int(9), debit_account_id: ID.from_int(102), credit_account_id: ID.from_int(103), - amount: 10, + amount: 0, pending_id: ID.from_int(8), ledger: 1, code: 720, flags: %TransferFlags{void_pending_transfer: true} } -{:ok, _transfer_errors} = Connection.create_transfers(:tb, [transfer1]) +{:ok, _transfer_results} = Connection.create_transfers(:tb, [transfer1]) # Error handling omitted ``` @@ -431,7 +449,7 @@ filter = timestamp_min: 0, # No filter by Timestamp. timestamp_max: 0, - # Limit to ten transfers at most. + # Limit to ten accounts at most. limit: 10, flags: %AccountFilterFlags{ # Include transfer from the debit side. @@ -586,7 +604,7 @@ batch = [ %Transfer{id: ID.from_int(4)} ] -{:ok, _transfer_errors} = Connection.create_transfers(:tb, batch) +{:ok, _transfer_results} = Connection.create_transfers(:tb, batch) # Error handling omitted. ``` @@ -635,12 +653,12 @@ accounts = # We reverse everything to obtain the correct order |> Enum.reverse() -{:ok, _account_errors} = Connection.create_accounts(:tb, accounts) +{:ok, _account_results} = Connection.create_accounts(:tb, accounts) # Error handling omitted. # Then, load and import all transfers with their timestamps from the historical source. {reversed_transfers, _historical_timestamp} = - Enum.reduce(historical_transfers, {[], historical_timestamp}, fn _historical_account, + Enum.reduce(historical_transfers, {[], historical_timestamp}, fn _historical_transfer, {acc, last_timestamp} -> # Set a unique and strictly increasing timestamp. timestamp = last_timestamp + 1 @@ -649,8 +667,8 @@ accounts = timestamp: timestamp, # To ensure atomicity, the entire batch (except the last event in the chain) # must be `linked`. - flags: %AccountFlags{imported: true, linked: true} - # Bring over other fields from historical_account + flags: %TransferFlags{imported: true, linked: true} + # Bring over other fields from historical_transfer } {[transfer | acc], timestamp} @@ -660,13 +678,13 @@ transfers = reversed_transfers # We "unset" the linked flag to the "last" event in the chain, which is actually the # first one since we're accumulating by prepending - |> List.update_at(0, fn account -> - %{account | flags: %TransferFlags{imported: true}} + |> List.update_at(0, fn transfer -> + %{transfer | flags: %TransferFlags{imported: true}} end) # We reverse everything to obtain the correct order |> Enum.reverse() -{:ok, _transfer_errors} = Connection.create_transfers(:tb, transfers) +{:ok, _transfer_results} = Connection.create_transfers(:tb, transfers) # Error handling omitted. ``` diff --git a/lib/tigerbeetlex/bindings/create_account_result.ex b/lib/tigerbeetlex/bindings/create_account_result.ex index 0d624fd..3851d86 100644 --- a/lib/tigerbeetlex/bindings/create_account_result.ex +++ b/lib/tigerbeetlex/bindings/create_account_result.ex @@ -4,68 +4,51 @@ ####################################################### defmodule TigerBeetlex.CreateAccountResult do - @moduledoc false + @moduledoc """ + See [CreateAccountResult](https://docs.tigerbeetle.com/reference/requests/create_accounts#result). + """ + + use TypedStruct + + alias TigerBeetlex.CreateAccountStatus + + typedstruct do + field :timestamp, non_neg_integer(), default: 0 + field :status, atom() + end @doc """ - Obtains the atom representation of a result from its integer value. + Creates a `TigerBeetlex.CreateAccountResult` struct from its binary representation. """ - def to_atom(0), do: :ok - def to_atom(1), do: :linked_event_failed - def to_atom(2), do: :linked_event_chain_open - def to_atom(22), do: :imported_event_expected - def to_atom(23), do: :imported_event_not_expected - def to_atom(3), do: :timestamp_must_be_zero - def to_atom(24), do: :imported_event_timestamp_out_of_range - def to_atom(25), do: :imported_event_timestamp_must_not_advance - def to_atom(4), do: :reserved_field - def to_atom(5), do: :reserved_flag - def to_atom(6), do: :id_must_not_be_zero - def to_atom(7), do: :id_must_not_be_int_max - def to_atom(15), do: :exists_with_different_flags - def to_atom(16), do: :exists_with_different_user_data_128 - def to_atom(17), do: :exists_with_different_user_data_64 - def to_atom(18), do: :exists_with_different_user_data_32 - def to_atom(19), do: :exists_with_different_ledger - def to_atom(20), do: :exists_with_different_code - def to_atom(21), do: :exists - def to_atom(8), do: :flags_are_mutually_exclusive - def to_atom(9), do: :debits_pending_must_be_zero - def to_atom(10), do: :debits_posted_must_be_zero - def to_atom(11), do: :credits_pending_must_be_zero - def to_atom(12), do: :credits_posted_must_be_zero - def to_atom(13), do: :ledger_must_not_be_zero - def to_atom(14), do: :code_must_not_be_zero - def to_atom(26), do: :imported_event_timestamp_must_not_regress + @spec from_binary(binary :: <<_::128>>) :: t() + def from_binary(<<_::binary-size(16)>> = bin) do + << + timestamp::unsigned-little-64, + status::unsigned-little-32, + _reserved::unsigned-little-32 + >> = bin + + %__MODULE__{ + timestamp: timestamp, + status: CreateAccountStatus.to_atom(status) + } + end @doc """ - Obtains the integer representation of a result reason from its atom value. + Converts a `TigerBeetlex.CreateAccountResult` struct to its binary representation. """ + @spec to_binary(struct :: t()) :: <<_::128>> + def to_binary(struct) do + %__MODULE__{ + timestamp: timestamp, + status: status + } = struct - def from_atom(:ok), do: 0 - def from_atom(:linked_event_failed), do: 1 - def from_atom(:linked_event_chain_open), do: 2 - def from_atom(:imported_event_expected), do: 22 - def from_atom(:imported_event_not_expected), do: 23 - def from_atom(:timestamp_must_be_zero), do: 3 - def from_atom(:imported_event_timestamp_out_of_range), do: 24 - def from_atom(:imported_event_timestamp_must_not_advance), do: 25 - def from_atom(:reserved_field), do: 4 - def from_atom(:reserved_flag), do: 5 - def from_atom(:id_must_not_be_zero), do: 6 - def from_atom(:id_must_not_be_int_max), do: 7 - def from_atom(:exists_with_different_flags), do: 15 - def from_atom(:exists_with_different_user_data_128), do: 16 - def from_atom(:exists_with_different_user_data_64), do: 17 - def from_atom(:exists_with_different_user_data_32), do: 18 - def from_atom(:exists_with_different_ledger), do: 19 - def from_atom(:exists_with_different_code), do: 20 - def from_atom(:exists), do: 21 - def from_atom(:flags_are_mutually_exclusive), do: 8 - def from_atom(:debits_pending_must_be_zero), do: 9 - def from_atom(:debits_posted_must_be_zero), do: 10 - def from_atom(:credits_pending_must_be_zero), do: 11 - def from_atom(:credits_posted_must_be_zero), do: 12 - def from_atom(:ledger_must_not_be_zero), do: 13 - def from_atom(:code_must_not_be_zero), do: 14 - def from_atom(:imported_event_timestamp_must_not_regress), do: 26 + << + timestamp::unsigned-little-64, + CreateAccountStatus.from_atom(status)::unsigned-little-32, + # reserved + 0::unit(8)-size(4) + >> + end end diff --git a/lib/tigerbeetlex/bindings/create_account_status.ex b/lib/tigerbeetlex/bindings/create_account_status.ex new file mode 100644 index 0000000..600663d --- /dev/null +++ b/lib/tigerbeetlex/bindings/create_account_status.ex @@ -0,0 +1,71 @@ +####################################################### +# This file was auto-generated by elixir_bindings.zig # +# Do not manually modify. # +####################################################### + +defmodule TigerBeetlex.CreateAccountStatus do + @moduledoc false + + @doc """ + Obtains the atom representation of a result from its integer value. + """ + def to_atom(4_294_967_295), do: :created + def to_atom(1), do: :linked_event_failed + def to_atom(2), do: :linked_event_chain_open + def to_atom(22), do: :imported_event_expected + def to_atom(23), do: :imported_event_not_expected + def to_atom(3), do: :timestamp_must_be_zero + def to_atom(24), do: :imported_event_timestamp_out_of_range + def to_atom(25), do: :imported_event_timestamp_must_not_advance + def to_atom(4), do: :reserved_field + def to_atom(5), do: :reserved_flag + def to_atom(6), do: :id_must_not_be_zero + def to_atom(7), do: :id_must_not_be_int_max + def to_atom(15), do: :exists_with_different_flags + def to_atom(16), do: :exists_with_different_user_data_128 + def to_atom(17), do: :exists_with_different_user_data_64 + def to_atom(18), do: :exists_with_different_user_data_32 + def to_atom(19), do: :exists_with_different_ledger + def to_atom(20), do: :exists_with_different_code + def to_atom(21), do: :exists + def to_atom(8), do: :flags_are_mutually_exclusive + def to_atom(9), do: :debits_pending_must_be_zero + def to_atom(10), do: :debits_posted_must_be_zero + def to_atom(11), do: :credits_pending_must_be_zero + def to_atom(12), do: :credits_posted_must_be_zero + def to_atom(13), do: :ledger_must_not_be_zero + def to_atom(14), do: :code_must_not_be_zero + def to_atom(26), do: :imported_event_timestamp_must_not_regress + + @doc """ + Obtains the integer representation of a result reason from its atom value. + """ + + def from_atom(:created), do: 4_294_967_295 + def from_atom(:linked_event_failed), do: 1 + def from_atom(:linked_event_chain_open), do: 2 + def from_atom(:imported_event_expected), do: 22 + def from_atom(:imported_event_not_expected), do: 23 + def from_atom(:timestamp_must_be_zero), do: 3 + def from_atom(:imported_event_timestamp_out_of_range), do: 24 + def from_atom(:imported_event_timestamp_must_not_advance), do: 25 + def from_atom(:reserved_field), do: 4 + def from_atom(:reserved_flag), do: 5 + def from_atom(:id_must_not_be_zero), do: 6 + def from_atom(:id_must_not_be_int_max), do: 7 + def from_atom(:exists_with_different_flags), do: 15 + def from_atom(:exists_with_different_user_data_128), do: 16 + def from_atom(:exists_with_different_user_data_64), do: 17 + def from_atom(:exists_with_different_user_data_32), do: 18 + def from_atom(:exists_with_different_ledger), do: 19 + def from_atom(:exists_with_different_code), do: 20 + def from_atom(:exists), do: 21 + def from_atom(:flags_are_mutually_exclusive), do: 8 + def from_atom(:debits_pending_must_be_zero), do: 9 + def from_atom(:debits_posted_must_be_zero), do: 10 + def from_atom(:credits_pending_must_be_zero), do: 11 + def from_atom(:credits_posted_must_be_zero), do: 12 + def from_atom(:ledger_must_not_be_zero), do: 13 + def from_atom(:code_must_not_be_zero), do: 14 + def from_atom(:imported_event_timestamp_must_not_regress), do: 26 +end diff --git a/lib/tigerbeetlex/bindings/create_accounts_result.ex b/lib/tigerbeetlex/bindings/create_accounts_result.ex deleted file mode 100644 index 86f5141..0000000 --- a/lib/tigerbeetlex/bindings/create_accounts_result.ex +++ /dev/null @@ -1,51 +0,0 @@ -####################################################### -# This file was auto-generated by elixir_bindings.zig # -# Do not manually modify. # -####################################################### - -defmodule TigerBeetlex.CreateAccountsResult do - @moduledoc """ - See [CreateAccountsResult](https://docs.tigerbeetle.com/reference/requests/create_accounts#). - """ - - use TypedStruct - - alias TigerBeetlex.CreateAccountResult - - typedstruct do - field :index, non_neg_integer(), default: 0 - field :result, atom() - end - - @doc """ - Creates a `TigerBeetlex.CreateAccountsResult` struct from its binary representation. - """ - @spec from_binary(binary :: <<_::64>>) :: t() - def from_binary(<<_::binary-size(8)>> = bin) do - << - index::unsigned-little-32, - result::unsigned-little-32 - >> = bin - - %__MODULE__{ - index: index, - result: CreateAccountResult.to_atom(result) - } - end - - @doc """ - Converts a `TigerBeetlex.CreateAccountsResult` struct to its binary representation. - """ - @spec to_binary(struct :: t()) :: <<_::64>> - def to_binary(struct) do - %__MODULE__{ - index: index, - result: result - } = struct - - << - index::unsigned-little-32, - CreateAccountResult.from_atom(result)::unsigned-little-32 - >> - end -end diff --git a/lib/tigerbeetlex/bindings/create_transfer_result.ex b/lib/tigerbeetlex/bindings/create_transfer_result.ex index 5ea8252..aace9bd 100644 --- a/lib/tigerbeetlex/bindings/create_transfer_result.ex +++ b/lib/tigerbeetlex/bindings/create_transfer_result.ex @@ -4,150 +4,51 @@ ####################################################### defmodule TigerBeetlex.CreateTransferResult do - @moduledoc false + @moduledoc """ + See [CreateTransferResult](https://docs.tigerbeetle.com/reference/requests/create_transfers#result). + """ + + use TypedStruct + + alias TigerBeetlex.CreateTransferStatus + + typedstruct do + field :timestamp, non_neg_integer(), default: 0 + field :status, atom() + end @doc """ - Obtains the atom representation of a result from its integer value. + Creates a `TigerBeetlex.CreateTransferResult` struct from its binary representation. """ - def to_atom(0), do: :ok - def to_atom(1), do: :linked_event_failed - def to_atom(2), do: :linked_event_chain_open - def to_atom(56), do: :imported_event_expected - def to_atom(57), do: :imported_event_not_expected - def to_atom(3), do: :timestamp_must_be_zero - def to_atom(58), do: :imported_event_timestamp_out_of_range - def to_atom(59), do: :imported_event_timestamp_must_not_advance - def to_atom(4), do: :reserved_flag - def to_atom(5), do: :id_must_not_be_zero - def to_atom(6), do: :id_must_not_be_int_max - def to_atom(36), do: :exists_with_different_flags - def to_atom(40), do: :exists_with_different_pending_id - def to_atom(44), do: :exists_with_different_timeout - def to_atom(37), do: :exists_with_different_debit_account_id - def to_atom(38), do: :exists_with_different_credit_account_id - def to_atom(39), do: :exists_with_different_amount - def to_atom(41), do: :exists_with_different_user_data_128 - def to_atom(42), do: :exists_with_different_user_data_64 - def to_atom(43), do: :exists_with_different_user_data_32 - def to_atom(67), do: :exists_with_different_ledger - def to_atom(45), do: :exists_with_different_code - def to_atom(46), do: :exists - def to_atom(68), do: :id_already_failed - def to_atom(7), do: :flags_are_mutually_exclusive - def to_atom(8), do: :debit_account_id_must_not_be_zero - def to_atom(9), do: :debit_account_id_must_not_be_int_max - def to_atom(10), do: :credit_account_id_must_not_be_zero - def to_atom(11), do: :credit_account_id_must_not_be_int_max - def to_atom(12), do: :accounts_must_be_different - def to_atom(13), do: :pending_id_must_be_zero - def to_atom(14), do: :pending_id_must_not_be_zero - def to_atom(15), do: :pending_id_must_not_be_int_max - def to_atom(16), do: :pending_id_must_be_different - def to_atom(17), do: :timeout_reserved_for_pending_transfer - def to_atom(64), do: :closing_transfer_must_be_pending - def to_atom(19), do: :ledger_must_not_be_zero - def to_atom(20), do: :code_must_not_be_zero - def to_atom(21), do: :debit_account_not_found - def to_atom(22), do: :credit_account_not_found - def to_atom(23), do: :accounts_must_have_the_same_ledger - def to_atom(24), do: :transfer_must_have_the_same_ledger_as_accounts - def to_atom(25), do: :pending_transfer_not_found - def to_atom(26), do: :pending_transfer_not_pending - def to_atom(27), do: :pending_transfer_has_different_debit_account_id - def to_atom(28), do: :pending_transfer_has_different_credit_account_id - def to_atom(29), do: :pending_transfer_has_different_ledger - def to_atom(30), do: :pending_transfer_has_different_code - def to_atom(31), do: :exceeds_pending_transfer_amount - def to_atom(32), do: :pending_transfer_has_different_amount - def to_atom(33), do: :pending_transfer_already_posted - def to_atom(34), do: :pending_transfer_already_voided - def to_atom(35), do: :pending_transfer_expired - def to_atom(60), do: :imported_event_timestamp_must_not_regress - def to_atom(61), do: :imported_event_timestamp_must_postdate_debit_account - def to_atom(62), do: :imported_event_timestamp_must_postdate_credit_account - def to_atom(63), do: :imported_event_timeout_must_be_zero - def to_atom(65), do: :debit_account_already_closed - def to_atom(66), do: :credit_account_already_closed - def to_atom(47), do: :overflows_debits_pending - def to_atom(48), do: :overflows_credits_pending - def to_atom(49), do: :overflows_debits_posted - def to_atom(50), do: :overflows_credits_posted - def to_atom(51), do: :overflows_debits - def to_atom(52), do: :overflows_credits - def to_atom(53), do: :overflows_timeout - def to_atom(54), do: :exceeds_credits - def to_atom(55), do: :exceeds_debits + @spec from_binary(binary :: <<_::128>>) :: t() + def from_binary(<<_::binary-size(16)>> = bin) do + << + timestamp::unsigned-little-64, + status::unsigned-little-32, + _reserved::unsigned-little-32 + >> = bin + + %__MODULE__{ + timestamp: timestamp, + status: CreateTransferStatus.to_atom(status) + } + end @doc """ - Obtains the integer representation of a result reason from its atom value. + Converts a `TigerBeetlex.CreateTransferResult` struct to its binary representation. """ + @spec to_binary(struct :: t()) :: <<_::128>> + def to_binary(struct) do + %__MODULE__{ + timestamp: timestamp, + status: status + } = struct - def from_atom(:ok), do: 0 - def from_atom(:linked_event_failed), do: 1 - def from_atom(:linked_event_chain_open), do: 2 - def from_atom(:imported_event_expected), do: 56 - def from_atom(:imported_event_not_expected), do: 57 - def from_atom(:timestamp_must_be_zero), do: 3 - def from_atom(:imported_event_timestamp_out_of_range), do: 58 - def from_atom(:imported_event_timestamp_must_not_advance), do: 59 - def from_atom(:reserved_flag), do: 4 - def from_atom(:id_must_not_be_zero), do: 5 - def from_atom(:id_must_not_be_int_max), do: 6 - def from_atom(:exists_with_different_flags), do: 36 - def from_atom(:exists_with_different_pending_id), do: 40 - def from_atom(:exists_with_different_timeout), do: 44 - def from_atom(:exists_with_different_debit_account_id), do: 37 - def from_atom(:exists_with_different_credit_account_id), do: 38 - def from_atom(:exists_with_different_amount), do: 39 - def from_atom(:exists_with_different_user_data_128), do: 41 - def from_atom(:exists_with_different_user_data_64), do: 42 - def from_atom(:exists_with_different_user_data_32), do: 43 - def from_atom(:exists_with_different_ledger), do: 67 - def from_atom(:exists_with_different_code), do: 45 - def from_atom(:exists), do: 46 - def from_atom(:id_already_failed), do: 68 - def from_atom(:flags_are_mutually_exclusive), do: 7 - def from_atom(:debit_account_id_must_not_be_zero), do: 8 - def from_atom(:debit_account_id_must_not_be_int_max), do: 9 - def from_atom(:credit_account_id_must_not_be_zero), do: 10 - def from_atom(:credit_account_id_must_not_be_int_max), do: 11 - def from_atom(:accounts_must_be_different), do: 12 - def from_atom(:pending_id_must_be_zero), do: 13 - def from_atom(:pending_id_must_not_be_zero), do: 14 - def from_atom(:pending_id_must_not_be_int_max), do: 15 - def from_atom(:pending_id_must_be_different), do: 16 - def from_atom(:timeout_reserved_for_pending_transfer), do: 17 - def from_atom(:closing_transfer_must_be_pending), do: 64 - def from_atom(:ledger_must_not_be_zero), do: 19 - def from_atom(:code_must_not_be_zero), do: 20 - def from_atom(:debit_account_not_found), do: 21 - def from_atom(:credit_account_not_found), do: 22 - def from_atom(:accounts_must_have_the_same_ledger), do: 23 - def from_atom(:transfer_must_have_the_same_ledger_as_accounts), do: 24 - def from_atom(:pending_transfer_not_found), do: 25 - def from_atom(:pending_transfer_not_pending), do: 26 - def from_atom(:pending_transfer_has_different_debit_account_id), do: 27 - def from_atom(:pending_transfer_has_different_credit_account_id), do: 28 - def from_atom(:pending_transfer_has_different_ledger), do: 29 - def from_atom(:pending_transfer_has_different_code), do: 30 - def from_atom(:exceeds_pending_transfer_amount), do: 31 - def from_atom(:pending_transfer_has_different_amount), do: 32 - def from_atom(:pending_transfer_already_posted), do: 33 - def from_atom(:pending_transfer_already_voided), do: 34 - def from_atom(:pending_transfer_expired), do: 35 - def from_atom(:imported_event_timestamp_must_not_regress), do: 60 - def from_atom(:imported_event_timestamp_must_postdate_debit_account), do: 61 - def from_atom(:imported_event_timestamp_must_postdate_credit_account), do: 62 - def from_atom(:imported_event_timeout_must_be_zero), do: 63 - def from_atom(:debit_account_already_closed), do: 65 - def from_atom(:credit_account_already_closed), do: 66 - def from_atom(:overflows_debits_pending), do: 47 - def from_atom(:overflows_credits_pending), do: 48 - def from_atom(:overflows_debits_posted), do: 49 - def from_atom(:overflows_credits_posted), do: 50 - def from_atom(:overflows_debits), do: 51 - def from_atom(:overflows_credits), do: 52 - def from_atom(:overflows_timeout), do: 53 - def from_atom(:exceeds_credits), do: 54 - def from_atom(:exceeds_debits), do: 55 + << + timestamp::unsigned-little-64, + CreateTransferStatus.from_atom(status)::unsigned-little-32, + # reserved + 0::unit(8)-size(4) + >> + end end diff --git a/lib/tigerbeetlex/bindings/create_transfer_status.ex b/lib/tigerbeetlex/bindings/create_transfer_status.ex new file mode 100644 index 0000000..ee5244a --- /dev/null +++ b/lib/tigerbeetlex/bindings/create_transfer_status.ex @@ -0,0 +1,153 @@ +####################################################### +# This file was auto-generated by elixir_bindings.zig # +# Do not manually modify. # +####################################################### + +defmodule TigerBeetlex.CreateTransferStatus do + @moduledoc false + + @doc """ + Obtains the atom representation of a result from its integer value. + """ + def to_atom(4_294_967_295), do: :created + def to_atom(1), do: :linked_event_failed + def to_atom(2), do: :linked_event_chain_open + def to_atom(56), do: :imported_event_expected + def to_atom(57), do: :imported_event_not_expected + def to_atom(3), do: :timestamp_must_be_zero + def to_atom(58), do: :imported_event_timestamp_out_of_range + def to_atom(59), do: :imported_event_timestamp_must_not_advance + def to_atom(4), do: :reserved_flag + def to_atom(5), do: :id_must_not_be_zero + def to_atom(6), do: :id_must_not_be_int_max + def to_atom(36), do: :exists_with_different_flags + def to_atom(40), do: :exists_with_different_pending_id + def to_atom(44), do: :exists_with_different_timeout + def to_atom(37), do: :exists_with_different_debit_account_id + def to_atom(38), do: :exists_with_different_credit_account_id + def to_atom(39), do: :exists_with_different_amount + def to_atom(41), do: :exists_with_different_user_data_128 + def to_atom(42), do: :exists_with_different_user_data_64 + def to_atom(43), do: :exists_with_different_user_data_32 + def to_atom(67), do: :exists_with_different_ledger + def to_atom(45), do: :exists_with_different_code + def to_atom(46), do: :exists + def to_atom(68), do: :id_already_failed + def to_atom(7), do: :flags_are_mutually_exclusive + def to_atom(8), do: :debit_account_id_must_not_be_zero + def to_atom(9), do: :debit_account_id_must_not_be_int_max + def to_atom(10), do: :credit_account_id_must_not_be_zero + def to_atom(11), do: :credit_account_id_must_not_be_int_max + def to_atom(12), do: :accounts_must_be_different + def to_atom(13), do: :pending_id_must_be_zero + def to_atom(14), do: :pending_id_must_not_be_zero + def to_atom(15), do: :pending_id_must_not_be_int_max + def to_atom(16), do: :pending_id_must_be_different + def to_atom(17), do: :timeout_reserved_for_pending_transfer + def to_atom(64), do: :closing_transfer_must_be_pending + def to_atom(19), do: :ledger_must_not_be_zero + def to_atom(20), do: :code_must_not_be_zero + def to_atom(21), do: :debit_account_not_found + def to_atom(22), do: :credit_account_not_found + def to_atom(23), do: :accounts_must_have_the_same_ledger + def to_atom(24), do: :transfer_must_have_the_same_ledger_as_accounts + def to_atom(25), do: :pending_transfer_not_found + def to_atom(26), do: :pending_transfer_not_pending + def to_atom(27), do: :pending_transfer_has_different_debit_account_id + def to_atom(28), do: :pending_transfer_has_different_credit_account_id + def to_atom(29), do: :pending_transfer_has_different_ledger + def to_atom(30), do: :pending_transfer_has_different_code + def to_atom(31), do: :exceeds_pending_transfer_amount + def to_atom(32), do: :pending_transfer_has_different_amount + def to_atom(33), do: :pending_transfer_already_posted + def to_atom(34), do: :pending_transfer_already_voided + def to_atom(35), do: :pending_transfer_expired + def to_atom(60), do: :imported_event_timestamp_must_not_regress + def to_atom(61), do: :imported_event_timestamp_must_postdate_debit_account + def to_atom(62), do: :imported_event_timestamp_must_postdate_credit_account + def to_atom(63), do: :imported_event_timeout_must_be_zero + def to_atom(65), do: :debit_account_already_closed + def to_atom(66), do: :credit_account_already_closed + def to_atom(47), do: :overflows_debits_pending + def to_atom(48), do: :overflows_credits_pending + def to_atom(49), do: :overflows_debits_posted + def to_atom(50), do: :overflows_credits_posted + def to_atom(51), do: :overflows_debits + def to_atom(52), do: :overflows_credits + def to_atom(53), do: :overflows_timeout + def to_atom(54), do: :exceeds_credits + def to_atom(55), do: :exceeds_debits + + @doc """ + Obtains the integer representation of a result reason from its atom value. + """ + + def from_atom(:created), do: 4_294_967_295 + def from_atom(:linked_event_failed), do: 1 + def from_atom(:linked_event_chain_open), do: 2 + def from_atom(:imported_event_expected), do: 56 + def from_atom(:imported_event_not_expected), do: 57 + def from_atom(:timestamp_must_be_zero), do: 3 + def from_atom(:imported_event_timestamp_out_of_range), do: 58 + def from_atom(:imported_event_timestamp_must_not_advance), do: 59 + def from_atom(:reserved_flag), do: 4 + def from_atom(:id_must_not_be_zero), do: 5 + def from_atom(:id_must_not_be_int_max), do: 6 + def from_atom(:exists_with_different_flags), do: 36 + def from_atom(:exists_with_different_pending_id), do: 40 + def from_atom(:exists_with_different_timeout), do: 44 + def from_atom(:exists_with_different_debit_account_id), do: 37 + def from_atom(:exists_with_different_credit_account_id), do: 38 + def from_atom(:exists_with_different_amount), do: 39 + def from_atom(:exists_with_different_user_data_128), do: 41 + def from_atom(:exists_with_different_user_data_64), do: 42 + def from_atom(:exists_with_different_user_data_32), do: 43 + def from_atom(:exists_with_different_ledger), do: 67 + def from_atom(:exists_with_different_code), do: 45 + def from_atom(:exists), do: 46 + def from_atom(:id_already_failed), do: 68 + def from_atom(:flags_are_mutually_exclusive), do: 7 + def from_atom(:debit_account_id_must_not_be_zero), do: 8 + def from_atom(:debit_account_id_must_not_be_int_max), do: 9 + def from_atom(:credit_account_id_must_not_be_zero), do: 10 + def from_atom(:credit_account_id_must_not_be_int_max), do: 11 + def from_atom(:accounts_must_be_different), do: 12 + def from_atom(:pending_id_must_be_zero), do: 13 + def from_atom(:pending_id_must_not_be_zero), do: 14 + def from_atom(:pending_id_must_not_be_int_max), do: 15 + def from_atom(:pending_id_must_be_different), do: 16 + def from_atom(:timeout_reserved_for_pending_transfer), do: 17 + def from_atom(:closing_transfer_must_be_pending), do: 64 + def from_atom(:ledger_must_not_be_zero), do: 19 + def from_atom(:code_must_not_be_zero), do: 20 + def from_atom(:debit_account_not_found), do: 21 + def from_atom(:credit_account_not_found), do: 22 + def from_atom(:accounts_must_have_the_same_ledger), do: 23 + def from_atom(:transfer_must_have_the_same_ledger_as_accounts), do: 24 + def from_atom(:pending_transfer_not_found), do: 25 + def from_atom(:pending_transfer_not_pending), do: 26 + def from_atom(:pending_transfer_has_different_debit_account_id), do: 27 + def from_atom(:pending_transfer_has_different_credit_account_id), do: 28 + def from_atom(:pending_transfer_has_different_ledger), do: 29 + def from_atom(:pending_transfer_has_different_code), do: 30 + def from_atom(:exceeds_pending_transfer_amount), do: 31 + def from_atom(:pending_transfer_has_different_amount), do: 32 + def from_atom(:pending_transfer_already_posted), do: 33 + def from_atom(:pending_transfer_already_voided), do: 34 + def from_atom(:pending_transfer_expired), do: 35 + def from_atom(:imported_event_timestamp_must_not_regress), do: 60 + def from_atom(:imported_event_timestamp_must_postdate_debit_account), do: 61 + def from_atom(:imported_event_timestamp_must_postdate_credit_account), do: 62 + def from_atom(:imported_event_timeout_must_be_zero), do: 63 + def from_atom(:debit_account_already_closed), do: 65 + def from_atom(:credit_account_already_closed), do: 66 + def from_atom(:overflows_debits_pending), do: 47 + def from_atom(:overflows_credits_pending), do: 48 + def from_atom(:overflows_debits_posted), do: 49 + def from_atom(:overflows_credits_posted), do: 50 + def from_atom(:overflows_debits), do: 51 + def from_atom(:overflows_credits), do: 52 + def from_atom(:overflows_timeout), do: 53 + def from_atom(:exceeds_credits), do: 54 + def from_atom(:exceeds_debits), do: 55 +end diff --git a/lib/tigerbeetlex/bindings/create_transfers_result.ex b/lib/tigerbeetlex/bindings/create_transfers_result.ex deleted file mode 100644 index 10be246..0000000 --- a/lib/tigerbeetlex/bindings/create_transfers_result.ex +++ /dev/null @@ -1,51 +0,0 @@ -####################################################### -# This file was auto-generated by elixir_bindings.zig # -# Do not manually modify. # -####################################################### - -defmodule TigerBeetlex.CreateTransfersResult do - @moduledoc """ - See [CreateTransfersResult](https://docs.tigerbeetle.com/reference/requests/create_transfers#). - """ - - use TypedStruct - - alias TigerBeetlex.CreateTransferResult - - typedstruct do - field :index, non_neg_integer(), default: 0 - field :result, atom() - end - - @doc """ - Creates a `TigerBeetlex.CreateTransfersResult` struct from its binary representation. - """ - @spec from_binary(binary :: <<_::64>>) :: t() - def from_binary(<<_::binary-size(8)>> = bin) do - << - index::unsigned-little-32, - result::unsigned-little-32 - >> = bin - - %__MODULE__{ - index: index, - result: CreateTransferResult.to_atom(result) - } - end - - @doc """ - Converts a `TigerBeetlex.CreateTransfersResult` struct to its binary representation. - """ - @spec to_binary(struct :: t()) :: <<_::64>> - def to_binary(struct) do - %__MODULE__{ - index: index, - result: result - } = struct - - << - index::unsigned-little-32, - CreateTransferResult.from_atom(result)::unsigned-little-32 - >> - end -end diff --git a/lib/tigerbeetlex/bindings/operation.ex b/lib/tigerbeetlex/bindings/operation.ex index 689c8a2..e11017d 100644 --- a/lib/tigerbeetlex/bindings/operation.ex +++ b/lib/tigerbeetlex/bindings/operation.ex @@ -3,23 +3,23 @@ defmodule TigerBeetlex.Operation do def available_operations do [ - :create_accounts, - :create_transfers, :lookup_accounts, :lookup_transfers, :get_account_transfers, :get_account_balances, :query_accounts, - :query_transfers + :query_transfers, + :create_accounts, + :create_transfers ] end - def from_atom(:create_accounts), do: 138 - def from_atom(:create_transfers), do: 139 def from_atom(:lookup_accounts), do: 140 def from_atom(:lookup_transfers), do: 141 def from_atom(:get_account_transfers), do: 142 def from_atom(:get_account_balances), do: 143 def from_atom(:query_accounts), do: 144 def from_atom(:query_transfers), do: 145 + def from_atom(:create_accounts), do: 146 + def from_atom(:create_transfers), do: 147 end diff --git a/lib/tigerbeetlex/bindings/response.ex b/lib/tigerbeetlex/bindings/response.ex index 1bf827a..13667b7 100644 --- a/lib/tigerbeetlex/bindings/response.ex +++ b/lib/tigerbeetlex/bindings/response.ex @@ -13,14 +13,14 @@ defmodule TigerBeetlex.Response do If successful, it returns `{:ok, list}`. The type of the items of the list depend on the operation. - - `create_accounts`: a list of `%TigerBeetlex.CreateAccountsResult{}` - - `create_transfers`: a list of `%TigerBeetlex.CreateTransfersResult{}` - `lookup_accounts`: a list of `%TigerBeetlex.Account{}` - `lookup_transfers`: a list of `%TigerBeetlex.Transfer{}` - `get_account_transfers`: a list of `%TigerBeetlex.Transfer{}` - `get_account_balances`: a list of `%TigerBeetlex.AccountBalance{}` - `query_accounts`: a list of `%TigerBeetlex.Account{}` - `query_transfers`: a list of `%TigerBeetlex.Transfer{}` + - `create_accounts`: a list of `%TigerBeetlex.CreateAccountResult{}` + - `create_transfers`: a list of `%TigerBeetlex.CreateTransferResult{}` """ def decode({0, operation, batch} = _response) do {:ok, build_result_list(operation, batch)} @@ -54,18 +54,6 @@ defmodule TigerBeetlex.Response do {:error, :invalid_data_size} end - defp build_result_list(138, batch) when rem(bit_size(batch), 64) == 0 do - for <> do - TigerBeetlex.CreateAccountsResult.from_binary(item) - end - end - - defp build_result_list(139, batch) when rem(bit_size(batch), 64) == 0 do - for <> do - TigerBeetlex.CreateTransfersResult.from_binary(item) - end - end - defp build_result_list(140, batch) when rem(bit_size(batch), 1024) == 0 do for <> do TigerBeetlex.Account.from_binary(item) @@ -102,6 +90,18 @@ defmodule TigerBeetlex.Response do end end + defp build_result_list(146, batch) when rem(bit_size(batch), 128) == 0 do + for <> do + TigerBeetlex.CreateAccountResult.from_binary(item) + end + end + + defp build_result_list(147, batch) when rem(bit_size(batch), 128) == 0 do + for <> do + TigerBeetlex.CreateTransferResult.from_binary(item) + end + end + @doc false def status_map do %{ diff --git a/lib/tigerbeetlex/client.ex b/lib/tigerbeetlex/client.ex index 8bb4798..20b9d75 100644 --- a/lib/tigerbeetlex/client.ex +++ b/lib/tigerbeetlex/client.ex @@ -97,10 +97,11 @@ defmodule TigerBeetlex.Client do `accounts` is a list of `TigerBeetlex.Account` structs. - The decoded `results` are a list of `TigerBeetlex.CreateAccountsResult` structs - which contain the index of the account list and the reason of the failure. An account has a - corresponding `TigerBeetlex.CreateAccountsResult` only if it fails to be created, otherwise - the account has been created succesfully (so a successful request returns an empty list). + The decoded `results` are a list of `TigerBeetlex.CreateAccountResult` structs. + + The list contains one result for each account in the submitted batch, in the same order. + Successful creations have `status: :created`, existing accounts have `status: :exists`, and + all other statuses indicate that the account was not created. See [`create_accounts`](https://docs.tigerbeetle.com/reference/requests/create_accounts/). @@ -115,7 +116,7 @@ defmodule TigerBeetlex.Client do Client.receive_and_decode(ref) - #=> {:ok, []} + #=> {:ok, [%TigerBeetlex.CreateAccountResult{status: :created}]} # Creation error accounts = [%Account{id: ID.from_int(0), ledger: 3, code: 4}] @@ -124,7 +125,7 @@ defmodule TigerBeetlex.Client do Client.receive_and_decode(ref) - #=> {:ok, [%TigerBeetlex.CreateAccountsResult{index: 0, result: :id_must_not_be_zero}]} + #=> {:ok, [%TigerBeetlex.CreateAccountResult{status: :id_must_not_be_zero}]} """ @spec create_accounts(client :: t(), accounts :: [Account.t()]) :: {:ok, reference()} | {:error, Types.request_error()} @@ -141,10 +142,11 @@ defmodule TigerBeetlex.Client do `transfers` is a list of `TigerBeetlex.Transfer` structs. - The decoded `results` are a list of `TigerBeetlex.CreateTransfersResult` structs - which contain the index of the transfer list and the reason of the failure. A transfer has a - corresponding `TigerBeetlex.CreateTransfersResult` only if it fails to be created, otherwise - the transfer has been created succesfully (so a successful request returns an empty list). + The decoded `results` are a list of `TigerBeetlex.CreateTransferResult` structs. + + The list contains one result for each transfer in the submitted batch, in the same order. + Successful creations have `status: :created`, existing transfers have `status: :exists`, and + all other statuses indicate that the transfer was not created. See [`create_transfers`](https://docs.tigerbeetle.com/reference/requests/create_transfers/). @@ -168,7 +170,7 @@ defmodule TigerBeetlex.Client do Client.receive_and_decode(ref) - #=> {:ok, []} + #=> {:ok, [%TigerBeetlex.CreateTransferResult{status: :created}]} # Creation error transfers = [ @@ -186,7 +188,7 @@ defmodule TigerBeetlex.Client do Client.receive_and_decode(ref) - #=> {:ok, [%TigerBeetlex.CreateTransfersResult{index: 0, result: :id_must_not_be_zero}]} + #=> {:ok, [%TigerBeetlex.CreateTransferResult{status: :id_must_not_be_zero}]} """ @spec create_transfers(client :: t(), transfers :: [Transfer.t()]) :: {:ok, reference()} | {:error, Types.request_error()} diff --git a/lib/tigerbeetlex/connection.ex b/lib/tigerbeetlex/connection.ex index ebe573d..50257f6 100644 --- a/lib/tigerbeetlex/connection.ex +++ b/lib/tigerbeetlex/connection.ex @@ -13,8 +13,8 @@ defmodule TigerBeetlex.Connection do alias TigerBeetlex.AccountBalance alias TigerBeetlex.AccountFilter alias TigerBeetlex.Client - alias TigerBeetlex.CreateAccountsResult - alias TigerBeetlex.CreateTransfersResult + alias TigerBeetlex.CreateAccountResult + alias TigerBeetlex.CreateTransferResult alias TigerBeetlex.Operation alias TigerBeetlex.QueryFilter alias TigerBeetlex.Receiver @@ -123,10 +123,11 @@ defmodule TigerBeetlex.Connection do `accounts` is a list of `TigerBeetlex.Account` structs. If successful, the function returns `{:ok, results}` where `results` is a list of - `TigerBeetlex.CreateAccountsResult` structs which contain the index - of the account list and the reason of the failure. An account has a corresponding - `TigerBeetlex.CreateAccountsResult` only if it fails to be created, otherwise the account - has been created succesfully (so a successful request returns an empty list). + `TigerBeetlex.CreateAccountResult` structs. + + The list contains one result for each account in the submitted batch, in the same order. + Successful creations have `status: :created`, existing accounts have `status: :exists`, and + all other statuses indicate that the account was not created. See [`create_accounts`](https://docs.tigerbeetle.com/reference/requests/create_accounts/). @@ -138,20 +139,20 @@ defmodule TigerBeetlex.Connection do accounts = [%Account{id: ID.generate(), ledger: 3, code: 4}] TigerBeetlex.Connection.create_accounts(:tb, accounts) - #=> {:ok, []} + #=> {:ok, [%TigerBeetlex.CreateAccountResult{status: :created}]} # Creation error accounts = [%Account{id: ID.from_int(0), ledger: 3, code: 4}] TigerBeetlex.Connection.create_accounts(:tb, accounts) - #=> {:ok, [%TigerBeetlex.CreateAccountsResult{index: 0, result: :id_must_not_be_zero}]} + #=> {:ok, [%TigerBeetlex.CreateAccountResult{status: :id_must_not_be_zero}]} """ @spec create_accounts( name :: PartitionSupervisor.name(), accounts :: [Account.t()] ) :: - {:ok, [CreateAccountsResult.t()]} | {:error, Types.request_error()} + {:ok, [CreateAccountResult.t()]} | {:error, Types.request_error()} def create_accounts(name, accounts) when is_list(accounts) do submit(name, :create_accounts, accounts) end @@ -164,10 +165,11 @@ defmodule TigerBeetlex.Connection do `transfers` is a list of `TigerBeetlex.Transfer` structs. If successful, the function returns `{:ok, results}` where `results` is a list of - `TigerBeetlex.CreateTransfersResult` structs which contain the index - of the transfer list and the reason of the failure. A transfer has a corresponding - `TigerBeetlex.CreateTransfersResult` only if it fails to be created, otherwise the transfer - has been created succesfully (so a successful request returns an empty list). + `TigerBeetlex.CreateTransferResult` structs. + + The list contains one result for each transfer in the submitted batch, in the same order. + Successful creations have `status: :created`, existing transfers have `status: :exists`, and + all other statuses indicate that the transfer was not created. See [`create_transfers`](https://docs.tigerbeetle.com/reference/requests/create_transfers/). @@ -188,7 +190,7 @@ defmodule TigerBeetlex.Connection do ] TigerBeetlex.Connection.create_transfers(:tb, transfers) - #=> {:ok, []} + #=> {:ok, [%TigerBeetlex.CreateTransferResult{status: :created}]} # Creation error transfers = [ @@ -203,13 +205,13 @@ defmodule TigerBeetlex.Connection do ] TigerBeetlex.Connection.create_transfers(:tb, transfers) - #=> {:ok, [%TigerBeetlex.CreateTransferError{index: 0, result: :id_must_not_be_zero}]} + #=> {:ok, [%TigerBeetlex.CreateTransferResult{status: :id_must_not_be_zero}]} """ @spec create_transfers( name :: PartitionSupervisor.name(), transfers :: [Transfer.t()] ) :: - {:ok, [CreateTransfersResult.t()]} | {:error, Types.request_error()} + {:ok, [CreateTransferResult.t()]} | {:error, Types.request_error()} def create_transfers(name, transfers) when is_list(transfers) do submit(name, :create_transfers, transfers) end diff --git a/mix.exs b/mix.exs index 2da6f9d..c6e1a60 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule TigerBeetlex.MixProject do use Mix.Project - @version "0.16.78" + @version "0.17.0" @repo_url "https://github.com/rbino/tigerbeetlex" @@ -30,8 +30,10 @@ defmodule TigerBeetlex.MixProject do TigerBeetlex.AccountFilter, TigerBeetlex.AccountFilterFlags, TigerBeetlex.AccountFlags, - TigerBeetlex.CreateAccountsResult, - TigerBeetlex.CreateTransfersResult, + TigerBeetlex.CreateAccountResult, + TigerBeetlex.CreateAccountStatus, + TigerBeetlex.CreateTransferResult, + TigerBeetlex.CreateTransferStatus, TigerBeetlex.QueryFilter, TigerBeetlex.QueryFilterFlags, TigerBeetlex.Transfer, diff --git a/test/concurrency_test.exs b/test/concurrency_test.exs index 2f40b07..6d9a4c6 100644 --- a/test/concurrency_test.exs +++ b/test/concurrency_test.exs @@ -3,6 +3,7 @@ defmodule TigerBeetlex.ConcurrencyTest do alias TigerBeetlex.Account alias TigerBeetlex.Client + alias TigerBeetlex.CreateAccountResult alias TigerBeetlex.ID alias TigerBeetlex.Response @@ -26,9 +27,12 @@ defmodule TigerBeetlex.ConcurrencyTest do {:ok, ref} = Client.create_accounts(client, [account]) - assert_receive {:tigerbeetlex_response, ^ref, response}, 1_000 + assert_receive {:tigerbeetlex_response, ^ref, response}, 5_000 - assert {:ok, []} = Response.decode(response) + assert {:ok, [%CreateAccountResult{status: :created, timestamp: timestamp}]} = + Response.decode(response) + + assert timestamp > 0 end end) end diff --git a/test/integration_test.exs b/test/integration_test.exs index 66b2ffa..0570e25 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -6,8 +6,8 @@ defmodule TigerBeetlex.IntegrationTest do alias TigerBeetlex.AccountFilter alias TigerBeetlex.AccountFlags alias TigerBeetlex.Connection - alias TigerBeetlex.CreateAccountsResult - alias TigerBeetlex.CreateTransfersResult + alias TigerBeetlex.CreateAccountResult + alias TigerBeetlex.CreateTransferResult alias TigerBeetlex.ID alias TigerBeetlex.QueryFilter alias TigerBeetlex.QueryFilterFlags @@ -44,7 +44,7 @@ defmodule TigerBeetlex.IntegrationTest do flags: %AccountFlags{credits_must_not_exceed_debits: true} } - assert {:ok, []} = Connection.create_accounts(conn, [account]) + assert_accounts_created(conn, [account]) assert %Account{ id: ^id, @@ -73,7 +73,7 @@ defmodule TigerBeetlex.IntegrationTest do } ] - assert {:ok, []} = Connection.create_accounts(conn, accounts) + assert_accounts_created(conn, accounts) assert [ %Account{ @@ -100,9 +100,7 @@ defmodule TigerBeetlex.IntegrationTest do assert {:ok, results} = Connection.create_accounts(conn, [account]) - assert [ - %CreateAccountsResult{index: 0, result: :id_must_not_be_zero} - ] == results + assert_account_results(results, [:id_must_not_be_zero]) end test "failed linked account creation", %{conn: conn} do @@ -126,10 +124,7 @@ defmodule TigerBeetlex.IntegrationTest do assert {:ok, results} = Connection.create_accounts(conn, accounts) - assert [ - %CreateAccountsResult{index: 0, result: :linked_event_failed}, - %CreateAccountsResult{index: 1, result: :ledger_must_not_be_zero} - ] == results + assert_account_results(results, [:linked_event_failed, :ledger_must_not_be_zero]) assert_account_not_existing(conn, id_1) assert_account_not_existing(conn, id_2) @@ -159,9 +154,7 @@ defmodule TigerBeetlex.IntegrationTest do assert {:ok, results} = Connection.create_accounts(conn, accounts) - assert [ - %CreateAccountsResult{index: 1, result: :flags_are_mutually_exclusive} - ] == results + assert_account_results(results, [:created, :flags_are_mutually_exclusive]) assert %Account{ id: ^id_1, @@ -184,7 +177,7 @@ defmodule TigerBeetlex.IntegrationTest do } end - assert {:ok, []} = Connection.create_accounts(conn, accounts) + assert_accounts_created(conn, accounts) end end @@ -220,7 +213,7 @@ defmodule TigerBeetlex.IntegrationTest do amount: 100 } - assert {:ok, []} = Connection.create_transfers(conn, [transfer]) + assert_transfers_created(conn, [transfer]) assert %Transfer{ id: ^id, @@ -277,7 +270,7 @@ defmodule TigerBeetlex.IntegrationTest do } ] - assert {:ok, []} = Connection.create_transfers(conn, transfers) + assert_transfers_created(conn, transfers) assert [ %Transfer{ @@ -332,7 +325,7 @@ defmodule TigerBeetlex.IntegrationTest do flags: %TransferFlags{pending: true} } - assert {:ok, []} = Connection.create_transfers(conn, [pending_transfer]) + assert_transfers_created(conn, [pending_transfer]) assert %Transfer{ id: ^pending_id, @@ -371,7 +364,7 @@ defmodule TigerBeetlex.IntegrationTest do flags: %TransferFlags{post_pending_transfer: true} } - assert {:ok, []} = Connection.create_transfers(conn, [post_pending_transfer]) + assert_transfers_created(conn, [post_pending_transfer]) assert %Transfer{ id: ^post_pending_id, @@ -420,7 +413,7 @@ defmodule TigerBeetlex.IntegrationTest do flags: %TransferFlags{pending: true} } - assert {:ok, []} = Connection.create_transfers(conn, [pending_transfer]) + assert_transfers_created(conn, [pending_transfer]) assert %Transfer{ id: ^pending_id, @@ -456,7 +449,7 @@ defmodule TigerBeetlex.IntegrationTest do flags: %TransferFlags{void_pending_transfer: true} } - assert {:ok, []} = Connection.create_transfers(conn, [void_pending_transfer]) + assert_transfers_created(conn, [void_pending_transfer]) assert %Transfer{ id: ^void_pending_id, @@ -505,9 +498,7 @@ defmodule TigerBeetlex.IntegrationTest do assert {:ok, results} = Connection.create_transfers(conn, [transfer]) - assert [ - %CreateTransfersResult{index: 0, result: :accounts_must_be_different} - ] == results + assert_transfer_results(results, [:accounts_must_be_different]) assert_transfer_not_existing(conn, id) end @@ -544,13 +535,7 @@ defmodule TigerBeetlex.IntegrationTest do assert {:ok, results} = Connection.create_transfers(conn, transfers) - assert [ - %CreateTransfersResult{index: 0, result: :linked_event_failed}, - %CreateTransfersResult{ - index: 1, - result: :transfer_must_have_the_same_ledger_as_accounts - } - ] == results + assert_transfer_results(results, [:linked_event_failed, :transfer_must_have_the_same_ledger_as_accounts]) assert_transfer_not_existing(conn, id_1) assert_transfer_not_existing(conn, id_2) @@ -587,12 +572,7 @@ defmodule TigerBeetlex.IntegrationTest do assert {:ok, results} = Connection.create_transfers(conn, transfers) - assert [ - %CreateTransfersResult{ - index: 1, - result: :code_must_not_be_zero - } - ] == results + assert_transfer_results(results, [:created, :code_must_not_be_zero]) assert %Transfer{ id: ^id_1, @@ -752,7 +732,7 @@ defmodule TigerBeetlex.IntegrationTest do } ] - assert {:ok, []} = Connection.create_accounts(conn, accounts) + assert_accounts_created(conn, accounts) transfers = [ %Transfer{ @@ -775,7 +755,7 @@ defmodule TigerBeetlex.IntegrationTest do } ] - assert {:ok, []} = Connection.create_transfers(conn, transfers) + assert_transfers_created(conn, transfers) assert %Account{ ledger: 1, @@ -853,7 +833,7 @@ defmodule TigerBeetlex.IntegrationTest do } ] - assert {:ok, []} = Connection.create_accounts(conn, accounts) + assert_accounts_created(conn, accounts) transfers = [ %Transfer{ @@ -876,7 +856,7 @@ defmodule TigerBeetlex.IntegrationTest do } ] - assert {:ok, []} = Connection.create_transfers(conn, transfers) + assert_transfers_created(conn, transfers) assert %Account{ ledger: 1, @@ -948,7 +928,7 @@ defmodule TigerBeetlex.IntegrationTest do other_user_data_account ] - assert {:ok, []} = Connection.create_accounts(conn, accounts) + assert_accounts_created(conn, accounts) query_filter = %QueryFilter{user_data_128: <<42::128>>, code: target_code, limit: 10} @@ -974,7 +954,7 @@ defmodule TigerBeetlex.IntegrationTest do code: target_code } - assert {:ok, []} = Connection.create_accounts(conn, [account_1, account_2]) + assert_accounts_created(conn, [account_1, account_2]) query_filter = %QueryFilter{ code: target_code, @@ -1043,7 +1023,7 @@ defmodule TigerBeetlex.IntegrationTest do other_user_data_transfer ] - assert {:ok, []} = Connection.create_transfers(conn, transfers) + assert_transfers_created(conn, transfers) query_filter = %QueryFilter{user_data_128: <<42::128>>, code: target_code, limit: 10} @@ -1079,7 +1059,7 @@ defmodule TigerBeetlex.IntegrationTest do code: target_code } - assert {:ok, []} = Connection.create_transfers(conn, [transfer_1, transfer_2]) + assert_transfers_created(conn, [transfer_1, transfer_2]) query_filter = %QueryFilter{ code: target_code, @@ -1101,8 +1081,7 @@ defmodule TigerBeetlex.IntegrationTest do code: 1 } - {:ok, results} = Connection.create_accounts(conn, [account]) - assert [] = results + assert_accounts_created(conn, [account]) id end @@ -1122,12 +1101,39 @@ defmodule TigerBeetlex.IntegrationTest do amount: 100 } - {:ok, results} = Connection.create_transfers(conn, [transfer]) - assert [] = results + assert_transfers_created(conn, [transfer]) id end + defp assert_accounts_created(conn, accounts) do + assert {:ok, results} = Connection.create_accounts(conn, accounts) + assert_account_results(results, List.duplicate(:created, length(accounts))) + results + end + + defp assert_transfers_created(conn, transfers) do + assert {:ok, results} = Connection.create_transfers(conn, transfers) + assert_transfer_results(results, List.duplicate(:created, length(transfers))) + results + end + + defp assert_account_results(results, expected_statuses) do + assert Enum.all?(results, &match?(%CreateAccountResult{}, &1)) + assert_result_statuses(results, expected_statuses) + end + + defp assert_transfer_results(results, expected_statuses) do + assert Enum.all?(results, &match?(%CreateTransferResult{}, &1)) + assert_result_statuses(results, expected_statuses) + end + + defp assert_result_statuses(results, expected_statuses) do + assert length(results) == length(expected_statuses) + assert Enum.map(results, & &1.status) == expected_statuses + assert Enum.all?(results, &(&1.timestamp > 0)) + end + defp get_balances!(conn, account_id) do account_filter = %AccountFilter{ account_id: account_id, diff --git a/test/tigerbeetlex/client_test.exs b/test/tigerbeetlex/client_test.exs index 36b4847..dd656f6 100644 --- a/test/tigerbeetlex/client_test.exs +++ b/test/tigerbeetlex/client_test.exs @@ -3,7 +3,7 @@ defmodule Tigerbeetlex.ClientTest do alias TigerBeetlex.Account alias TigerBeetlex.Client - alias TigerBeetlex.CreateAccountsResult + alias TigerBeetlex.CreateAccountResult alias TigerBeetlex.ID describe "new/2" do @@ -33,8 +33,10 @@ defmodule Tigerbeetlex.ClientTest do {:ok, ref} = Client.create_accounts(client, accounts) - assert Client.receive_and_decode(ref) == - {:ok, [%CreateAccountsResult{index: 0, result: :id_must_not_be_zero}]} + assert {:ok, [%CreateAccountResult{status: :id_must_not_be_zero, timestamp: timestamp}]} = + Client.receive_and_decode(ref) + + assert timestamp > 0 end end end diff --git a/test/tigerbeetlex/response_test.exs b/test/tigerbeetlex/response_test.exs index 075d2a3..609fd78 100644 --- a/test/tigerbeetlex/response_test.exs +++ b/test/tigerbeetlex/response_test.exs @@ -3,8 +3,8 @@ defmodule TigerBeetlex.ResponseTest do alias TigerBeetlex.Account alias TigerBeetlex.AccountBalance - alias TigerBeetlex.CreateAccountsResult - alias TigerBeetlex.CreateTransfersResult + alias TigerBeetlex.CreateAccountResult + alias TigerBeetlex.CreateTransferResult alias TigerBeetlex.Operation alias TigerBeetlex.Response alias TigerBeetlex.Transfer @@ -33,19 +33,23 @@ defmodule TigerBeetlex.ResponseTest do end describe "decode/1" do - test "returns list of CreateAccountsResult for create_accounts operation" do - assert {:ok, [%CreateAccountsResult{}]} = + test "returns list of CreateAccountResult for create_accounts operation" do + result = CreateAccountResult.to_binary(%CreateAccountResult{timestamp: 42, status: :created}) + + assert {:ok, [%CreateAccountResult{timestamp: 42, status: :created}]} = :create_accounts |> Operation.from_atom() - |> ok_response(<<0::unsigned-little-32, 1::unsigned-little-32>>) + |> ok_response(result) |> Response.decode() end - test "returns list of CreateTransfersResult for create_transfers operation" do - assert {:ok, [%CreateTransfersResult{}]} = + test "returns list of CreateTransferResult for create_transfers operation" do + result = CreateTransferResult.to_binary(%CreateTransferResult{timestamp: 42, status: :created}) + + assert {:ok, [%CreateTransferResult{timestamp: 42, status: :created}]} = :create_transfers |> Operation.from_atom() - |> ok_response(<<0::unsigned-little-32, 1::unsigned-little-32>>) + |> ok_response(result) |> Response.decode() end diff --git a/tools/elixir_bindings.zig b/tools/elixir_bindings.zig index 1d23e41..d9a80d7 100644 --- a/tools/elixir_bindings.zig +++ b/tools/elixir_bindings.zig @@ -100,26 +100,28 @@ const type_mappings = .{ .hidden_fields = &.{"reserved"}, .docs_link = "reference/account-filter#", } }, + .{ tb.CreateAccountStatus, TypeMapping{ + .file_name = "create_account_status", + .module_name = "CreateAccountStatus", + .docs_link = "reference/requests/create_accounts#status", + } }, .{ tb.CreateAccountResult, TypeMapping{ .file_name = "create_account_result", .module_name = "CreateAccountResult", + .hidden_fields = &.{"reserved"}, .docs_link = "reference/requests/create_accounts#result", } }, + .{ tb.CreateTransferStatus, TypeMapping{ + .file_name = "create_transfer_status", + .module_name = "CreateTransferStatus", + .docs_link = "reference/requests/create_transfers#status", + } }, .{ tb.CreateTransferResult, TypeMapping{ .file_name = "create_transfer_result", .module_name = "CreateTransferResult", + .hidden_fields = &.{"reserved"}, .docs_link = "reference/requests/create_transfers#result", } }, - .{ tb.CreateAccountsResult, TypeMapping{ - .file_name = "create_accounts_result", - .module_name = "CreateAccountsResult", - .docs_link = "reference/requests/create_accounts#", - } }, - .{ tb.CreateTransfersResult, TypeMapping{ - .file_name = "create_transfers_result", - .module_name = "CreateTransfersResult", - .docs_link = "reference/requests/create_transfers#", - } }, }; fn emit_flags(