Skip to content

Commit 17d6012

Browse files
authored
Merge pull request #5237 from hjoliver/workflow-state-back-compat
workflow-state command back compat.
2 parents 96ddc35 + 2ac41ea commit 17d6012

File tree

7 files changed

+116
-8
lines changed

7 files changed

+116
-8
lines changed

changes.d/5237.feat.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Back-compat: allow workflow-state xtriggers (and the `cylc workflow-state`
2+
command) to read Cylc 7 databases.

cylc/flow/dbstatecheck.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,13 @@ class CylcWorkflowDBChecker:
5656
],
5757
}
5858

59-
def __init__(self, rund, workflow):
60-
db_path = expand_path(
61-
rund, workflow, "log", CylcWorkflowDAO.DB_FILE_BASE_NAME
62-
)
59+
def __init__(self, rund, workflow, db_path=None):
60+
# (Explicit dp_path arg is to make testing easier).
61+
if db_path is None:
62+
# Infer DB path from workflow name and run dir.
63+
db_path = expand_path(
64+
rund, workflow, "log", CylcWorkflowDAO.DB_FILE_BASE_NAME
65+
)
6366
if not os.path.exists(db_path):
6467
raise OSError(errno.ENOENT, os.strerror(errno.ENOENT), db_path)
6568
self.conn = sqlite3.connect(db_path, timeout=10.0)
@@ -73,7 +76,7 @@ def display_maps(res):
7376
sys.stdout.write((", ").join(row) + "\n")
7477

