Skip to content

Commit 1b67643

Browse files
committed
Add Hx tracking to climo tables, adjust functions as needed to support join tables (combined keys)
1 parent 3795a78 commit 1b67643

23 files changed

Lines changed: 5915 additions & 2814 deletions

File tree

pycds/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,14 @@
7070
"ObsCountPerDayHistory",
7171
"ObsWithFlags",
7272
"ClimatologicalPeriod",
73+
"ClimatologicalPeriodHistory",
7374
"ClimatologicalStation",
75+
"ClimatologicalStationHistory",
7476
"ClimatologicalStationXHistory",
7577
"ClimatologicalVariable",
78+
"ClimatologicalVariableHistory",
7679
"ClimatologicalValue",
80+
"ClimatologicalValueHistory",
7781
]
7882

7983
from pycds.context import get_schema_name, get_su_role_name
@@ -100,10 +104,14 @@
100104
PCICFlag,
101105
DerivedValue,
102106
ClimatologicalPeriod,
107+
ClimatologicalPeriodHistory,
103108
ClimatologicalStation,
109+
ClimatologicalStationHistory,
104110
ClimatologicalStationXHistory,
105111
ClimatologicalVariable,
112+
ClimatologicalVariableHistory,
106113
ClimatologicalValue,
114+
ClimatologicalValueHistory,
107115
)
108116

