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}
@foreign_key_type :binary_id
schema "qsos" do
schema "contacts" do
field :station1, :string
field :station2, :string
field :qso_timestamp, :utc_datetime

View file

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

View file

@ -8,7 +8,7 @@ defmodule Microwaveprop.Terrain.TerrainProfile do
@foreign_key_type :binary_id
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 :path_points, {:array, :map}
field :max_elevation_m, :float
@ -21,14 +21,14 @@ defmodule Microwaveprop.Terrain.TerrainProfile do
timestamps(type: :utc_datetime)
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
def changeset(profile, attrs) do
profile
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> foreign_key_constraint(:qso_id)
|> unique_constraint([:qso_id])
|> foreign_key_constraint(:contact_id)
|> unique_constraint([:contact_id])
end
end

View file

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

View file

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

View file

@ -217,7 +217,7 @@ defmodule MicrowavepropWeb.BackfillLive do
obs_count = Map.get(table_counts, "surface_observations", 0)
sounding_count = Map.get(table_counts, "soundings", 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),

View file

@ -117,7 +117,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
end
@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}
terrain = Terrain.get_terrain_profile(contact.id)
soundings = socket.assigns.soundings
@ -287,7 +287,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
defp maybe_enqueue_terrain(nil, contact) 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)
%{contact | terrain_status: :queued}
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()
attrs = %{
qso_id: contact.id,
contact_id: contact.id,
sample_count: 65,
path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}],
verdict: "CLEAR"
@ -43,7 +43,7 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
refute changeset.valid?
errors = errors_on(changeset)
assert errors[:qso_id]
assert errors[:contact_id]
assert errors[:sample_count]
assert errors[:path_points]
assert errors[:verdict]
@ -53,7 +53,7 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
contact = create_contact()
attrs = %{
qso_id: contact.id,
contact_id: contact.id,
sample_count: 65,
path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}],
verdict: "BLOCKED",
@ -68,11 +68,11 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
assert changeset.valid?
end
test "enforces unique qso_id constraint on insert" do
test "enforces unique contact_id constraint on insert" do
contact = create_contact()
attrs = %{
qso_id: contact.id,
contact_id: contact.id,
sample_count: 65,
path_points: [],
verdict: "CLEAR"
@ -88,7 +88,7 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
|> TerrainProfile.changeset(attrs)
|> Repo.insert()
assert errors_on(changeset)[:qso_id]
assert errors_on(changeset)[:contact_id]
end
end
end

View file

@ -25,7 +25,7 @@ defmodule Microwaveprop.TerrainTest do
defp terrain_attrs(contact) do
%{
qso_id: contact.id,
contact_id: contact.id,
sample_count: 65,
path_points: [
%{"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)
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.verdict == "CLEAR"
end
test "upserts on conflict (same qso_id)" do
test "upserts on conflict (same contact_id)" do
contact = create_contact()
attrs = terrain_attrs(contact)
@ -71,7 +71,7 @@ defmodule Microwaveprop.TerrainTest do
{:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(contact))
profile = Terrain.get_terrain_profile(contact.id)
assert profile.qso_id == contact.id
assert profile.contact_id == contact.id
end
test "returns nil when no profile exists" do

View file

@ -370,10 +370,10 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
assert length(jobs) == 1
job = hd(jobs)
assert job.changes.args["qso_id"] == contact.id
assert job.changes.args["contact_id"] == contact.id
end
test "deduplicates by qso_id" do
test "deduplicates by contact_id" do
contact = create_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)
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 Terrain.has_terrain_profile?(contact.id)
@ -58,13 +58,13 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
{:ok, _} =
Terrain.upsert_terrain_profile(%{
qso_id: contact.id,
contact_id: contact.id,
sample_count: 65,
path_points: [],
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})
end
@ -75,7 +75,7 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
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})
refute Terrain.has_terrain_profile?(contact.id)
@ -85,7 +85,7 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
stub_elevation_api()
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})
profile = Terrain.get_terrain_profile(contact.id)