Rename qsos table to contacts, qso_id to contact_id

- Migration renames table, column, all indexes, and FK constraints
- Contact schema now references "contacts" table
- TerrainProfile foreign key renamed from qso_id to contact_id
- All code and tests updated to use contact_id
This commit is contained in:
Graham McIntire 2026-04-02 15:52:19 -05:00
parent ae0e667cab
commit da75977a55
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
12 changed files with 87 additions and 40 deletions

View file

@ -9,7 +9,7 @@ defmodule Microwaveprop.Radio.Contact do
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id @foreign_key_type :binary_id
schema "qsos" do schema "contacts" do
field :station1, :string field :station1, :string
field :station2, :string field :station2, :string
field :qso_timestamp, :utc_datetime field :qso_timestamp, :utc_datetime

View file

@ -10,19 +10,19 @@ defmodule Microwaveprop.Terrain do
%TerrainProfile{} %TerrainProfile{}
|> TerrainProfile.changeset(attrs) |> TerrainProfile.changeset(attrs)
|> Repo.insert( |> Repo.insert(
on_conflict: {:replace_all_except, [:id, :qso_id, :inserted_at]}, on_conflict: {:replace_all_except, [:id, :contact_id, :inserted_at]},
conflict_target: [:qso_id], conflict_target: [:contact_id],
returning: true returning: true
) )
end end
def get_terrain_profile(qso_id) do def get_terrain_profile(contact_id) do
Repo.get_by(TerrainProfile, qso_id: qso_id) Repo.get_by(TerrainProfile, contact_id: contact_id)
end end
def has_terrain_profile?(qso_id) do def has_terrain_profile?(contact_id) do
TerrainProfile TerrainProfile
|> where([t], t.qso_id == ^qso_id) |> where([t], t.contact_id == ^contact_id)
|> Repo.exists?() |> Repo.exists?()
end end
end end

View file

@ -8,7 +8,7 @@ defmodule Microwaveprop.Terrain.TerrainProfile do
@foreign_key_type :binary_id @foreign_key_type :binary_id
schema "terrain_profiles" do schema "terrain_profiles" do
belongs_to :contact, Microwaveprop.Radio.Contact, foreign_key: :qso_id belongs_to :contact, Microwaveprop.Radio.Contact, foreign_key: :contact_id
field :sample_count, :integer field :sample_count, :integer
field :path_points, {:array, :map} field :path_points, {:array, :map}
field :max_elevation_m, :float field :max_elevation_m, :float
@ -21,14 +21,14 @@ defmodule Microwaveprop.Terrain.TerrainProfile do
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end
@required_fields ~w(qso_id sample_count path_points verdict)a @required_fields ~w(contact_id sample_count path_points verdict)a
@optional_fields ~w(max_elevation_m min_clearance_m diffraction_db fresnel_hit_count obstructed_count)a @optional_fields ~w(max_elevation_m min_clearance_m diffraction_db fresnel_hit_count obstructed_count)a
def changeset(profile, attrs) do def changeset(profile, attrs) do
profile profile
|> cast(attrs, @required_fields ++ @optional_fields) |> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields) |> validate_required(@required_fields)
|> foreign_key_constraint(:qso_id) |> foreign_key_constraint(:contact_id)
|> unique_constraint([:qso_id]) |> unique_constraint([:contact_id])
end end
end end

View file

@ -114,7 +114,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
contacts contacts
|> Enum.filter(fn contact -> contact.pos1 && contact.pos2 end) |> Enum.filter(fn contact -> contact.pos1 && contact.pos2 end)
|> Enum.map(fn contact -> |> Enum.map(fn contact ->
TerrainProfileWorker.new(%{"qso_id" => contact.id}) TerrainProfileWorker.new(%{"contact_id" => contact.id})
end) end)
|> Enum.uniq_by(fn changeset -> changeset.changes.args end) |> Enum.uniq_by(fn changeset -> changeset.changes.args end)
end end

View file

