Skip to content

Commit 19221ad

Browse files
authored
BFD-4349: Prune older shared system phase 1 claims (#3178)
1 parent 3f363be commit 19221ad

7 files changed

Lines changed: 139 additions & 20 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE INDEX ON idr.claim_professional_ss(bfd_updated_ts) WHERE clm_type_cd BETWEEN 1000 and 1999;
2+
3+
CREATE INDEX ON idr.claim_institutional_ss(bfd_updated_ts) WHERE clm_type_cd BETWEEN 1000 and 1999;

apps/bfd-pipeline-idr/constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,18 @@
1010
CLAIM_RX_TABLE = "idr.claim_rx"
1111
CLAIM_PROFESSIONAL_NCH_TABLE = "idr.claim_professional_nch"
1212
CLAIM_PROFESSIONAL_SS_TABLE = "idr.claim_professional_ss"
13+
CLAIM_PROFESSIONAL_ITEM_SS_TABLE = "idr.claim_item_professional_ss"
1314
CLAIM_INSTITUTIONAL_NCH_TABLE = "idr.claim_institutional_nch"
1415
CLAIM_INSTITUTIONAL_SS_TABLE = "idr.claim_institutional_ss"
16+
CLAIM_INSTITUTIONAL_ITEM_SS_TABLE = "idr.claim_item_institutional_ss"
17+
18+
# Phase 1 claims younger than 60 days are allowed
19+
PHASE_1_CUTOFF = 60
20+
PHASE_1_SS_MIN = 1000
21+
PHASE_1_SS_MAX = 1999
22+
FISS_CLM_SOURCE = "21000"
23+
MCS_CLM_SOURCE = "22000"
24+
VMS_CLM_SOURCE = "23000"
1525

1626
IDR_PREFIX = "cms_vdm_view_mdcr_prd"
1727
IDR_BENE_HISTORY_TABLE = f"{IDR_PREFIX}.v2_mdcr_bene_hstry"

apps/bfd-pipeline-idr/model/base_model.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
DEFAULT_MAX_DATE,
2020
DEFAULT_MIN_DATE,
2121
EMPTY_PARTITION,
22+
FISS_CLM_SOURCE,
2223
IDR_BENE_HISTORY_TABLE,
2324
IDR_CLAIM_ANSI_SIGNATURE_TABLE,
2425
IDR_CLAIM_DATE_SIGNATURE_TABLE,
@@ -29,11 +30,16 @@
2930
IDR_PRIOR_AUTH_TABLE,
3031
INSTITUTIONAL_NCH_PARTITIONS,
3132
INSTITUTIONAL_SS_PARTITIONS,
33+
MCS_CLM_SOURCE,
3234
NON_CLAIM_PARTITION,
3335
PART_D_CLAIM_TYPE_CODES,
3436
PART_D_PARTITIONS,
37+
PHASE_1_CUTOFF,
38+
PHASE_1_SS_MAX,
39+
PHASE_1_SS_MIN,
3540
PROFESSIONAL_NCH_PARTITIONS,
3641
PROFESSIONAL_SS_PARTITIONS,
42+
VMS_CLM_SOURCE,
3743
)
3844
from load_partition import LoadPartition, LoadPartitionGroup, PartitionType
3945
from settings import (
@@ -42,6 +48,7 @@
4248
MIN_CLAIM_NCH_TRANSACTION_DATE,
4349
MIN_CLAIM_SS_TRANSACTION_DATE,
4450
MIN_PRIOR_AUTH_TRANSACTION_DATE,
51+
PHASE_1_PRUNE_BATCH_LIMIT,
4552
)
4653

4754
type DbType = str | float | int | bool | date | datetime
@@ -343,9 +350,6 @@ def base_claim_filter(partition: LoadPartition) -> str:
343350
EXPR = "expr"
344351
DERIVED = "derived"
345352
COLUMN_MAP = "column_map"
346-
FISS_CLM_SOURCE = "21000"
347-
MCS_CLM_SOURCE = "22000"
348-
VMS_CLM_SOURCE = "23000"
349353

