• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thanos / terminusdb-client-elixir / 4f23525f7f740037ae6700b3cc5d797042bf07ab-PR-58

26 Jun 2026 06:45PM UTC coverage: 92.841% (-1.7%) from 94.588%
4f23525f7f740037ae6700b3cc5d797042bf07ab-PR-58

Pull #58

github

thanos
1. True divergence — Added a commit on main after creating the feature branch (inserting "from-main"), so both branches have diverged from the common ancestor. This avoids the fast-forward edge case that triggers the 500.
2. Retry on 500 — The rebase endpoint in TerminusDB 12.0.6 intermittently returns 500 ("Unexpected failure in request handler"). The test now retries up to 3 times on 500 errors, passing through other errors immediately.
3. Stronger assertions — Verifies both "from-feature" and "from-main" are present after merge, confirming the rebase correctly applied both branches' commits.
Pull Request #58: v0.3.2: GraphQL, temporal/Allen, RDF list, CSV/IO, range queries, API…

482 of 535 new or added lines in 13 files covered. (90.09%)

2 existing lines in 1 file now uncovered.

1206 of 1299 relevant lines covered (92.84%)

59.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.35
/lib/terminus_db/patch.ex
1
defmodule TerminusDB.Patch do
2
  @moduledoc """
3
  A JSON-LD patch container for TerminusDB document diffs.
4

5
  A `Patch` holds the raw JSON-LD patch content produced by `TerminusDB.Diff`
6
  operations. It provides convenience projections for extracting the "before"
7
  and "after" (update) states from `SwapValue` operations.
8

9
  ## Quick start
10

11
      {:ok, patch} = TerminusDB.Diff.diff_object(config,
12
        before: %{"@id" => "Person/1", "name" => "old"},
13
        after: %{"@id" => "Person/1", "name" => "new"}
14
      )
15

16
      patch.update   # => %{"name" => "new"}
17
      patch.before   # => %{"name" => "old"}
18

19
  """
20

21
  @enforce_keys [:content]
22
  defstruct [:content]
23

24
  @type t :: %__MODULE__{content: map() | [map()]}
25

26
  @doc """
27
  Parses a JSON string into a `%Patch{}`.
28

29
  ## Examples
30

31
      iex> {:ok, patch} = TerminusDB.Patch.from_json(~s({"name": {"@op": "SwapValue", "@before": "old", "@after": "new"}}))
32
      iex> patch.content["name"]["@after"]
33
      "new"
34

35
  """
36
  @spec from_json(String.t()) :: {:ok, t()} | {:error, term()}
37
  def from_json(json_string) do
38
    case Jason.decode(json_string) do
9✔
39
      {:ok, content} when is_map(content) -> {:ok, %__MODULE__{content: content}}
6✔
NEW
40
      {:ok, content} when is_list(content) -> {:ok, %__MODULE__{content: content}}
×
41
      {:error, _} = error -> error
3✔
42
    end
43
  end
44

45
  @doc """
46
  Parses a JSON string into a `%Patch{}`, or raises.
47

48
  ## Examples
49

50
      iex> patch = TerminusDB.Patch.from_json!(~s({"name": {"@op": "SwapValue", "@before": "old", "@after": "new"}}))
51
      iex> patch.content["name"]["@before"]
52
      "old"
53

54
  """
55
  @spec from_json!(String.t()) :: t()
56
  def from_json!(json_string) do
57
    case from_json(json_string) do
3✔
58
      {:ok, patch} -> patch
3✔
NEW
59
      {:error, error} -> raise ArgumentError, "invalid JSON: #{inspect(error)}"
×
60
    end
61
  end
62

63
  @doc """
64
  Serializes a `%Patch{}` to a JSON string.
65

66
  ## Examples
67

68
      iex> patch = %TerminusDB.Patch{content: %{"name" => %{"@op" => "SwapValue", "@before" => "old", "@after" => "new"}}}
69
      iex> json = TerminusDB.Patch.to_json(patch)
70
      iex> is_binary(json)
71
      true
72

73
  """
74
  @spec to_json(t()) :: String.t()
75
  def to_json(%__MODULE__{content: content}) do
76
    Jason.encode!(content)
