I have one table which store information about school vacation periods and an other one which stores information about locations which are recursively nested. Think schools → cities → counties → federal_states → countries (each of them having their own vacation periods).
defmodule Feriendaten.Calendars.Period do
use Ecto.Schema
import Ecto.Changeset
alias Feriendaten.Geography.Location
schema "periods" do
field :ends_on, :date
field :holiday_or_vacation_type_id, :integer
field :memo, :string
field :starts_on, :date
belongs_to :location, Location
timestamps()
end
end
defmodule Feriendaten.Geography.Location do
use Ecto.Schema
import Ecto.Changeset
alias Feriendaten.EctoSlug.LocationNameSlug
alias Feriendaten.Calendars.Period
schema "locations" do
field :name, :string
field :parent_id, :integer
field :slug, LocationNameSlug.Type
has_many :periods, Period, on_delete: :delete_all
timestamps()
end
end
Because of better performance I use this SQL code to query:
SELECT
periods.id,
periods.starts_on,
periods.ends_on,
holiday_or_vacation_types.name,
holiday_or_vacation_types.colloquial,
periods.ends_on - periods.starts_on + 1 AS days,
holiday_or_vacation_types.slug
FROM
periods
INNER JOIN holiday_or_vacation_types ON periods.holiday_or_vacation_type_id = holiday_or_vacation_types.id
WHERE
periods.ends_on >= '2022-09-20'
AND periods.starts_on <= '2023-12-31'
AND holiday_or_vacation_types.school_vacation = TRUE
AND location_id IN(WITH RECURSIVE cte_location AS (
SELECT
id,
parent_id FROM locations
WHERE
id = 325
UNION ALL
SELECT
e.id,
e.parent_id FROM locations e
INNER JOIN cte_location o ON o.parent_id = e.id
)
SELECT
id FROM cte_location)
ORDER BY
periods.starts_on;
Which results in this:
To make this work within my Phoenix application I use this function:
def school_vacation_periods(%Location{} = location, starts_on, ends_on) do
query =
"SELECT periods.id,periods.starts_on,periods.ends_on,holiday_or_vacation_types.name,holiday_or_vacation_types.colloquial,periods.ends_on-periods.starts_on+1 AS days,holiday_or_vacation_types.slug FROM periods INNER JOIN holiday_or_vacation_types ON periods.holiday_or_vacation_type_id=holiday_or_vacation_types.id WHERE periods.ends_on>='" <>
Date.to_string(starts_on) <>
"' AND periods.starts_on<='" <>
Date.to_string(ends_on) <>
"' AND holiday_or_vacation_types.school_vacation=TRUE AND location_id IN(WITH RECURSIVE cte_location AS(SELECT id,parent_id FROM locations WHERE id=" <>
Integer.to_string(location.id) <>
" UNION ALL SELECT e.id,e.parent_id FROM locations e INNER JOIN cte_location o ON o.parent_id=e.id)SELECT id FROM cte_location)ORDER BY periods.starts_on;
"
result = Ecto.Adapters.SQL.query!(Repo, query, [])
result.rows
end
And the result is a list of lists:
[
[1115, ~D[2022-10-17], ~D[2022-10-31], "Herbst", "Herbstferien", 15, "herbst"],
[885, ~D[2022-12-23], ~D[2023-01-02], "Weihnachten", "Weihnachtsferien", 11,
"weihnachten"],
[944, ~D[2023-04-03], ~D[2023-04-06], "Ostern/Frühjahr", "Osterferien", 4,
"ostern-fruehjahr"],
[1002, ~D[2023-05-30], ~D[2023-06-07], "Himmelfahrt/Pfingsten",
"Pfingstferien", 9, "himmelfahrt-pfingsten"],
[1059, ~D[2023-07-24], ~D[2023-09-01], "Sommer", "Sommerferien", 40, "sommer"],
[1390, ~D[2023-10-16], ~D[2023-10-27], "Herbst", "Herbstferien", 12, "herbst"],
[1413, ~D[2023-12-27], ~D[2024-01-05], "Weihnachten", "Weihnachtsferien", 10,
"weihnachten"]
]
Needless to say that it is ugly and hard to maintain code. It works but it is hard to look at and work with. I don’t get a struct which I can use within a HEEx template the easy way.
Is there a better approach for this problem? A way to get cleaner code which is better to maintain? And a result which contains a list of structs or some sort of schema.