350354

351355
ALIAS_CLM = "clm"
@@ -692,17 +696,16 @@ def claim_filter(start_time: datetime, partition: LoadPartition) -> str:
692696
latest_claim_ind = ""
693697

694698
# PAC data older than 60 days should be filtered
695-
pac_cutoff_date = start_time - timedelta(days=60)
699+
pac_cutoff_date = start_time - timedelta(days=PHASE_1_CUTOFF)
696700
start_time_sql = pac_cutoff_date.strftime("'%Y-%m-%d %H:%M:%S'")
697-
pac_phase_1_min = 1000
698-
pac_phase_1_max = 1999
701+
699702
# Note: checking clm_type_cd as the first branch of the OR here might be more efficient
700703
# Since it's more likely to return true
701704
pac_filter = (
702705
f"""
703706
AND
704707
(
705-
{clm}.clm_type_cd NOT BETWEEN {pac_phase_1_min} AND {pac_phase_1_max}
708+
{clm}.clm_type_cd NOT BETWEEN {PHASE_1_SS_MIN} AND {PHASE_1_SS_MAX}
706709
OR
707710
(
708711
{clm}.clm_src_id IN (
@@ -825,3 +828,26 @@ def claim_related_conditions_cte(source: Source) -> str:
825828
AND clm_rlt_cond_cd != '~'
826829
GROUP BY clm_rlt_cond_sgntr_sk
827830
"""
831+
832+
833+
def stale_phase_1_claims_query(
834+
header_table: str,
835+
item_table: str,
836+
cutoff_date: datetime,
837+
) -> tuple[str, tuple[datetime, datetime]]:
838+
return (
839+
f"""
840+
SELECT clm.clm_uniq_id
841+
FROM {header_table} clm
842+
WHERE clm.clm_type_cd BETWEEN {PHASE_1_SS_MIN} AND {PHASE_1_SS_MAX}
843+
AND clm.clm_src_id IN ('{FISS_CLM_SOURCE}', '{MCS_CLM_SOURCE}', '{VMS_CLM_SOURCE}')
844+
AND clm.bfd_updated_ts < %s
845+
AND NOT EXISTS (
846+
SELECT 1 FROM {item_table} item
847+
WHERE clm.clm_uniq_id = item.clm_uniq_id
848+
AND item.bfd_updated_ts >= %s
849+
)
850+
LIMIT {PHASE_1_PRUNE_BATCH_LIMIT}
851+
""",
852+
(cutoff_date, cutoff_date),
853+
)

apps/bfd-pipeline-idr/pipeline_stages.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from model.idr_contract_pbp_number import IdrContractPbpNumber
3636
from model.idr_prior_auth import IdrPriorAuth
3737
from parallel_executor import ParallelStagesExecutor, Stage
38-
from pipeline_utils import extract_and_load, prune_bene_lis_cmbnd
38+
from pipeline_utils import extract_and_load, prune_bene_lis_cmbnd, prune_phase_1_ss_claims
3939
from settings import enable_prior_auth_ingestion
4040

4141
type NodePartitionedModelInput = tuple[type[IdrBaseModel], LoadPartition | None]
@@ -47,6 +47,10 @@
4747
IdrClaimProfessionalSs,
4848
IdrClaimInstitutionalSs,
4949
]
50+
CLAIM_SS_TABLES: list[type[IdrBaseModel]] = [
51+
IdrClaimProfessionalSs,
52+
IdrClaimInstitutionalSs,
53+
]
5054
CLAIM_AUX_TABLES: list[type[IdrBaseModel]] = [
5155
# RX/Part D is special because we combine claim + claim line
5256
IdrClaimRx,
@@ -147,6 +151,14 @@ def _stage5_do_phase_1_prune(self) -> Stage[bool]:
147151
self.load_mode,
148152
)
149153

