Skip to content

Commit b1bca1f

Browse files
committed
fix(s090): type the UUID columns the schemaless ledger reads, for Postgres
The pull request failed the postgres leg while the gate passed, twelve times with one shape: Postgrex expected a binary of 16 bytes, got a UUID string. A schemaless query has no schema to tell the adapter that session_id is a UUID. SQLite stores one as text and accepts the string; Postgres wants sixteen bytes and refuses it. The queries now annotate the type, which is correct on both. The test seeded rows the same way and with the same result, so it seeds through the schema instead, which dumps the UUID and the JSON correctly wherever it runs and surfaced a third thing the schemaless path had hidden: utc_datetime_usec refuses a second-truncated timestamp. This is the second cost of the schemaless choice, and the choice is still right. The first was losing the compiler's check on column names, answered with a test that reads the table's columns; this is the second. Both are the price of a boundary rule that is correct, and neither is an argument against it. Verified locally rather than by pushing again: a pgvector pg17 container on 5433, MIX_BUILD_PATH=_build_pg, and the leg's own command, mix test --exclude sqlite, giving 656 passed and 38 excluded. Re-running CI to find out would have been slower and told me less. NOTES also records a mistake that cannot be undone. slice/090 was tagged and pushed before the merge it named was confirmed, and the merge had not happened because this very check was red, so the tag points at slice 080's merge commit. The ruleset forbids deleting or moving a tag, which is the correct rule working as intended, so the error is permanent and public. It is recorded beside the truth rather than worked around: the real commit is named in PROOF and in coverage.tsv. The rule that prevents a repeat is to tag after a merge is confirmed, never after one is requested. Signed-off-by: Ayla Croft <aylacroft@proton.me>
1 parent 1ab456a commit b1bca1f

3 files changed

Lines changed: 32 additions & 29 deletions

File tree

‎lib/trinity/telemetry/costs.ex‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ defmodule Trinity.Telemetry.Costs do
5959
@spec for_session(String.t()) :: float()
6060
def for_session(session_id) do
6161
@table
62-
|> where([u], u.session_id == ^session_id)
62+
# `type/2` is not decoration here. A schemaless query has no schema to tell the adapter this
63+
# column is a UUID: SQLite stores one as text and accepts the string, Postgres wants sixteen
64+
# bytes and refuses it. The postgres leg caught that and the SQLite one never could.
65+
|> where([u], u.session_id == type(^session_id, Trinity.UUID))
6366
|> select([u], sum(u.cost_usd))
6467
|> Repo.one()
6568
|> zero_if_nil()
@@ -77,7 +80,7 @@ defmodule Trinity.Telemetry.Costs do
7780
from(u in @table,
7881
join: s in "sessions",
7982
on: s.id == u.session_id,
80-
where: s.persona_id == ^persona_id,
83+
where: s.persona_id == type(^persona_id, Trinity.UUID),
8184
select: sum(u.cost_usd)
8285
)
8386
|> Repo.one()

‎scripts/dev_activity.sh‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,14 @@ alias Trinity.LLM.Providers.Fake
4242
{:ok, session} = Sessions.create_session(%{persona_id: Sessions.default_persona().id, title: "Auditing the receipt chain"})
4343
IO.puts("SESSION=#{session.id}")
4444
45-
now = DateTime.utc_now() |> DateTime.truncate(:second)
46-
rows =
47-
for {model, cost, n} <- [{"anthropic:claude", 0.0121, 4}, {"openai:gpt", 0.0043, 6}, {"local:qwen", 0.0002, 11}],
48-
_ <- 1..n do
49-
%{id: Trinity.UUID.generate(), model_id: model, provider: "fake", kind: "stream",
50-
input_tokens: 900, output_tokens: 420, cached_tokens: 0, reasoning_tokens: 0,
51-
cost_usd: cost, session_id: session.id, provider_meta: JSON.encode!(%{}), inserted_at: now}
52-
end
53-
Repo.insert_all("usage_events", rows)
45+
# Seeded through the schema, so the UUID and JSON columns are dumped correctly whichever adapter
46+
# is underneath. A schemaless insert works on SQLite and is refused by Postgres.
47+
for {model, cost, n} <- [{"anthropic:claude", 0.0121, 4}, {"openai:gpt", 0.0043, 6}, {"local:qwen", 0.0002, 11}],
48+
_ <- 1..n do
49+
Repo.insert!(%Trinity.LLM.Usage{model_id: model, provider: "fake", kind: "stream",
50+
input_tokens: 900, output_tokens: 420, cached_tokens: 0, reasoning_tokens: 0,
51+
cost_usd: cost, session_id: session.id, provider_meta: %{}, inserted_at: DateTime.utc_now()})
52+
end
5453
5554
Trinity.Telemetry.approval_requested("write_note", :write, session.id)
5655
Trinity.Telemetry.approval_decided("write_note", :write, session.id, decision: :once, basis: :user, waited_ms: 3400)

‎test/trinity/telemetry/costs_test.exs‎

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,26 @@ defmodule Trinity.Telemetry.CostsTest do
1616
alias Trinity.Sessions
1717
alias Trinity.Telemetry.Costs
1818

19+
# Inserted through the schema rather than schemalessly. A schemaless insert has no schema to tell
20+
# the adapter which columns are UUIDs or JSON, so it passes a string and a map straight through:
21+
# SQLite accepts both, Postgres refuses both. Seeding through the schema keeps this test honest
22+
# on the adapter the postgres leg runs, which is where the first version of it failed.
1923
defp seed!(session_id, cost, opts \\ []) do
20-
Repo.insert_all("usage_events", [
21-
%{
22-
id: Trinity.UUID.generate(),
23-
model_id: Keyword.get(opts, :model, "fake:chat"),
24-
provider: "fake",
25-
kind: "stream",
26-
input_tokens: 10,
27-
output_tokens: 20,
28-
cached_tokens: 0,
29-
reasoning_tokens: 0,
30-
cost_usd: cost,
31-
session_id: session_id,
32-
# Encoded here rather than passed as a map: a schemaless insert has no schema to tell the
33-
# adapter this column is JSON, and SQLite refuses a bare map.
34-
provider_meta: JSON.encode!(%{}),
35-
inserted_at: Keyword.get(opts, :at, DateTime.utc_now() |> DateTime.truncate(:second))
36-
}
37-
])
24+
Repo.insert!(%Trinity.LLM.Usage{
25+
model_id: Keyword.get(opts, :model, "fake:chat"),
26+
provider: "fake",
27+
kind: "stream",
28+
input_tokens: 10,
29+
output_tokens: 20,
30+
cached_tokens: 0,
31+
reasoning_tokens: 0,
32+
cost_usd: cost,
33+
session_id: session_id,
34+
provider_meta: %{},
35+
# Microsecond precision, because the schema declares :utc_datetime_usec and truncating to the
36+
# second is a value it refuses. The schemaless version accepted it and wrote it anyway.
37+
inserted_at: Keyword.get(opts, :at, DateTime.utc_now())
38+
})
3839
end
3940

4041
defp session!(opts \\ []) do

0 commit comments

Comments
 (0)