7578
def get_remote_point_format(self):
76-
"""Query a remote workflow database for a 'cycle point format' entry"""
79+
"""Query a workflow database for a 'cycle point format' entry"""
7780
for row in self.conn.execute(
7881
rf'''
7982
SELECT
@@ -87,6 +90,24 @@ def get_remote_point_format(self):
8790
):
8891
return row[0]
8992

93+
def get_remote_point_format_compat(self):
94+
"""Query a Cylc 7 suite database for a 'cycle point format' entry.
95+
96+
Back compat for Cylc 8 workflow state triggers targeting Cylc 7 DBs.
97+
"""
98+
for row in self.conn.execute(
99+
rf'''
100+
SELECT
101+
value
102+
FROM
103+
{CylcWorkflowDAO.TABLE_SUITE_PARAMS}
104+
WHERE
105+
key==?
106+
''', # nosec (table name is code constant)
107+
['cycle_point_format']
108+
):
109+
return row[0]
110+
90111
def state_lookup(self, state):
91112
"""allows for multiple states to be searched via a status alias"""
92113
if state in self.STATE_ALIASES:

cylc/flow/rundb.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ class CylcWorkflowDAO:
179179
TABLE_BROADCAST_STATES = "broadcast_states"
180180
TABLE_INHERITANCE = "inheritance"
181181
TABLE_WORKFLOW_PARAMS = "workflow_params"
182+
# BACK COMPAT: suite_params
183+
# This Cylc 7 DB table is needed to allow workflow-state
184+
# xtriggers (and the `cylc workflow-state` command) to
185+
# work with Cylc 7 workflows.
186+
# url: https://github.com/cylc/cylc-flow/issues/5236
187+
# remove at: 8.x
188+
TABLE_SUITE_PARAMS = "suite_params"
182189
TABLE_WORKFLOW_FLOWS = "workflow_flows"
183190
TABLE_WORKFLOW_TEMPLATE_VARS = "workflow_template_vars"
184191
TABLE_TASK_JOBS = "task_jobs"

cylc/flow/scripts/workflow_state.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,13 @@ def connect(self):
115115
sys.stderr.write('\n')
116116

117117
if connected and self.args['cycle']:
118-
fmt = self.checker.get_remote_point_format()
118+
try:
119+
fmt = self.checker.get_remote_point_format()
120+
except sqlite3.OperationalError as exc:
121+
try:
122+
fmt = self.checker.get_remote_point_format_compat()
123+
except sqlite3.OperationalError:
124+
raise exc # original error
119125
if fmt:
120126
my_parser = TimePointParser()
121127
my_point = my_parser.parse(self.args['cycle'], dump_format=fmt)

cylc/flow/xtriggers/workflow_state.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,13 @@ def workflow_state(
9090
except (OSError, sqlite3.Error):
9191
# Failed to connect to DB; target workflow may not be started.
9292
return (False, None)
93-
fmt = checker.get_remote_point_format()
93+
try:
94+
fmt = checker.get_remote_point_format()
95+
except sqlite3.OperationalError as exc:
96+
try:
97+
fmt = checker.get_remote_point_format_compat()
98+
except sqlite3.OperationalError:
99+
raise exc # original error
94100
if fmt:
95101
my_parser = TimePointParser()
96102
point = str(my_parser.parse(point, dump_format=fmt))

tests/unit/test_workflow_db_mgr.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
CylcWorkflowDAO,
2727
WorkflowDatabaseManager,
2828
)
29+
from cylc.flow.dbstatecheck import CylcWorkflowDBChecker
2930

3031

3132
@pytest.fixture
@@ -116,3 +117,22 @@ def test_check_workflow_db_compat(_setup_db, capsys):
116117

117118
with pytest.raises(ServiceFileError, match='99.99'):
118119
WorkflowDatabaseManager.check_db_compatibility(pri_path)
120+
121+
122+
def test_cylc_7_db_wflow_params_table(_setup_db):
123+
"""Test back-compat needed by workflow state xtrigger for Cylc 7 DBs."""
124+
ptformat = "CCYY"
125+
create = r'CREATE TABLE suite_params(key TEXT, value TEXT)'
126+
insert = (
127+
r'INSERT INTO suite_params VALUES'
128+
rf'("cycle_point_format", "{ptformat}")'
129+
)
130+
db_file_name = _setup_db([create, insert])
131+
checker = CylcWorkflowDBChecker('foo', 'bar', db_path=db_file_name)
132+
133+
with pytest.raises(
134+
sqlite3.OperationalError, match="no such table: workflow_params"
135+
):
136+
checker.get_remote_point_format()
137+
138+
assert checker.get_remote_point_format_compat() == ptformat

tests/unit/xtriggers/test_workflow_state.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
# You should have received a copy of the GNU General Public License
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17+
from pathlib import Path
18+
import sqlite3
1719
from typing import Callable
1820
from unittest.mock import Mock
1921

20-
22+
from cylc.flow.workflow_files import WorkflowFiles
2123
from cylc.flow.xtriggers.workflow_state import workflow_state
2224
from ..conftest import MonkeyMock
2325

@@ -38,3 +40,47 @@ def test_inferred_run(tmp_run_dir: Callable, monkeymock: MonkeyMock):
3840
_, results = workflow_state(id_, task='precious', point='3000')
3941
mock_db_checker.assert_called_once_with(cylc_run_dir, expected_workflow_id)
4042
assert results['workflow'] == expected_workflow_id
43+
44+
45+
def test_back_compat(tmp_run_dir):
46+
"""Test workflow_state xtrigger backwards compatibility with Cylc 7
47+
database."""
48+
id_ = 'celebrimbor'
49+
c7_run_dir: Path = tmp_run_dir(id_)
50+
(c7_run_dir / WorkflowFiles.FLOW_FILE).rename(
51+
c7_run_dir / WorkflowFiles.SUITE_RC
52+
)
53+
db_file = c7_run_dir / 'log' / 'db'
54+
db_file.parent.mkdir(exist_ok=True)
55+
# Note: cannot use CylcWorkflowDAO here as creating outdated DB
56+
conn = sqlite3.connect(str(db_file))
57+
try:
58+
conn.execute(r"""
59+
CREATE TABLE suite_params(key TEXT, value TEXT, PRIMARY KEY(key));
60+
""")
61+
conn.execute(r"""
62+
CREATE TABLE task_states(
63+
name TEXT, cycle TEXT, time_created TEXT, time_updated TEXT,
64+
submit_num INTEGER, status TEXT, PRIMARY KEY(name, cycle)
65+
);
66+
""")
67+
conn.executemany(
68+
r'INSERT INTO "suite_params" VALUES(?,?);',
69+
[('cylc_version', '7.8.12'),
70+
('cycle_point_format', '%Y'),
71+
('cycle_point_tz', 'Z')]
72+
)
73+
conn.execute(r"""
74+
INSERT INTO "task_states" VALUES(
75+
'mithril','2012','2023-01-30T18:19:15Z','2023-01-30T18:19:15Z',
76+
0,'succeeded'
77+
);
78+
""")
79+
conn.commit()
80+
finally:
81+
conn.close()
82+
83+
satisfied, _ = workflow_state(id_, task='mithril', point='2012')
84+
assert satisfied
85+
satisfied, _ = workflow_state(id_, task='arkenstone', point='2012')
86+
assert not satisfied

0 commit comments

Comments
 (0)