-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add views to aggregate daily allocations and usage by area #100
Merged
Merged
Changes from 2 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
7501dd5
Add views to aggregate consent allocations and usage by day
vimto 1e1ba58
Start writing tests
vimto 9b5f81b
Allow easier setup and assertion against test data, start adding more…
vimto 5d105d0
Add tests for various allocation scenarios
vimto 79c673b
Add failing test for aggregating data across different areas
vimto 8628302
Simplify views
vimto fa1b741
Splitting out the calculation of daily useage by area
smozely 6608714
Add more assertions for aggregation of different areas
vimto 947053b
water_allocation.meter should correlate with observation_site.name
vimto a98dbbd
Format code
vimto a5e9c88
Feedback from PR review
vimto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
73 changes: 73 additions & 0 deletions
73
packages/Manager/src/main/resources/db/migration/V0032__effective_daily_consents.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
create or replace view effective_daily_consents as | ||
|
||
with all_consents as ( | ||
select distinct source_id, | ||
null::varchar as area_id, | ||
null::varchar as status, | ||
null::numeric as allocation, | ||
null::boolean as is_metered, | ||
null::numeric as metered_allocation_daily, | ||
null::numeric as metered_allocation_yearly, | ||
null::varchar[] as meters, | ||
0 as is_real | ||
from water_allocations | ||
), | ||
|
||
days_in_last_year as ( | ||
select GENERATE_SERIES(DATE_TRUNC('day', now()) - INTERVAL '1 YEAR', DATE_TRUNC('day', now()), INTERVAL '1 DAY') as effective_from | ||
), | ||
|
||
data_per_day as ( | ||
select * from all_consents cross join days_in_last_year | ||
union | ||
SELECT source_id, | ||
area_id, | ||
status, | ||
allocation, | ||
is_metered, | ||
metered_allocation_daily, | ||
metered_allocation_yearly, | ||
meters, | ||
1 as is_real, | ||
effective_from | ||
from water_allocations | ||
), | ||
|
||
latest_values as ( | ||
select source_id, effective_from, area_id, status, allocation, is_metered, metered_allocation_daily, metered_allocation_yearly, meters, is_real, | ||
sum (case when area_id is not null then 1 end) over (partition by source_id order by effective_from, is_real) as is_area_id_null, | ||
sum (case when status is not null then 1 end) over (partition by source_id order by effective_from, is_real) as is_status_null, | ||
sum (case when allocation is not null then 1 end) over (partition by source_id order by effective_from, is_real) as is_allocation_null, | ||
sum (case when is_metered is not null then 1 end) over (partition by source_id order by effective_from, is_real) as is_is_metered_null, | ||
sum (case when metered_allocation_daily is not null then 1 end) over (partition by source_id order by effective_from, is_real) as is_mad_null, | ||
sum (case when metered_allocation_yearly is not null then 1 end) over (partition by source_id order by effective_from, is_real) as is_may_null, | ||
sum (case when meters is not null then 1 end) over (partition by source_id order by effective_from, is_real) as is_meters_null | ||
from data_per_day | ||
), | ||
|
||
effective_daily_data as ( | ||
select source_id, effective_from, | ||
first_value(area_id) over (partition by source_id, latest_values.is_area_id_null) as area_id, | ||
first_value(status) over (partition by source_id, latest_values.is_status_null) as status, | ||
first_value(allocation) over (partition by source_id, latest_values.is_allocation_null) as allocation, | ||
first_value(is_metered) over (partition by source_id, latest_values.is_is_metered_null) as is_metered, | ||
first_value(metered_allocation_daily) over (partition by source_id, latest_values.is_mad_null) as allocation_daily, | ||
first_value(metered_allocation_yearly) over (partition by source_id, latest_values.is_may_null) as metered_allocation_yearly, | ||
first_value(meters) over (partition by source_id, latest_values.is_meters_null) as meters, | ||
ROW_NUMBER() over(order by source_id, effective_from, is_real) as row_number | ||
from latest_values), | ||
|
||
grouped_effective_data as ( | ||
select ea.* from effective_daily_data ea | ||
inner join | ||
( | ||
select source_id, date(effective_from) as effective_from, max(row_number) as max_row_number | ||
from effective_daily_data | ||
group by source_id, date(effective_from) | ||
) lef | ||
on lef.source_id = ea.source_id | ||
and lef.max_row_number = ea.row_number | ||
order by source_id, effective_from | ||
) | ||
|
||
select * from grouped_effective_data |
31 changes: 31 additions & 0 deletions
31
...ges/Manager/src/main/resources/db/migration/V0033__water_allocation_and_usage_by_area.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
create or replace view water_allocation_and_usage_by_area as | ||
with all_areas as | ||
( | ||
select distinct area_id | ||
from effective_daily_consents | ||
where area_id is not null | ||
), | ||
all_dates as ( | ||
select distinct date(effective_from) as date | ||
from effective_daily_consents | ||
), | ||
area_defaults as ( | ||
select area_id, date, 0 as allocation, 0 as allocation_daily, 0 as metered_allocation_yearly, 0 as daily_usage | ||
from all_areas cross join all_dates | ||
), | ||
usage as ( | ||
select area_id, date(effective_from) as date, allocation, allocation_daily, metered_allocation_yearly, case when is_metered = true then daily_usage else 0 end as daily_usage | ||
from effective_daily_consents | ||
inner join observed_water_use_aggregated_daily | ||
on day_observed_at = date(effective_daily_consents.effective_from) | ||
and site_id::varchar in( | ||
select unnest(meters) | ||
) | ||
where status = 'active' | ||
union | ||
select * from area_defaults | ||
) | ||
|
||
select area_id, date, sum(allocation) as allocation, sum(allocation_daily) as allocation_daily, sum(metered_allocation_yearly) as metered_allocation_yearly, sum(daily_usage) as daily_usage | ||
from usage | ||
group by area_id, date |
90 changes: 90 additions & 0 deletions
90
packages/Manager/src/test/kotlin/nz/govt/eop/plan_limits/WaterAllocationAndUsageViewsTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package nz.govt.eop.plan_limits | ||
|
||
import io.kotest.matchers.collections.shouldContain | ||
import io.kotest.matchers.shouldBe | ||
import java.time.LocalDate | ||
import org.junit.jupiter.api.BeforeEach | ||
import org.junit.jupiter.api.Test | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.boot.test.context.SpringBootTest | ||
import org.springframework.jdbc.core.JdbcTemplate | ||
import org.springframework.test.context.ActiveProfiles | ||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* | ||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* | ||
import org.springframework.transaction.annotation.Transactional | ||
|
||
@ActiveProfiles("test") | ||
@SpringBootTest | ||
@Transactional | ||
class WaterAllocationAndUsageViewsTest(@Autowired val jdbcTemplate: JdbcTemplate) { | ||
|
||
@BeforeEach | ||
fun resetTestData() { | ||
truncateTestData() | ||
createTestData() | ||
} | ||
|
||
@Test | ||
fun `should be empty with no data`() { | ||
truncateTestData() | ||
val result = | ||
jdbcTemplate.queryForObject( | ||
"select count(*) from water_allocation_and_usage_by_area", Int::class.java) | ||
result shouldBe 0 | ||
} | ||
|
||
@Test | ||
fun `should include a year of data`() { | ||
val result = | ||
jdbcTemplate.queryForMap( | ||
"select count(*) as count, min(date) as min_date, max(date) as max_date from water_allocation_and_usage_by_area") | ||
arrayOf(365L, 366L) shouldContain result["count"] as Long | ||
val min = LocalDate.parse(result["min_date"].toString()) | ||
val max = LocalDate.parse(result["max_date"].toString()) | ||
min.plusYears(1) shouldBe max | ||
} | ||
|
||
// Pending tests | ||
// Single consent | ||
fun `should not include data before a consent was effective`() {} | ||
fun `should data once a consent is active`() {} | ||
fun `should handle changes in consent data`() {} | ||
fun `should not include data when a consent status is not active`() {} | ||
fun `should not include observations when is_metered is false`() {} | ||
|
||
// Multiple consents | ||
// Multiple meters | ||
|
||
fun truncateTestData() { | ||
jdbcTemplate.execute("truncate water_allocations cascade") | ||
jdbcTemplate.execute("truncate observations cascade") | ||
jdbcTemplate.execute("truncate observation_sites_measurements cascade") | ||
jdbcTemplate.execute("truncate observation_sites cascade") | ||
} | ||
|
||
fun createTestData() { | ||
val siteId = 1 | ||
val councilId = 9 | ||
val measurementId = 1 | ||
val measurementName = "Water Meter Reading" | ||
val observationTimestamp = "2023-09-10 06:00:00+00" | ||
val consentAreaId = "area-id" | ||
val consentSourceId = "source-id" | ||
val consentAllocation = 10 | ||
val consentEffectiveFrom = "2023-09-01 06:00:00+00" | ||
jdbcTemplate.update( | ||
"""INSERT INTO observation_sites (id, council_id, name) VALUES ($siteId, $councilId, 'Test site')""") | ||
jdbcTemplate.update( | ||
""" | ||
INSERT INTO observation_sites_measurements (id, site_id, measurement_name, first_observation_at, last_observation_at, observation_count) | ||
VALUES ($measurementId, $siteId, '$measurementName', now(), now(), 0)""" | ||
.trimIndent()) | ||
jdbcTemplate.update( | ||
"""INSERT INTO observations (observation_measurement_id, amount, observed_at) VALUES ($measurementId, 1, '$observationTimestamp')""") | ||
jdbcTemplate.update( | ||
""" | ||
INSERT INTO water_allocations (area_id, allocation, ingest_id, source_id, consent_id, status, is_metered, metered_allocation_daily, metered_allocation_yearly, meters, effective_from, effective_to, created_at, updated_at) | ||
VALUES ('$consentAreaId', '$consentAllocation', 'ingest-id', '$consentSourceId', 'consent-id', 'active', true, 10, 100, '{$siteId}', '$consentEffectiveFrom', null, now(), now())""" | ||
.trimIndent()) | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smozely I've started writing some tests for the view in this style. Feels like they could quickly get verbose without some helpers for generating sets of test data. This test data needs to be dynamic, since the new views generate data relative to now.
Any thoughts on things in the Java/Kotlin/Spring ecosystem to help with this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ... not sure I have any great answers
We had the same kinda thing when trying to test complex views at ECAN, we had written a small JS layer to do similar to this style of testing, we did struggle with how to express in the code what the test data means as it was difficult to jump from "these rows here, mean this output in the view" ... not sure any suggestion here really helps with that.
via DBT we did have an a way of testing constituent views in stages, well actually it was less than perfect in that we could only test the views in stages, not the whole chain together.
Keeping it kinda how you've got there Kotlin lets you add parameters with defaults, and you can pass named parameters. Which would kinda give you the equivalent of passing an object in JS and then it being merged with a default object.
Which can then be called like
I think that is a good way to keep the same core test data setup but let you customize it per test
And then for the dates stuff, using the date
LocalDate.now().atStartOfDay().minusDays(10)
to make everything relative to now ... its kinda ugly because its hard to keep that date mangling noise down.Maybe there could be the equivalent of
createTestData
something likecreateExpectedResult
that would help encapsulate the logic in code, in a way that you can then useSELECT * FROM water_allocation_and_usage_by_area
and compare directly to that? ... it might mean each test is doing more work, but keeps some consistency of how they flowSome of this would be a bit easier if we had objects that were being returned from the view queries, just because it would be easier to create those than the
Map
rows to assert against. But that might be overkill if they are only going to be used for tests.