154+
for model in self._filter_tables(CLAIM_SS_TABLES):
155+
yield functools.partial(
156+
prune_phase_1_ss_claims,
157+
model,
158+
self.load_mode,
159+
self.start_time,
160+
)
161+
150162
def _filter_tables(self, tables: list[type[IdrBaseModel]]) -> list[type[IdrBaseModel]]:
151163
return [
152164
t

apps/bfd-pipeline-idr/pipeline_utils.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,22 @@
88
from snowflake.connector.network import ReauthenticationRequest, RetryRequest
99

1010
from batch_worker import LoadingBatchWorkerClient
11-
from constants import DEFAULT_MAX_DATE, DEFAULT_PARTITION
11+
from constants import (
12+
CLAIM_INSTITUTIONAL_ITEM_SS_TABLE,
13+
CLAIM_INSTITUTIONAL_SS_TABLE,
14+
CLAIM_PROFESSIONAL_ITEM_SS_TABLE,
15+
CLAIM_PROFESSIONAL_SS_TABLE,
16+
DEFAULT_MAX_DATE,
17+
DEFAULT_PARTITION,
18+
PHASE_1_CUTOFF,
19+
)
1220
from extractor import PostgresExtractor, SnowflakeExtractor, Source
1321
from load_partition import LoadPartition
1422
from loader import LoadType, PostgresLoader, get_connection_string, should_track_load_progress
1523
from model.base_model import (
1624
LoadMode,
1725
T,
26+
stale_phase_1_claims_query,
1827
)
1928
from model.idr_beneficiary_low_income_subsidy_cmbnd import IdrBeneficiaryLowIncomeSubsidyCmbnd
2029
from model.load_progress import LoadProgress
@@ -115,6 +124,45 @@ def extract_and_load(
115124
raise ex
116125

117126

127+
def prune_phase_1_ss_claims(
128+
cls: type[T],
129+
load_mode: LoadMode,
130+
job_start: datetime,
131+
) -> bool:
132+
shared_claim_tables = {
133+
CLAIM_INSTITUTIONAL_SS_TABLE: CLAIM_INSTITUTIONAL_ITEM_SS_TABLE,
134+
CLAIM_PROFESSIONAL_SS_TABLE: CLAIM_PROFESSIONAL_ITEM_SS_TABLE,
135+
}
136+
137+
claim_table = cls.table()
138+
item_table = shared_claim_tables.get(claim_table)
139+
if item_table is None:
140+
return True
141+
142+
prune_cutoff_date = job_start - timedelta(days=PHASE_1_CUTOFF)
143+
logger.info("pruning phase 1 ss claims older than {}", prune_cutoff_date)
144+
145+
prune_query, params = stale_phase_1_claims_query(claim_table, item_table, prune_cutoff_date)
146+
147+
with psycopg.connect(get_connection_string(load_mode)) as conn:
148+
for target_table in [item_table, claim_table]:
149+
total_row_count = 0
150+
while True:
151+
with conn.transaction():
152+
res = conn.execute(
153+
f"""DELETE FROM {target_table} WHERE clm_uniq_id IN ({prune_query})""", # type: ignore
154+
params,
155+
)
156+
157+
total_row_count += res.rowcount
158+
logger.info("pruned {} rows from {}", res.rowcount, item_table)
159+
160+
if res.rowcount == 0:
161+
logger.info("Total rows pruned from {}: {}", item_table, total_row_count)
162+
break
163+
return True
164+
165+
118166
def prune_bene_lis_cmbnd(
119167
load_mode: LoadMode,
120168
) -> bool:

apps/bfd-pipeline-idr/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ def enable_prior_auth_ingestion() -> bool:
100100
"""Number of minimum connections to hold in the pool concurrently per-batch for non-LOCAL loads.
101101
Defaults to 20."""
102102

103+
PHASE_1_PRUNE_BATCH_LIMIT = int(getenv("PHASE_1_PRUNE_BATCH_LIMIT", "10_000"))
104+
"""The maximum batch size for pruning old claims (phase 1 claims from shared systems) on
105+
INCREMENTAL loads. Defaults to 10000."""
106+
103107
BENEFICIARY_PRUNE_BATCH_LIMIT = int(getenv("IDR_BENEFICIARY_PRUNE_BATCH_LIMIT", "1000"))
104108
"""Maximum rows to delete per prune statement for LIS combined beneficiary records."""
105109

apps/bfd-pipeline-idr/test_pipeline.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import shutil
33
import subprocess
44
from collections.abc import Generator
5-
from datetime import datetime, timedelta
5+
from datetime import UTC, datetime, timedelta
66
from pathlib import Path
77
from typing import cast
88
from uuid import uuid4
@@ -198,11 +198,6 @@ def _do_test_pipeline(conn: Connection[DictRow], load_type: LoadType) -> None:
198198
rows = cur.fetchmany(1)
199199
assert rows[0]["clm_uniq_id"] == 113370100080
200200

201-
cur = conn.execute("select * from idr.claim_institutional_ss order by clm_uniq_id")
202-
assert cur.rowcount == 21
203-
rows = cur.fetchmany(1)
204-
assert rows[0]["clm_uniq_id"] == 123359318723
205-
206201
cur = conn.execute("select * from idr.claim_professional_nch order by clm_uniq_id")
207202
assert cur.rowcount == 51
208203
rows = cur.fetchmany(1)
@@ -223,11 +218,6 @@ def _do_test_pipeline(conn: Connection[DictRow], load_type: LoadType) -> None:
223218
rows = cur.fetchmany(1)
224219
assert rows[0]["clm_uniq_id"] == 113370100080
225220

226-
cur = conn.execute("select * from idr.claim_item_institutional_ss order by clm_uniq_id")
227-
assert cur.rowcount == 327
228-
rows = cur.fetchmany(1)
229-
assert rows[0]["clm_uniq_id"] == 123359318723
230-
231221
cur = conn.execute("select * from idr.claim_item_professional_nch order by clm_uniq_id")
232222
assert cur.rowcount == 442
233223
rows = cur.fetchmany(1)
@@ -240,6 +230,32 @@ def _do_test_pipeline(conn: Connection[DictRow], load_type: LoadType) -> None:
240230

241231
conn.commit()
242232

233+
# Phase 1 SS (PAC) claims older than 60 days will be pruned on incremental loads
234+
if load_type == LoadType.INITIAL:
235+
cur = conn.execute("select * from idr.claim_institutional_ss order by clm_uniq_id")
236+
assert cur.rowcount == 21
237+
rows = cur.fetchmany(1)
238+
assert rows[0]["clm_uniq_id"] == 123359318723
239+
240+
cur = conn.execute("select * from idr.claim_item_institutional_ss order by clm_uniq_id")
241+
assert cur.rowcount == 327
242+
rows = cur.fetchmany(1)
243+
assert rows[0]["clm_uniq_id"] == 123359318723
244+
245+
else:
246+
make_it_stale_ts = datetime.now(UTC) + timedelta(days=60)
247+
_advance_time(make_it_stale_ts)
248+
run(Source.POSTGRES, LoadMode.SYNTHETIC, LoadType.INCREMENTAL)
249+
cur = conn.execute("select * from idr.claim_institutional_ss order by clm_uniq_id")
250+
assert cur.rowcount == 9
251+
rows = cur.fetchmany(1)
252+
assert rows[0]["clm_uniq_id"] == 849348853948
253+
254+
cur = conn.execute("select * from idr.claim_item_institutional_ss order by clm_uniq_id")
255+
assert cur.rowcount == 151
256+
rows = cur.fetchmany(1)
257+
assert rows[0]["clm_uniq_id"] == 849348853948
258+
243259
# Test incremental loading logic involving 'source_load_events' if we're testing incremental
244260
# mode
245261
if load_type == LoadType.INCREMENTAL:

0 commit comments

Comments
 (0)