diff options
Diffstat (limited to 'tools/patman/database.py')
| -rw-r--r-- | tools/patman/database.py | 823 |
1 files changed, 0 insertions, 823 deletions
diff --git a/tools/patman/database.py b/tools/patman/database.py deleted file mode 100644 index 9c25b04a720..00000000000 --- a/tools/patman/database.py +++ /dev/null @@ -1,823 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0+ -# -# Copyright 2025 Simon Glass <[email protected]> -# -"""Handles the patman database - -This uses sqlite3 with a local file. - -To adjsut the schema, increment LATEST, create a migrate_to_v<x>() function -and write some code in migrate_to() to call it. -""" - -from collections import namedtuple, OrderedDict -import os -import sqlite3 - -from u_boot_pylib import tools -from u_boot_pylib import tout -from patman.series import Series - -# Schema version (version 0 means there is no database yet) -LATEST = 4 - -# Information about a series/version record -SerVer = namedtuple( - 'SER_VER', - 'idnum,series_id,version,link,cover_id,cover_num_comments,name,' - 'archive_tag') - -# Record from the pcommit table: -# idnum (int): record ID -# seq (int): Patch sequence in series (0 is first) -# subject (str): patch subject -# svid (int): ID of series/version record in ser_ver table -# change_id (str): Change-ID value -# state (str): Current status in patchwork -# patch_id (int): Patchwork's patch ID for this patch -# num_comments (int): Number of comments attached to the commit -Pcommit = namedtuple( - 'PCOMMIT', - 'idnum,seq,subject,svid,change_id,state,patch_id,num_comments') - - -class Database: - """Database of information used by patman""" - - # dict of databases: - # key: filename - # value: Database object - instances = {} - - def __init__(self, db_path): - """Set up a new database object - - Args: - db_path (str): Path to the database - """ - if db_path in Database.instances: - # Two connections to the database can cause: - # sqlite3.OperationalError: database is locked - raise ValueError(f"There is already a database for '{db_path}'") - self.con = None - self.cur = None - self.db_path = db_path - self.is_open = False - Database.instances[db_path] = self - - @staticmethod - def get_instance(db_path): - """Get the database instance for a path - - This is provides to ensure that different callers can obtain the - same database object when accessing the same database file. - - Args: - db_path (str): Path to the database - - Return: - Database: Database instance, which is created if necessary - """ - db = Database.instances.get(db_path) - if db: - return db, False - return Database(db_path), True - - def start(self): - """Open the database read for use, migrate to latest schema""" - self.open_it() - self.migrate_to(LATEST) - - def open_it(self): - """Open the database, creating it if necessary""" - if self.is_open: - raise ValueError('Already open') - if not os.path.exists(self.db_path): - tout.warning(f'Creating new database {self.db_path}') - self.con = sqlite3.connect(self.db_path) - self.cur = self.con.cursor() - self.is_open = True - - def close(self): - """Close the database""" - if not self.is_open: - raise ValueError('Already closed') - self.con.close() - self.cur = None - self.con = None - self.is_open = False - - def create_v1(self): - """Create a database with the v1 schema""" - self.cur.execute( - 'CREATE TABLE series (id INTEGER PRIMARY KEY AUTOINCREMENT,' - 'name UNIQUE, desc, archived BIT)') - - # Provides a series_id/version pair, which is used to refer to a - # particular series version sent to patchwork. This stores the link - # to patchwork - self.cur.execute( - 'CREATE TABLE ser_ver (id INTEGER PRIMARY KEY AUTOINCREMENT,' - 'series_id INTEGER, version INTEGER, link,' - 'FOREIGN KEY (series_id) REFERENCES series (id))') - - self.cur.execute( - 'CREATE TABLE upstream (name UNIQUE, url, is_default BIT)') - - # change_id is the Change-Id - # patch_id is the ID of the patch on the patchwork server - self.cur.execute( - 'CREATE TABLE pcommit (id INTEGER PRIMARY KEY AUTOINCREMENT,' - 'svid INTEGER, seq INTEGER, subject, patch_id INTEGER, ' - 'change_id, state, num_comments INTEGER, ' - 'FOREIGN KEY (svid) REFERENCES ser_ver (id))') - - self.cur.execute( - 'CREATE TABLE settings (name UNIQUE, proj_id INT, link_name)') - - def _migrate_to_v2(self): - """Add a schema_version table""" - self.cur.execute('CREATE TABLE schema_version (version INTEGER)') - - def _migrate_to_v3(self): - """Store the number of cover-letter comments in the schema""" - self.cur.execute('ALTER TABLE ser_ver ADD COLUMN cover_id') - self.cur.execute('ALTER TABLE ser_ver ADD COLUMN cover_num_comments ' - 'INTEGER') - self.cur.execute('ALTER TABLE ser_ver ADD COLUMN name') - - def _migrate_to_v4(self): - """Add an archive tag for each ser_ver""" - self.cur.execute('ALTER TABLE ser_ver ADD COLUMN archive_tag') - - def migrate_to(self, dest_version): - """Migrate the database to the selected version - - Args: - dest_version (int): Version to migrate to - """ - while True: - version = self.get_schema_version() - if version == dest_version: - break - - self.close() - tools.write_file(f'{self.db_path}old.v{version}', - tools.read_file(self.db_path)) - - version += 1 - tout.info(f'Update database to v{version}') - self.open_it() - if version == 1: - self.create_v1() - elif version == 2: - self._migrate_to_v2() - elif version == 3: - self._migrate_to_v3() - elif version == 4: - self._migrate_to_v4() - - # Save the new version if we have a schema_version table - if version > 1: - self.cur.execute('DELETE FROM schema_version') - self.cur.execute( - 'INSERT INTO schema_version (version) VALUES (?)', - (version,)) - self.commit() - - def get_schema_version(self): - """Get the version of the database's schema - - Return: - int: Database version, 0 means there is no data; anything less than - LATEST means the schema is out of date and must be updated - """ - # If there is no database at all, assume v0 - version = 0 - try: - self.cur.execute('SELECT name FROM series') - except sqlite3.OperationalError: - return 0 - - # If there is no schema, assume v1 - try: - self.cur.execute('SELECT version FROM schema_version') - version = self.cur.fetchone()[0] - except sqlite3.OperationalError: - return 1 - return version - - def execute(self, query, parameters=()): - """Execute a database query - - Args: - query (str): Query string - parameters (list of values): Parameters to pass - - Return: - - """ - return self.cur.execute(query, parameters) - - def commit(self): - """Commit changes to the database""" - self.con.commit() - - def rollback(self): - """Roll back changes to the database""" - self.con.rollback() - - def lastrowid(self): - """Get the last row-ID reported by the database - - Return: - int: Value for lastrowid - """ - return self.cur.lastrowid - - def rowcount(self): - """Get the row-count reported by the database - - Return: - int: Value for rowcount - """ - return self.cur.rowcount - - def _get_series_list(self, include_archived): - """Get a list of Series objects from the database - - Args: - include_archived (bool): True to include archives series - - Return: - list of Series - """ - res = self.execute( - 'SELECT id, name, desc FROM series ' + - ('WHERE archived = 0' if not include_archived else '')) - return [Series.from_fields(idnum=idnum, name=name, desc=desc) - for idnum, name, desc in res.fetchall()] - - # series functions - - def series_get_dict_by_id(self, include_archived=False): - """Get a dict of Series objects from the database - - Args: - include_archived (bool): True to include archives series - - Return: - OrderedDict: - key: series ID - value: Series with idnum, name and desc filled out - """ - sdict = OrderedDict() - for ser in self._get_series_list(include_archived): - sdict[ser.idnum] = ser - return sdict - - def series_find_by_name(self, name, include_archived=False): - """Find a series and return its details - - Args: - name (str): Name to search for - include_archived (bool): True to include archives series - - Returns: - idnum, or None if not found - """ - res = self.execute( - 'SELECT id FROM series WHERE name = ?' + - ('AND archived = 0' if not include_archived else ''), (name,)) - recs = res.fetchall() - - # This shouldn't happen - assert len(recs) <= 1, 'Expected one match, but multiple found' - - if len(recs) != 1: - return None - return recs[0][0] - - def series_get_info(self, idnum): - """Get information for a series from the database - - Args: - idnum (int): Series ID to look up - - Return: tuple: - str: Series name - str: Series description - - Raises: - ValueError: Series is not found - """ - res = self.execute('SELECT name, desc FROM series WHERE id = ?', - (idnum,)) - recs = res.fetchall() - if len(recs) != 1: - raise ValueError(f'No series found (id {idnum} len {len(recs)})') - return recs[0] - - def series_get_dict(self, include_archived=False): - """Get a dict of Series objects from the database - - Args: - include_archived (bool): True to include archives series - - Return: - OrderedDict: - key: series name - value: Series with idnum, name and desc filled out - """ - sdict = OrderedDict() - for ser in self._get_series_list(include_archived): - sdict[ser.name] = ser - return sdict - - def series_get_version_list(self, series_idnum): - """Get a list of the versions available for a series - - Args: - series_idnum (int): ID of series to look up - - Return: - str: List of versions, which may be empty if the series is in the - process of being added - """ - res = self.execute('SELECT version FROM ser_ver WHERE series_id = ?', - (series_idnum,)) - return [x[0] for x in res.fetchall()] - - def series_get_max_version(self, series_idnum): - """Get the highest version number available for a series - - Args: - series_idnum (int): ID of series to look up - - Return: - int: Maximum version number - """ - res = self.execute( - 'SELECT MAX(version) FROM ser_ver WHERE series_id = ?', - (series_idnum,)) - return res.fetchall()[0][0] - - def series_get_all_max_versions(self): - """Find the latest version of all series - - Return: list of: - int: ser_ver ID - int: series ID - int: Maximum version - """ - res = self.execute( - 'SELECT id, series_id, MAX(version) FROM ser_ver ' - 'GROUP BY series_id') - return res.fetchall() - - def series_add(self, name, desc): - """Add a new series record - - The new record is set to not archived - - Args: - name (str): Series name - desc (str): Series description - - Return: - int: ID num of the new series record - """ - self.execute( - 'INSERT INTO series (name, desc, archived) ' - f"VALUES ('{name}', '{desc}', 0)") - return self.lastrowid() - - def series_remove(self, idnum): - """Remove a series from the database - - The series must exist - - Args: - idnum (int): ID num of series to remove - """ - self.execute('DELETE FROM series WHERE id = ?', (idnum,)) - assert self.rowcount() == 1 - - def series_remove_by_name(self, name): - """Remove a series from the database - - Args: - name (str): Name of series to remove - - Raises: - ValueError: Series does not exist (database is rolled back) - """ - self.execute('DELETE FROM series WHERE name = ?', (name,)) - if self.rowcount() != 1: - self.rollback() - raise ValueError(f"No such series '{name}'") - - def series_set_archived(self, series_idnum, archived): - """Update archive flag for a series - - Args: - series_idnum (int): ID num of the series - archived (bool): Whether to mark the series as archived or - unarchived - """ - self.execute( - 'UPDATE series SET archived = ? WHERE id = ?', - (archived, series_idnum)) - - def series_set_name(self, series_idnum, name): - """Update name for a series - - Args: - series_idnum (int): ID num of the series - name (str): new name to use - """ - self.execute( - 'UPDATE series SET name = ? WHERE id = ?', (name, series_idnum)) - - # ser_ver functions - - def ser_ver_get_link(self, series_idnum, version): - """Get the link for a series version - - Args: - series_idnum (int): ID num of the series - version (int): Version number to search for - - Return: - str: Patchwork link as a string, e.g. '12325', or None if none - - Raises: - ValueError: Multiple matches are found - """ - res = self.execute( - 'SELECT link FROM ser_ver WHERE ' - f"series_id = {series_idnum} AND version = '{version}'") - recs = res.fetchall() - if not recs: - return None - if len(recs) > 1: - raise ValueError('Expected one match, but multiple matches found') - return recs[0][0] - - def ser_ver_set_link(self, series_idnum, version, link): - """Set the link for a series version - - Args: - series_idnum (int): ID num of the series - version (int): Version number to search for - link (str): Patchwork link for the ser_ver - - Return: - bool: True if the record was found and updated, else False - """ - if link is None: - link = '' - self.execute( - 'UPDATE ser_ver SET link = ? WHERE series_id = ? AND version = ?', - (str(link), series_idnum, version)) - return self.rowcount() != 0 - - def ser_ver_set_info(self, info): - """Set the info for a series version - - Args: - info (SER_VER): Info to set. Only two options are supported: - 1: svid,cover_id,cover_num_comments,name - 2: svid,name - - Return: - bool: True if the record was found and updated, else False - """ - assert info.idnum is not None - if info.cover_id: - assert info.series_id is None - self.execute( - 'UPDATE ser_ver SET cover_id = ?, cover_num_comments = ?, ' - 'name = ? WHERE id = ?', - (info.cover_id, info.cover_num_comments, info.name, - info.idnum)) - else: - assert not info.cover_id - assert not info.cover_num_comments - assert not info.series_id - assert not info.version - assert not info.link - self.execute('UPDATE ser_ver SET name = ? WHERE id = ?', - (info.name, info.idnum)) - - return self.rowcount() != 0 - - def ser_ver_set_version(self, svid, version): - """Sets the version for a ser_ver record - - Args: - svid (int): Record ID to update - version (int): Version number to add - - Raises: - ValueError: svid was not found - """ - self.execute( - 'UPDATE ser_ver SET version = ? WHERE id = ?', (version, svid)) - if self.rowcount() != 1: - raise ValueError(f'No ser_ver updated (svid {svid})') - - def ser_ver_set_archive_tag(self, svid, tag): - """Sets the archive tag for a ser_ver record - - Args: - svid (int): Record ID to update - tag (tag): Tag to add - - Raises: - ValueError: svid was not found - """ - self.execute( - 'UPDATE ser_ver SET archive_tag = ? WHERE id = ?', (tag, svid)) - if self.rowcount() != 1: - raise ValueError(f'No ser_ver updated (svid {svid})') - - def ser_ver_add(self, series_idnum, version, link=None): - """Add a new ser_ver record - - Args: - series_idnum (int): ID num of the series which is getting a new - version - version (int): Version number to add - link (str): Patchwork link, or None if not known - - Return: - int: ID num of the new ser_ver record - """ - self.execute( - 'INSERT INTO ser_ver (series_id, version, link) VALUES (?, ?, ?)', - (series_idnum, version, link)) - return self.lastrowid() - - def ser_ver_get_for_series(self, series_idnum, version=None): - """Get a list of ser_ver records for a given series ID - - Args: - series_idnum (int): ID num of the series to search - version (int): Version number to search for, or None for all - - Return: - SER_VER: Requested information - - Raises: - ValueError: There is no matching idnum/version - """ - base = ('SELECT id, series_id, version, link, cover_id, ' - 'cover_num_comments, name, archive_tag FROM ser_ver ' - 'WHERE series_id = ?') - if version: - res = self.execute(base + ' AND version = ?', - (series_idnum, version)) - else: - res = self.execute(base, (series_idnum,)) - recs = res.fetchall() - if not recs: - raise ValueError( - f'No matching series for id {series_idnum} version {version}') - if version: - return SerVer(*recs[0]) - return [SerVer(*x) for x in recs] - - def ser_ver_get_ids_for_series(self, series_idnum, version=None): - """Get a list of ser_ver records for a given series ID - - Args: - series_idnum (int): ID num of the series to search - version (int): Version number to search for, or None for all - - Return: - list of int: List of svids for the matching records - """ - if version: - res = self.execute( - 'SELECT id FROM ser_ver WHERE series_id = ? AND version = ?', - (series_idnum, version)) - else: - res = self.execute( - 'SELECT id FROM ser_ver WHERE series_id = ?', (series_idnum,)) - return list(res.fetchall()[0]) - - def ser_ver_get_list(self): - """Get a list of patchwork entries from the database - - Return: - list of SER_VER - """ - res = self.execute( - 'SELECT id, series_id, version, link, cover_id, ' - 'cover_num_comments, name, archive_tag FROM ser_ver') - items = res.fetchall() - return [SerVer(*x) for x in items] - - def ser_ver_remove(self, series_idnum, version=None, remove_pcommits=True, - remove_series=True): - """Delete a ser_ver record - - Removes the record which has the given series ID num and version - - Args: - series_idnum (int): ID num of the series - version (int): Version number, or None to remove all versions - remove_pcommits (bool): True to remove associated pcommits too - remove_series (bool): True to remove the series if versions is None - """ - if remove_pcommits: - # Figure out svids to delete - svids = self.ser_ver_get_ids_for_series(series_idnum, version) - - self.pcommit_delete_list(svids) - - if version: - self.execute( - 'DELETE FROM ser_ver WHERE series_id = ? AND version = ?', - (series_idnum, version)) - else: - self.execute( - 'DELETE FROM ser_ver WHERE series_id = ?', - (series_idnum,)) - if not version and remove_series: - self.series_remove(series_idnum) - - # pcommit functions - - def pcommit_get_list(self, find_svid=None): - """Get a dict of pcommits entries from the database - - Args: - find_svid (int): If not None, finds the records associated with a - particular series and version; otherwise returns all records - - Return: - list of PCOMMIT: pcommit records - """ - query = ('SELECT id, seq, subject, svid, change_id, state, patch_id, ' - 'num_comments FROM pcommit') - if find_svid is not None: - query += f' WHERE svid = {find_svid}' - res = self.execute(query) - return [Pcommit(*rec) for rec in res.fetchall()] - - def pcommit_add_list(self, svid, pcommits): - """Add records to the pcommit table - - Args: - svid (int): ser_ver ID num - pcommits (list of PCOMMIT): Only seq, subject, change_id are - uses; svid comes from the argument passed in and the others - are assumed to be obtained from patchwork later - """ - for pcm in pcommits: - self.execute( - 'INSERT INTO pcommit (svid, seq, subject, change_id) VALUES ' - '(?, ?, ?, ?)', (svid, pcm.seq, pcm.subject, pcm.change_id)) - - def pcommit_delete(self, svid): - """Delete pcommit records for a given ser_ver ID - - Args_: - svid (int): ser_ver ID num of records to delete - """ - self.execute('DELETE FROM pcommit WHERE svid = ?', (svid,)) - - def pcommit_delete_list(self, svid_list): - """Delete pcommit records for a given set of ser_ver IDs - - Args_: - svid (list int): ser_ver ID nums of records to delete - """ - vals = ', '.join([str(x) for x in svid_list]) - self.execute('DELETE FROM pcommit WHERE svid IN (?)', (vals,)) - - def pcommit_update(self, pcm): - """Update a pcommit record - - Args: - pcm (PCOMMIT): Information to write; only the idnum, state, - patch_id and num_comments are used - - Return: - True if the data was written - """ - self.execute( - 'UPDATE pcommit SET ' - 'patch_id = ?, state = ?, num_comments = ? WHERE id = ?', - (pcm.patch_id, pcm.state, pcm.num_comments, pcm.idnum)) - return self.rowcount() > 0 - - # upstream functions - - def upstream_add(self, name, url): - """Add a new upstream record - - Args: - name (str): Name of the tree - url (str): URL for the tree - - Raises: - ValueError if the name already exists in the database - """ - try: - self.execute( - 'INSERT INTO upstream (name, url) VALUES (?, ?)', (name, url)) - except sqlite3.IntegrityError as exc: - if 'UNIQUE constraint failed: upstream.name' in str(exc): - raise ValueError(f"Upstream '{name}' already exists") from exc - - def upstream_set_default(self, name): - """Mark (only) the given upstream as the default - - Args: - name (str): Name of the upstream remote to set as default, or None - - Raises: - ValueError if more than one name matches (should not happen); - database is rolled back - """ - self.execute("UPDATE upstream SET is_default = 0") - if name is not None: - self.execute( - 'UPDATE upstream SET is_default = 1 WHERE name = ?', (name,)) - if self.rowcount() != 1: - self.rollback() - raise ValueError(f"No such upstream '{name}'") - - def upstream_get_default(self): - """Get the name of the default upstream - - Return: - str: Default-upstream name, or None if there is no default - """ - res = self.execute( - "SELECT name FROM upstream WHERE is_default = 1") - recs = res.fetchall() - if len(recs) != 1: - return None - return recs[0][0] - - def upstream_delete(self, name): - """Delete an upstream target - - Args: - name (str): Name of the upstream remote to delete - - Raises: - ValueError: Upstream does not exist (database is rolled back) - """ - self.execute(f"DELETE FROM upstream WHERE name = '{name}'") - if self.rowcount() != 1: - self.rollback() - raise ValueError(f"No such upstream '{name}'") - - def upstream_get_dict(self): - """Get a list of upstream entries from the database - - Return: - OrderedDict: - key (str): upstream name - value (str): url - """ - res = self.execute('SELECT name, url, is_default FROM upstream') - udict = OrderedDict() - for name, url, is_default in res.fetchall(): - udict[name] = url, is_default - return udict - - # settings functions - - def settings_update(self, name, proj_id, link_name): - """Set the patchwork settings of the project - - Args: - name (str): Name of the project to use in patchwork - proj_id (int): Project ID for the project - link_name (str): Link name for the project - """ - self.execute('DELETE FROM settings') - self.execute( - 'INSERT INTO settings (name, proj_id, link_name) ' - 'VALUES (?, ?, ?)', (name, proj_id, link_name)) - - def settings_get(self): - """Get the patchwork settings of the project - - Returns: - tuple or None if there are no settings: - name (str): Project name, e.g. 'U-Boot' - proj_id (int): Patchworks project ID for this project - link_name (str): Patchwork's link-name for the project - """ - res = self.execute("SELECT name, proj_id, link_name FROM settings") - recs = res.fetchall() - if len(recs) != 1: - return None - return recs[0] |
