[Concept,06/11] patman: Add workflow table to the database

Message ID 20260329150140.4095446-7-sjg@u-boot.org
State New
Headers
Series patman: Add workflow tracking for patch series |

Commit Message

Simon Glass March 29, 2026, 3:01 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

Add an extensible workflow table for tracking series-related tasks.
Each entry has a type, series reference, timestamp and an archived
flag for preserving history. The first type is 'todo' for marking
series that need attention after a certain date.

Add database methods for adding, archiving and querying workflow
entries, with filtering by type and due date. Archived entries are
kept for history but excluded from active queries.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 tools/patman/database.py     | 91 +++++++++++++++++++++++++++++++++++-
 tools/patman/test_cseries.py | 48 ++++++++++++++++++-
 tools/patman/workflow.py     | 13 ++++++
 3 files changed, 150 insertions(+), 2 deletions(-)
 create mode 100644 tools/patman/workflow.py
  

Patch

diff --git a/tools/patman/database.py b/tools/patman/database.py
index e1ec0dc00e2..edb7d116c33 100644
--- a/tools/patman/database.py
+++ b/tools/patman/database.py
@@ -19,7 +19,7 @@  from u_boot_pylib import tout
 from patman.series import Series
 
 # Schema version (version 0 means there is no database yet)
-LATEST = 5
+LATEST = 6
 
 # Information about a series/version record
 SerVer = namedtuple(
@@ -198,6 +198,21 @@  class Database:  # pylint:disable=R0904
         self.cur.execute('ALTER TABLE upstream ADD COLUMN no_tags BIT')
         self.cur.execute('ALTER TABLE ser_ver ADD COLUMN desc')
 
+    def _migrate_to_v6(self):
+        """Add workflow table for tracking todos and other workflow items
+
+        Fields:
+            id: Auto-increment primary key
+            type: Workflow-entry type, e.g. 'todo' or 'sent'
+            series_id: Foreign key referencing series.id
+            timestamp: Due/event time as 'YYYY-MM-DD HH:MM:SS'
+            archived: 0 for active entries, 1 for archived (soft-delete)
+        """
+        self.cur.execute(
+            'CREATE TABLE workflow (id INTEGER PRIMARY KEY AUTOINCREMENT,'
+            'type, series_id INTEGER, timestamp, archived BIT,'
+            'FOREIGN KEY (series_id) REFERENCES series (id))')
+
     def migrate_to(self, dest_version):
         """Migrate the database to the selected version
 
@@ -226,6 +241,8 @@  class Database:  # pylint:disable=R0904
                 self._migrate_to_v4()
             elif version == 5:
                 self._migrate_to_v5()
+            elif version == 6:
+                self._migrate_to_v6()
 
             # Save the new version if we have a schema_version table
             if version > 1:
@@ -1041,3 +1058,75 @@  class Database:  # pylint:disable=R0904
         if not recs:
             return None
         return recs[0]
+
+    # workflow functions
+
+    def workflow_add(self, wtype, series_id, timestamp):
+        """Add a workflow entry
+
+        Args:
+            wtype (str): Workflow type, e.g. 'todo'
+            series_id (int): ID of the series
+            timestamp (str): Timestamp string, e.g. '2025-01-15 10:30:00'
+        """
+        self.execute(
+            'INSERT INTO workflow (type, series_id, timestamp, archived) '
+            'VALUES (?, ?, ?, 0)', (wtype, series_id, timestamp))
+
+    def workflow_archive(self, wtype, series_id):
+        """Archive active workflow entries for a given type and series
+
+        Args:
+            wtype (str): Workflow type, e.g. 'todo'
+            series_id (int): ID of the series
+        """
+        self.execute(
+            'UPDATE workflow SET archived = 1 '
+            'WHERE type = ? AND series_id = ? AND archived = 0',
+            (wtype, series_id))
+
+    def workflow_get(self, wtype, series_id):
+        """Get the active workflow entry for a given type and series
+
+        Args:
+            wtype (str): Workflow type, e.g. 'todo'
+            series_id (int): ID of the series
+
+        Return:
+            str or None: Timestamp string if found, else None
+        """
+        res = self.execute(
+            'SELECT timestamp FROM workflow '
+            'WHERE type = ? AND series_id = ? AND archived = 0',
+            (wtype, series_id))
+        rec = res.fetchone()
+        if rec:
+            return rec[0]
+        return None
+
+    def workflow_get_by_type(self, wtype, before=None):
+        """Get active workflow entries for a given type, joined with series
+
+        Args:
+            wtype (str): Workflow type, e.g. 'todo'
+            before (str or None): If set, only return entries where
+                timestamp <= this value
+
+        Return:
+            list of tuple:
+                int: series ID
+                str: series name
+                str: series description
+                str: timestamp
+        """
+        query = ('SELECT s.id, s.name, s.desc, w.timestamp '
+                 'FROM workflow w '
+                 'JOIN series s ON w.series_id = s.id '
+                 'WHERE w.type = ? AND w.archived = 0 AND s.archived = 0')
+        params = [wtype]
+        if before is not None:
+            query += ' AND w.timestamp <= ?'
+            params.append(before)
+        query += ' ORDER BY w.timestamp'
+        res = self.execute(query, params)
+        return res.fetchall()
diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py
index c0beb128265..798673e09cb 100644
--- a/tools/patman/test_cseries.py
+++ b/tools/patman/test_cseries.py
@@ -26,6 +26,7 @@  from patman import database
 from patman import patchstream
 from patman.patchwork import Patchwork
 from patman.test_common import TestCommon
+from patman import workflow as wf
 
 HASH_RE = r'[0-9a-f]+'
 #pylint: disable=protected-access
@@ -3556,7 +3557,7 @@  Date:   .*
             self.assertEqual(f'Update database to v{version}',
                              out.getvalue().strip())
             self.assertEqual(version, db.get_schema_version())
-        self.assertEqual(5, database.LATEST)
+        self.assertEqual(6, database.LATEST)
 
     def test_migrate_future_version(self):
         """Test that a database newer than patman is rejected"""
@@ -4130,3 +4131,48 @@  Date:   .*
             self.run_args('series', '-s', 'first', 'version-change',
                           '--new-version', '3', pwork=True)
         method.assert_called_once_with('first', None, 3, dry_run=False)
+
+    def test_workflow_db_methods(self):
+        """Test workflow database methods"""
+        cser = self.get_cser()
+        with terminal.capture():
+            cser.add('first', 'my description', allow_unmarked=True)
+
+        ser = cser.get_series_by_name('first')
+
+        # Initially there is no workflow entry
+        self.assertIsNone(cser.db.workflow_get('todo', ser.idnum))
+
+        # Add a todo entry
+        cser.db.workflow_add('todo', ser.idnum, '2025-03-15 10:00:00')
+        cser.commit()
+
+        # Should be able to read it back
+        ts = cser.db.workflow_get('todo', ser.idnum)
+        self.assertEqual('2025-03-15 10:00:00', ts)
+
+        # Get by type should return it
+        entries = cser.db.workflow_get_by_type('todo')
+        self.assertEqual(1, len(entries))
+        entry = entries[0]
+        self.assertEqual(ser.idnum, entry[0])
+        self.assertEqual('first', entry[1])
+        self.assertEqual('my description', entry[2])
+        self.assertEqual('2025-03-15 10:00:00', entry[3])
+
+        # Get by type with before filter
+        entries = cser.db.workflow_get_by_type(
+            'todo', before='2025-03-14 00:00:00')
+        self.assertEqual(0, len(entries))
+        entries = cser.db.workflow_get_by_type(
+            'todo', before='2025-03-16 00:00:00')
+        self.assertEqual(1, len(entries))
+
+        # Archive it - should no longer be active, but still in the table
+        cser.db.workflow_archive('todo', ser.idnum)
+        cser.commit()
+        self.assertIsNone(cser.db.workflow_get('todo', ser.idnum))
+        res = cser.db.execute(
+            'SELECT archived FROM workflow WHERE series_id = ?',
+            (ser.idnum,))
+        self.assertEqual(1, res.fetchone()[0])
diff --git a/tools/patman/workflow.py b/tools/patman/workflow.py
new file mode 100644
index 00000000000..37644a5de88
--- /dev/null
+++ b/tools/patman/workflow.py
@@ -0,0 +1,13 @@ 
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Copyright 2025 Google LLC
+#
+
+"""Workflow types and operations for patman series management"""
+
+import enum
+
+
+class Wtype(enum.StrEnum):
+    """Types of workflow entry"""
+    TODO = 'todo'