@ -14,11 +14,11 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
end end
@impl Oban.Worker @impl Oban.Worker
def perform(%Oban.Job{args: %{"qso_id" => qso_id}}) do def perform(%Oban.Job{args: %{"contact_id" => contact_id}}) do
if Terrain.has_terrain_profile?(qso_id) do if Terrain.has_terrain_profile?(contact_id) do
:ok :ok
else else
contact = Radio.get_contact!(qso_id) contact = Radio.get_contact!(contact_id)
with %{"lat" => lat1} <- contact.pos1, with %{"lat" => lat1} <- contact.pos1,
lon1 when is_number(lon1) <- contact.pos1["lon"] || contact.pos1["lng"], lon1 when is_number(lon1) <- contact.pos1["lon"] || contact.pos1["lng"],
@ -46,7 +46,7 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
end) end)
Terrain.upsert_terrain_profile(%{ Terrain.upsert_terrain_profile(%{
qso_id: qso_id, contact_id: contact_id,
sample_count: length(profile), sample_count: length(profile),
path_points: path_points, path_points: path_points,
max_elevation_m: analysis.max_elevation_m, max_elevation_m: analysis.max_elevation_m,
@ -57,12 +57,12 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
verdict: analysis.verdict verdict: analysis.verdict
}) })
Radio.set_enrichment_status!([qso_id], :terrain_status, :complete) Radio.set_enrichment_status!([contact_id], :terrain_status, :complete)
Phoenix.PubSub.broadcast( Phoenix.PubSub.broadcast(
Microwaveprop.PubSub, Microwaveprop.PubSub,
"contact_enrichment:#{qso_id}", "contact_enrichment:#{contact_id}",
{:terrain_ready, qso_id} {:terrain_ready, contact_id}
) )
:ok :ok
@ -72,7 +72,7 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
end end
else else
_ -> _ ->
Radio.set_enrichment_status!([qso_id], :terrain_status, :unavailable) Radio.set_enrichment_status!([contact_id], :terrain_status, :unavailable)
:ok :ok
end end
end end

View file