3✔
77
  end
78

79
  @doc """
80
  Extracts the "after" (updated) values from `SwapValue` operations,
81
  recursively.
82

83
  Only fields containing `SwapValue` operations are included in the result.
84
  Non-`SwapValue` fields are omitted. This is intentionally asymmetric with
85
  `before/1`, which includes all fields (preserving non-`SwapValue` values)
86
  to allow full reconstruction of the "before" state.
87

88
  ## Examples
89

90
      iex> patch = %TerminusDB.Patch{content: %{"name" => %{"@op" => "SwapValue", "@before" => "old", "@after" => "new"}}}
91
      iex> TerminusDB.Patch.update(patch)
92
      %{"name" => "new"}
93

94
  """
95
  @spec update(t()) :: map()
96
  def update(%__MODULE__{content: content}) do
97
    extract_after(content)
15✔
98
  end
99

100
  @doc """
101
  Extracts the "before" values from `SwapValue` operations, recursively.
102

103
  Includes all fields from the patch content, preserving non-`SwapValue`
104
  values as-is. This is intentionally asymmetric with `update/1`, which
105
  only includes `SwapValue` fields — `before/1` aims to reconstruct the
106
  full "before" state while `update/1` shows only what changed.
107

108
  ## Examples
109

110
      iex> patch = %TerminusDB.Patch{content: %{"name" => %{"@op" => "SwapValue", "@before" => "old", "@after" => "new"}}}
111
      iex> TerminusDB.Patch.before(patch)
112
      %{"name" => "old"}
113

114
  """
115
  @spec before(t()) :: map()
116
  def before(%__MODULE__{content: content}) do
117
    extract_before(content)
12✔
118
  end
119

120
  @doc """
121
  Creates a deep copy of the patch.
122

123
  ## Examples
124

125
      iex> patch = %TerminusDB.Patch{content: %{"name" => "value"}}
126
      iex> copy = TerminusDB.Patch.copy(patch)
127
      iex> copy == patch
128
      true
129

130
  """
131
  @spec copy(t()) :: t()
132
  def copy(%__MODULE__{content: content}) do
133
    %__MODULE__{content: deep_copy(content)}
9✔
134
  end
135

136
  defp extract_after(item) when is_map(item) do
137
    case item do
21✔
138
      %{"@op" => "SwapValue", "@after" => after_val} ->
NEW
139
        after_val
×
140

141
      _ ->
142
        Enum.reduce(item, %{}, fn {key, val}, acc ->
21✔
143
          case val do
24✔
144
            %{"@op" => "SwapValue", "@after" => after_val} ->
145
              Map.put(acc, key, after_val)
12✔
146

147
            v when is_map(v) ->
148
              extracted = extract_after(v)
9✔
149

150
              if map_size(extracted) > 0 do
9✔
151
                Map.put(acc, key, extracted)
9✔
152
              else
NEW
153
                acc
×
154
              end
155

156
            _ ->
157
              acc
3✔
158
          end
159
        end)
160
    end
161
  end
162

163
  defp extract_after(_), do: %{}
3✔
164

165
  defp extract_before(item) when is_map(item) do
166
    Enum.reduce(item, %{}, fn {key, val}, acc ->
18✔
167
      case val do
27✔
168
        %{"@op" => "SwapValue", "@before" => before_val} ->
169
          Map.put(acc, key, before_val)
15✔
170

171
        v when is_map(v) ->
172
          extracted = extract_before(v)
6✔
173

174
          if map_size(extracted) > 0 do
6✔
175
            Map.put(acc, key, extracted)
6✔
176
          else
NEW
177
            acc
×
178
          end
179

180
        v ->
181
          Map.put(acc, key, v)
6✔
182
      end
183
    end)
184
  end
185

NEW
186
  defp extract_before(_), do: %{}
×
187

188
  defp deep_copy(term) when is_map(term) do
189
    Map.new(term, fn {k, v} -> {k, deep_copy(v)} end)
18✔
190
  end
191

192
  defp deep_copy(term) when is_list(term) do
193
    Enum.map(term, &deep_copy/1)
6✔
194
  end
195

196
  defp deep_copy(term), do: term
18✔
197
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc