Skip to content

Commit 13a1e67

Browse files
Refactor meter score tables to match leaks/irrigation pattern (#219)
Replace CREATE OR REPLACE with CREATE TABLE IF NOT EXISTS + DELETE + INSERT for idempotency, and add a new meters_score_{org_id}_agg table grouped by class_full_name with array_agg of per-device scores and avg/stddev summaries. Co-authored-by: Vicente Ferreyra <36196252+vicenteferreyra@users.noreply.github.com>
1 parent 75824e3 commit 13a1e67

2 files changed

Lines changed: 128 additions & 7 deletions

File tree

amiadapters/storage/snowflake.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,49 @@ def exec_postprocessor(self, run_id: str, min_date: datetime, max_date: datetime
360360

361361
conn = self.sink_config.connection()
362362

363+
# Ensure meters_score table exists (no-op after first run)
364+
conn.cursor().execute(f"""
365+
create table if not exists meters_score_{self.org_id} (
366+
org_id text,
367+
device_id text,
368+
class_full_name text,
369+
first_read timestamp_ntz,
370+
last_read timestamp_ntz,
371+
last_read_hours float,
372+
observations number(18,0),
373+
hours_in_period float,
374+
read_freq float,
375+
unread_freq float,
376+
stuck_events number(18,0),
377+
stuck_hours float,
378+
duration_hours_thr float,
379+
total_usage float,
380+
used_hours float,
381+
idle_hours float,
382+
avg_hourly_usage float,
383+
unaccounted float,
384+
z_last_read float,
385+
z_unread_freq float,
386+
z_stuck_hours float,
387+
z_stuck_events float,
388+
z_unaccounted float,
389+
score_last_read float,
390+
score_unread_freq float,
391+
score_stuck_hours float,
392+
score_stuck_events float,
393+
score_unaccounted float,
394+
score float
395+
)
396+
""")
397+
398+
# Delete existing rows for this org, then insert fresh results
399+
conn.cursor().execute(f"""
400+
delete from meters_score_{self.org_id}
401+
where org_id = '{self.org_id}'
402+
""")
403+
363404
ami_meters_score_sql = f"""
364-
create or replace table meters_score_{self.org_id}
365-
as
405+
insert into meters_score_{self.org_id}
366406
with cte as
367407
(
368408
select org_id
@@ -465,6 +505,31 @@ def exec_postprocessor(self, run_id: str, min_date: datetime, max_date: datetime
465505
"""
466506
conn.cursor().execute(ami_meters_score_sql)
467507

508+
ami_meters_score_agg_sql = f"""
509+
create or replace table meters_score_{self.org_id}_agg
510+
as
511+
select
512+
org_id
513+
, class_full_name
514+
, array_agg(device_id) as device_ids
515+
, array_agg(score) as scores
516+
, array_agg(score_last_read) as scores_last_read
517+
, array_agg(score_unread_freq) as scores_unread_freq
518+
, array_agg(score_stuck_hours) as scores_stuck_hours
519+
, array_agg(score_stuck_events) as scores_stuck_events
520+
, array_agg(score_unaccounted) as scores_unaccounted
521+
, avg(score) as avg_score
522+
, stddev(score) as stddev_score
523+
, avg(score_last_read) as avg_score_last_read
524+
, avg(score_unread_freq) as avg_score_unread_freq
525+
, avg(score_stuck_hours) as avg_score_stuck_hours
526+
, avg(score_stuck_events) as avg_score_stuck_events
527+
, avg(score_unaccounted) as avg_score_unaccounted
528+
from meters_score_{self.org_id}
529+
group by org_id, class_full_name
530+
"""
531+
conn.cursor().execute(ami_meters_score_agg_sql)
532+
468533
# Ensure leaks table exists (no-op after first run)
469534
conn.cursor().execute(f"""
470535
create table if not exists leaks_{self.org_id} (

test/amiadapters/storage/test_snowflake.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -269,11 +269,67 @@ def test_exec_postprocessor__creates_irrigation_tables_per_org(self):
269269
all_sqls = [call[0][0] for call in self.mock_cursor.execute.call_args_list]
270270
normalized = [self.normalize_sql(sql) for sql in all_sqls]
271271

272-
# Expects exactly 9 execute calls:
273-
# 1 meters_score, 1 leaks CREATE, 1 leaks DELETE, 1 leaks INSERT,
274-
# 1 leaks_agg, 1 irrigation CREATE, 1 irrigation DELETE,
275-
# 1 irrigation INSERT, 1 irrigation_agg
276-
self.assertEqual(9, self.mock_cursor.execute.call_count)
272+
# Expects exactly 12 execute calls:
273+
# 1 meters_score CREATE, 1 meters_score DELETE, 1 meters_score INSERT,
274+
# 1 meters_score_agg,
275+
# 1 leaks CREATE, 1 leaks DELETE, 1 leaks INSERT, 1 leaks_agg,
276+
# 1 irrigation CREATE, 1 irrigation DELETE, 1 irrigation INSERT, 1 irrigation_agg
277+
self.assertEqual(12, self.mock_cursor.execute.call_count)
278+
279+
# --- meters_score detail table ---
280+
meters_score_create = next(
281+
(
282+
s
283+
for s in normalized
284+
if "create table if not exists meters_score_org-id" in s.lower()
285+
),
286+
None,
287+
)
288+
self.assertIsNotNone(
289+
meters_score_create, "Should create meters_score_{org_id} table"
290+
)
291+
self.assertIn("class_full_name", meters_score_create)
292+
self.assertIn("score", meters_score_create)
293+
self.assertIn("stuck_hours", meters_score_create)
294+
295+
# --- DELETE by org_id ---
296+
meters_score_delete = next(
297+
(s for s in normalized if "delete from meters_score_org-id" in s.lower()),
298+
None,
299+
)
300+
self.assertIsNotNone(
301+
meters_score_delete, "Should DELETE from meters_score_{org_id}"
302+
)
303+
self.assertIn("org_id = 'org-id'", meters_score_delete)
304+
305+
# --- INSERT from readings ---
306+
meters_score_insert = next(
307+
(s for s in normalized if "insert into meters_score_org-id" in s.lower()),
308+
None,
309+
)
310+
self.assertIsNotNone(
311+
meters_score_insert, "Should INSERT into meters_score_{org_id}"
312+
)
313+
self.assertIn("ami_connect.readings", meters_score_insert)
314+
self.assertIn("z_scores", meters_score_insert)
315+
self.assertIn("2025-01-01", meters_score_insert)
316+
self.assertIn("2025-01-31", meters_score_insert)
317+
318+
# --- agg table ---
319+
meters_score_agg = next(
320+
(
321+
s
322+
for s in normalized
323+
if "create or replace table meters_score_org-id_agg" in s.lower()
324+
),
325+
None,
326+
)
327+
self.assertIsNotNone(
328+
meters_score_agg, "Should create meters_score_{org_id}_agg table"
329+
)
330+
self.assertIn("array_agg", meters_score_agg)
331+
self.assertIn("avg_score", meters_score_agg)
332+
self.assertIn("class_full_name", meters_score_agg)
277333

278334
# --- irrigation detail table ---
279335
irrigation_create = next(

0 commit comments

Comments
 (0)