@ -217,7 +217,7 @@ defmodule MicrowavepropWeb.BackfillLive do
obs_count = Map.get(table_counts, "surface_observations", 0) obs_count = Map.get(table_counts, "surface_observations", 0)
sounding_count = Map.get(table_counts, "soundings", 0) sounding_count = Map.get(table_counts, "soundings", 0)
iemre_count = Map.get(table_counts, "iemre_observations", 0) iemre_count = Map.get(table_counts, "iemre_observations", 0)
contact_count = max(Map.get(table_counts, "qsos", 0), 1) contact_count = max(Map.get(table_counts, "contacts", 0), 1)
%{ %{
db_size: format_bytes(db_bytes), db_size: format_bytes(db_bytes),

View file

@ -117,7 +117,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
end end
@impl true @impl true
def handle_info({:terrain_ready, _qso_id}, socket) do def handle_info({:terrain_ready, _contact_id}, socket) do
contact = %{socket.assigns.contact | terrain_status: :complete} contact = %{socket.assigns.contact | terrain_status: :complete}
terrain = Terrain.get_terrain_profile(contact.id) terrain = Terrain.get_terrain_profile(contact.id)
soundings = socket.assigns.soundings soundings = socket.assigns.soundings
@ -287,7 +287,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
defp maybe_enqueue_terrain(nil, contact) do defp maybe_enqueue_terrain(nil, contact) do
if contact.pos1 && contact.pos2 && contact.terrain_status in [:pending, :failed, :queued] do if contact.pos1 && contact.pos2 && contact.terrain_status in [:pending, :failed, :queued] do
Oban.insert(TerrainProfileWorker.new(%{"qso_id" => contact.id})) Oban.insert(TerrainProfileWorker.new(%{"contact_id" => contact.id}))
Radio.set_enrichment_status!([contact.id], :terrain_status, :queued) Radio.set_enrichment_status!([contact.id], :terrain_status, :queued)
%{contact | terrain_status: :queued} %{contact | terrain_status: :queued}
else else

View file

@ -0,0 +1,47 @@
defmodule Microwaveprop.Repo.Migrations.RenameQsosToContacts do
use Ecto.Migration
def change do
rename table(:qsos), to: table(:contacts)
rename table(:terrain_profiles), :qso_id, to: :contact_id
# Rename indexes that reference the old table name
execute "ALTER INDEX IF EXISTS qsos_pkey RENAME TO contacts_pkey",
"ALTER INDEX IF EXISTS contacts_pkey RENAME TO qsos_pkey"
execute "ALTER INDEX IF EXISTS qsos_band_index RENAME TO contacts_band_index",
"ALTER INDEX IF EXISTS contacts_band_index RENAME TO qsos_band_index"
execute "ALTER INDEX IF EXISTS qsos_distance_km_index RENAME TO contacts_distance_km_index",
"ALTER INDEX IF EXISTS contacts_distance_km_index RENAME TO qsos_distance_km_index"
execute "ALTER INDEX IF EXISTS qsos_qso_timestamp_index RENAME TO contacts_qso_timestamp_index",
"ALTER INDEX IF EXISTS contacts_qso_timestamp_index RENAME TO qsos_qso_timestamp_index"
execute "ALTER INDEX IF EXISTS qsos_station1_trgm_index RENAME TO contacts_station1_trgm_index",
"ALTER INDEX IF EXISTS contacts_station1_trgm_index RENAME TO qsos_station1_trgm_index"
execute "ALTER INDEX IF EXISTS qsos_station2_trgm_index RENAME TO contacts_station2_trgm_index",
"ALTER INDEX IF EXISTS contacts_station2_trgm_index RENAME TO qsos_station2_trgm_index"
execute "ALTER INDEX IF EXISTS qsos_hrrr_status_pending_index RENAME TO contacts_hrrr_status_pending_index",
"ALTER INDEX IF EXISTS contacts_hrrr_status_pending_index RENAME TO qsos_hrrr_status_pending_index"
execute "ALTER INDEX IF EXISTS qsos_weather_status_pending_index RENAME TO contacts_weather_status_pending_index",
"ALTER INDEX IF EXISTS contacts_weather_status_pending_index RENAME TO qsos_weather_status_pending_index"
execute "ALTER INDEX IF EXISTS qsos_terrain_status_pending_index RENAME TO contacts_terrain_status_pending_index",
"ALTER INDEX IF EXISTS contacts_terrain_status_pending_index RENAME TO qsos_terrain_status_pending_index"
execute "ALTER INDEX IF EXISTS qsos_iemre_status_pending_index RENAME TO contacts_iemre_status_pending_index",
"ALTER INDEX IF EXISTS contacts_iemre_status_pending_index RENAME TO qsos_iemre_status_pending_index"
# Rename terrain_profiles foreign key constraint
execute "ALTER TABLE terrain_profiles RENAME CONSTRAINT terrain_profiles_qso_id_fkey TO terrain_profiles_contact_id_fkey",
"ALTER TABLE terrain_profiles RENAME CONSTRAINT terrain_profiles_contact_id_fkey TO terrain_profiles_qso_id_fkey"
execute "ALTER INDEX IF EXISTS terrain_profiles_qso_id_index RENAME TO terrain_profiles_contact_id_index",
"ALTER INDEX IF EXISTS terrain_profiles_contact_id_index RENAME TO terrain_profiles_qso_id_index"
end
end

View file

@ -28,7 +28,7 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
contact = create_contact() contact = create_contact()
attrs = %{ attrs = %{
qso_id: contact.id, contact_id: contact.id,
sample_count: 65, sample_count: 65,
path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}], path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}],
verdict: "CLEAR" verdict: "CLEAR"
@ -43,7 +43,7 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
refute changeset.valid? refute changeset.valid?
errors = errors_on(changeset) errors = errors_on(changeset)
assert errors[:qso_id] assert errors[:contact_id]
assert errors[:sample_count] assert errors[:sample_count]
assert errors[:path_points] assert errors[:path_points]
assert errors[:verdict] assert errors[:verdict]
@ -53,7 +53,7 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
contact = create_contact() contact = create_contact()
attrs = %{ attrs = %{
qso_id: contact.id, contact_id: contact.id,
sample_count: 65, sample_count: 65,
path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}], path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}],
verdict: "BLOCKED", verdict: "BLOCKED",
@ -68,11 +68,11 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
assert changeset.valid? assert changeset.valid?
end end
test "enforces unique qso_id constraint on insert" do test "enforces unique contact_id constraint on insert" do
contact = create_contact() contact = create_contact()
attrs = %{ attrs = %{
qso_id: contact.id, contact_id: contact.id,
sample_count: 65, sample_count: 65,
path_points: [], path_points: [],
verdict: "CLEAR" verdict: "CLEAR"
@ -88,7 +88,7 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
|> TerrainProfile.changeset(attrs) |> TerrainProfile.changeset(attrs)
|> Repo.insert() |> Repo.insert()
assert errors_on(changeset)[:qso_id] assert errors_on(changeset)[:contact_id]
end end
end end
end end

View file

@ -25,7 +25,7 @@ defmodule Microwaveprop.TerrainTest do
defp terrain_attrs(contact) do defp terrain_attrs(contact) do
%{ %{
qso_id: contact.id, contact_id: contact.id,
sample_count: 65, sample_count: 65,
path_points: [ path_points: [
%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}, %{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0},
@ -46,12 +46,12 @@ defmodule Microwaveprop.TerrainTest do
attrs = terrain_attrs(contact) attrs = terrain_attrs(contact)
assert {:ok, profile} = Terrain.upsert_terrain_profile(attrs) assert {:ok, profile} = Terrain.upsert_terrain_profile(attrs)
assert profile.qso_id == contact.id assert profile.contact_id == contact.id
assert profile.sample_count == 65 assert profile.sample_count == 65
assert profile.verdict == "CLEAR" assert profile.verdict == "CLEAR"
end end
test "upserts on conflict (same qso_id)" do test "upserts on conflict (same contact_id)" do
contact = create_contact() contact = create_contact()
attrs = terrain_attrs(contact) attrs = terrain_attrs(contact)
@ -71,7 +71,7 @@ defmodule Microwaveprop.TerrainTest do
{:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(contact)) {:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(contact))
profile = Terrain.get_terrain_profile(contact.id) profile = Terrain.get_terrain_profile(contact.id)
assert profile.qso_id == contact.id assert profile.contact_id == contact.id
end end
test "returns nil when no profile exists" do test "returns nil when no profile exists" do

View file

@ -370,10 +370,10 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
assert length(jobs) == 1 assert length(jobs) == 1
job = hd(jobs) job = hd(jobs)
assert job.changes.args["qso_id"] == contact.id assert job.changes.args["contact_id"] == contact.id
end end
test "deduplicates by qso_id" do test "deduplicates by contact_id" do
contact = create_contact() contact = create_contact()
jobs = ContactWeatherEnqueueWorker.build_terrain_jobs([contact, contact]) jobs = ContactWeatherEnqueueWorker.build_terrain_jobs([contact, contact])

View file

@ -42,7 +42,7 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
refute Terrain.has_terrain_profile?(contact.id) refute Terrain.has_terrain_profile?(contact.id)
job = TerrainProfileWorker.new(%{"qso_id" => contact.id}) job = TerrainProfileWorker.new(%{"contact_id" => contact.id})
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args}) assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
assert Terrain.has_terrain_profile?(contact.id) assert Terrain.has_terrain_profile?(contact.id)
@ -58,13 +58,13 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
{:ok, _} = {:ok, _} =
Terrain.upsert_terrain_profile(%{ Terrain.upsert_terrain_profile(%{
qso_id: contact.id, contact_id: contact.id,
sample_count: 65, sample_count: 65,
path_points: [], path_points: [],
verdict: "CLEAR" verdict: "CLEAR"
}) })
job = TerrainProfileWorker.new(%{"qso_id" => contact.id}) job = TerrainProfileWorker.new(%{"contact_id" => contact.id})
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args}) assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
end end
@ -75,7 +75,7 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
contact = create_contact() contact = create_contact()
job = TerrainProfileWorker.new(%{"qso_id" => contact.id}) job = TerrainProfileWorker.new(%{"contact_id" => contact.id})
assert {:error, _} = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args}) assert {:error, _} = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
refute Terrain.has_terrain_profile?(contact.id) refute Terrain.has_terrain_profile?(contact.id)
@ -85,7 +85,7 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
stub_elevation_api() stub_elevation_api()
contact = create_contact(%{band: Decimal.new("10368")}) contact = create_contact(%{band: Decimal.new("10368")})
job = TerrainProfileWorker.new(%{"qso_id" => contact.id}) job = TerrainProfileWorker.new(%{"contact_id" => contact.id})
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args}) assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
profile = Terrain.get_terrain_profile(contact.id) profile = Terrain.get_terrain_profile(contact.id)