109117
from .orm.views import (

pycds/alembic/change_history_utils.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def drop_history_cols_from_primary(
6363
op.execute(f"ALTER TABLE {main_table_name(collection_name)} {drop_columns}")
6464

6565

66-
def create_history_table(collection_name: str, foreign_tables: list[tuple[str, str]]):
66+
def create_history_table(collection_name: str, foreign_tables: list[tuple[str, str]] | None):
6767
# Create the history table. We can't use Alembic create_table here because it doesn't
6868
# support the LIKE syntax we need.
6969
columns = ", ".join(
@@ -89,18 +89,23 @@ def drop_history_table(collection_name: str):
8989

9090
def create_history_table_indexes(
9191
collection_name: str,
92-
pri_id_name: str,
93-
foreign_tables: list[tuple[str, str]],
92+
pri_id_name: list[str] | str,
93+
foreign_tables: Iterable[tuple[str, str]] | None,
9494
extras=None,
9595
):
9696
"""
9797
Create indexes on the history table. For analysis on what indexes are needed,
9898
see https://github.com/pacificclimate/pycds/issues/228
9999
"""
100100

101+
if isinstance(pri_id_name, str):
102+
pri_id_name = [pri_id_name]
103+
104+
seen = []
105+
101106
for columns in (
102107
# Index on primary table primary key, mod_time, mod_user
103-
([pri_id_name], ["mod_time"], ["mod_user"])
108+
tuple([x] for x in pri_id_name) + (["mod_time"], ["mod_user"])
104109
# Index on all foreign main table primary keys
105110
+ tuple([ft_pk_name] for _, ft_pk_name in (foreign_tables or tuple()))
106111
# Index on all foreign history table primary keys
@@ -110,6 +115,9 @@ def create_history_table_indexes(
110115
)
111116
+ (extras or tuple())
112117
):
118+
if columns in seen:
119+
continue
120+
seen.append(columns)
113121
# How much do we care about index naming? SQLAlchemy uses a different pattern than
114122
# appears typical in CRMP.
115123
op.create_index(
@@ -122,8 +130,8 @@ def create_history_table_indexes(
122130

123131
def populate_history_table(
124132
collection_name: str,
125-
pri_id_name: str,
126-
foreign_tables: list[tuple[str, str]],
133+
pri_id_name: list[str] | str,
134+
foreign_tables: list[tuple[str, str]] | None,
127135
limit: int | None = None,
128136
):
129137
"""
@@ -140,7 +148,7 @@ def populate_history_table(
140148
# foreign table definitions: the CTE names, the CTE definitions, and their usages
141149
# within the query that populates the target history table.
142150

143-
foreign_tables = foreign_tables or tuple()
151+
foreign_tables = foreign_tables or []
144152

145153
conditional_comma = "," if len(foreign_tables) > 0 else ""
146154

@@ -173,6 +181,11 @@ def populate_history_table(
173181
else ""
174182
)
175183

184+
if isinstance(pri_id_name, str):
185+
pri_id_name = [pri_id_name]
186+
187+
pri_order_clause = ", ".join(f"main.{idn}" for idn in pri_id_name)
188+
176189
stmt = f"""
177190
{"WITH" if len(foreign_tables) > 0 else ""}
178191
{ft_cte_list}
@@ -183,7 +196,7 @@ def populate_history_table(
183196
FROM {main_table_name(collection_name)} main
184197
{conditional_comma} {ft_cte_name_list}
185198
{ft_where_clause}
186-
ORDER BY main.{pri_id_name}
199+
ORDER BY {pri_order_clause}
187200
"""
188201
op.execute(stmt)
189202

@@ -209,7 +222,7 @@ def create_primary_table_triggers(collection_name: str, prefix: str = "t100_"):
209222

210223

211224
def create_history_table_triggers(
212-
collection_name: str, foreign_tables: list, prefix: str = "t100_"
225+
collection_name: str, foreign_tables: list | None, prefix: str = "t100_"
213226
):
214227
# Trigger: Add foreign key values to each record inserted into history table.
215228
ft_args = (
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""apply hx tracking to multi climo normals
2+
3+
Revision ID: 7244176be9fa
4+
Revises: 758be4f4ce0f
5+
Create Date: 2025-09-23 16:15:58.236278
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
from pycds.alembic.util import grant_standard_table_privileges
13+
from pycds.context import get_schema_name
14+
from pycds.alembic.change_history_utils import (
15+
add_history_cols_to_primary,
16+
create_history_table,
17+
populate_history_table,
18+
drop_history_triggers,
19+
drop_history_table,
20+
drop_history_cols_from_primary,
21+
create_history_table_triggers,
22+
create_primary_table_triggers,
23+
create_history_table_indexes,
24+
)
25+
26+
27+
# revision identifiers, used by Alembic.
28+
revision = "7244176be9fa"
29+
down_revision = "758be4f4ce0f"
30+
branch_labels = None
31+
depends_on = None
32+
33+
34+
schema_name = get_schema_name()
35+
36+
37+
table_info = (
38+
# table_name, primary_key_name, foreign_keys, extra_indexes
39+
("climo_period", "climo_period_id", None, None),
40+
("climo_station", "climo_station_id", [("climo_period", "climo_period_id"), ], None),
41+
("climo_stn_x_hist", ["climo_station_id", "history_id"], [("climo_station", "climo_station_id"), ("meta_history", "history_id")], None),
42+
("climo_variable", "climo_variable_id", None, None),
43+
("climo_value", "climo_value_id", [("climo_variable", "climo_variable_id"), ("climo_station", "climo_station_id")], None),
44+
)
45+
46+
def upgrade():
47+
48+
# We have to set the search_path so that the trigger functions fired when
49+
# the history table is populated can find the functions that they call.
50+
op.get_bind().execute(sa.text(f"SET search_path TO {schema_name}, public"))
51+
52+
for table_name, primary_key_name, foreign_tables, extra_indexes in table_info:
53+
# Primary table
54+
add_history_cols_to_primary(table_name)
55+
create_primary_table_triggers(table_name)
56+
57+
# History table
58+
create_history_table(table_name, foreign_tables)
59+
populate_history_table(table_name, primary_key_name, foreign_tables)
60+
# History table triggers must be created after the table is populated.
61+
create_history_table_triggers(table_name, foreign_tables)
62+
create_history_table_indexes(
63+
table_name, primary_key_name, foreign_tables, extra_indexes
64+
)
65+
grant_standard_table_privileges(table_name, schema=schema_name)
66+
67+
68+
def downgrade():
69+
for table_name, _, _, _ in reversed(table_info):
70+
drop_history_triggers(table_name)
71+
drop_history_table(table_name)
72+
drop_history_cols_from_primary(table_name)

pycds/alembic/versions/758be4f4ce0f_support_multiple_climatological_normals.py

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Support multiple climatological normals
1+
"""Support multiple climo normals
22
33
Revision ID: 758be4f4ce0f
44
Revises: 7ab87f8fbcf4
@@ -42,16 +42,14 @@
4242
def upgrade():
4343

4444
op.create_table(
45-
"climatological_period",
46-
Column("climatological_period_id", Integer, primary_key=True),
45+
"climo_period",
46+
Column("climo_period_id", Integer, primary_key=True),
4747
Column("start_date", DateTime, nullable=False),
4848
Column("end_date", DateTime, nullable=False),
49-
Column("mod_time", DateTime, nullable=False),
50-
Column("mod_user", String, nullable=False),
5149
schema=schema_name,
52-
)
50+
)
5351

54-
Enum("long-record", "composite", "prism", name="climatological_station_type_enum").create(op.get_bind())
52+
Enum("long-record", "composite", "prism", name="climo_station_type_enum").create(op.get_bind())
5553

5654
op.create_table(
5755
# TODO: Columns in this table parallel those in meta_station and meta_history.
@@ -62,35 +60,33 @@ def upgrade():
6260
# prevent entry of erroneous values that just happen to be very long?)
6361
#
6462
# - None are nullable. In contrast, most in the model tables are.
65-
"climatological_station", # TODO: Revise name?
66-
Column("climatological_station_id", Integer, primary_key=True),
63+
"climo_station", # TODO: Revise name?
64+
Column("climo_station_id", Integer, primary_key=True),
6765
Column(
68-
"type", PG_ENUM("long-record", "composite", "prism", name="climatological_station_type_enum", create_type=False), nullable=False
66+
"type", PG_ENUM("long-record", "composite", "prism", name="climo_station_type_enum", create_type=False), nullable=False
6967
),
7068
Column("basin_id", Integer, nullable=True),
7169
Column("comments", String, nullable=False),
7270
Column(
73-
"climatological_period_id",
71+
"climo_period_id",
7472
Integer,
7573
ForeignKey(
76-
f"{schema_name}.climatological_period.climatological_period_id"
74+
f"{schema_name}.climo_period.climo_period_id"
7775
),
7876
nullable=False,
7977
),
80-
Column("mod_time", DateTime, nullable=False),
81-
Column("mod_user", String, nullable=False),
8278
schema=schema_name,
8379
)
8480

85-
Enum("base", "joint", name="climatological_station_role_enum").create(op.get_bind())
81+
Enum("base", "joint", name="climo_station_role_enum").create(op.get_bind())
8682

8783
op.create_table(
88-
"climatological_station_x_meta_history",
84+
"climo_stn_x_hist",
8985
Column(
90-
"climatological_station_id",
86+
"climo_station_id",
9187
Integer,
9288
ForeignKey(
93-
f"{schema_name}.climatological_station.climatological_station_id"
89+
f"{schema_name}.climo_station.climo_station_id"
9490
),
9591
primary_key=True,
9692
),
@@ -100,13 +96,11 @@ def upgrade():
10096
ForeignKey(f"{schema_name}.meta_history.history_id"),
10197
primary_key=True,
10298
),
103-
Column("role", PG_ENUM("base", "joint", name="climatological_station_role_enum", create_type=False), nullable=False),
104-
Column("mod_time", DateTime, nullable=False),
105-
Column("mod_user", String, nullable=False),
99+
Column("role", PG_ENUM("base", "joint", name="climo_station_role_enum", create_type=False), nullable=False),
106100
schema=schema_name,
107101
)
108102

109-
Enum("annual", "seasonal", "monthly", name="climatology_duration_enum").create(op.get_bind())
103+
Enum("annual", "seasonal", "monthly", name="climo_duration_enum").create(op.get_bind())
110104

111105
op.create_table(
112106
# TODO: Columns in this table parallel those in meta_vars.
@@ -117,54 +111,50 @@ def upgrade():
117111
# prevent entry of erroneous values that just happen to be very long?)
118112
#
119113
# - None are nullable. In contrast, most in the model tables are.
120-
"climatological_variable",
121-
Column("climatological_variable_id", Integer, primary_key=True),
114+
"climo_variable",
115+
Column("climo_variable_id", Integer, primary_key=True),
122116
Column(
123-
"duration", PG_ENUM("annual", "seasonal", "monthly", name="climatology_duration_enum", create_type=False), nullable=False
117+
"duration", PG_ENUM("annual", "seasonal", "monthly", name="climo_duration_enum", create_type=False), nullable=False
124118
),
125119
Column("unit", String, nullable=False),
126120
Column("standard_name", String, nullable=False),
127121
Column("display_name", String, nullable=False),
128122
Column("short_name", String, nullable=False),
129123
Column("cell_methods", String, nullable=False),
130124
Column("net_var_name", CITEXT(), nullable=False),
131-
Column("mod_time", DateTime, nullable=False),
132-
Column("mod_user", String, nullable=False),
133125
schema=schema_name,
134126
)
135127

136128
op.create_table(
137-
"climatological_value",
138-
Column("climatological_value_id", Integer, primary_key=True),
129+
"climo_value",
130+
Column("climo_value_id", Integer, primary_key=True),
139131
Column("value_time", DateTime, nullable=False),
140132
Column("value", Float, nullable=False),
141133
Column("num_contributing_years", Integer, nullable=False),
142134
Column(
143-
"climatological_variable_id",
135+
"climo_variable_id",
144136
Integer,
145137
ForeignKey(
146-
f"{schema_name}.climatological_variable.climatological_variable_id"
138+
f"{schema_name}.climo_variable.climo_variable_id"
147139
),
148140
),
149141
Column(
150-
"climatological_station_id",
142+
"climo_station_id",
151143
Integer,
152144
ForeignKey(
153-
f"{schema_name}.climatological_station.climatological_station_id"
145+
f"{schema_name}.climo_station.climo_station_id"
154146
),
155147
),
156-
Column("mod_time", DateTime, nullable=False),
157-
Column("mod_user", String, nullable=False),
158148
schema=schema_name,
159149
)
160150

161151

162152
def downgrade():
163-
op.drop_table("climatological_value", schema=schema_name)
164-
op.drop_table("climatological_variable", schema=schema_name)
165-
Enum("annual", "seasonal", "monthly", name="climatology_duration_enum").drop(op.get_bind())
166-
op.drop_table("climatological_station_x_meta_history", schema=schema_name)
167-
Enum("base", "joint", name="climatological_station_role_enum").drop(op.get_bind())
168-
op.drop_table("climatological_station", schema=schema_name)
169-
Enum("long-record", "composite", "prism", name="climatological_station_type_enum").drop(op.get_bind())
170-
op.drop_table("climatological_period", schema=schema_name)
153+
op.drop_table("climo_value", schema=schema_name)
154+
op.drop_table("climo_variable", schema=schema_name)
155+
Enum("annual", "seasonal", "monthly", name="climo_duration_enum").drop(op.get_bind())
156+
op.drop_table("climo_stn_x_hist", schema=schema_name)
157+
Enum("base", "joint", name="climo_station_role_enum").drop(op.get_bind())
158+
op.drop_table("climo_station", schema=schema_name)
159+
Enum("long-record", "composite", "prism", name="climo_station_type_enum").drop(op.get_bind())
160+
op.drop_table("climo_period", schema=schema_name)

0 commit comments

Comments
 (0)