mirror of
https://git.yoctoproject.org/poky
synced 2026-06-14 17:29:59 +00:00
65757c9e20
sqlite3 can allow multiple processes to access the database simultaneously, but it must be opened correctly. The key change is that the database is no longer opened in "exclusive" mode (defaulting to shared mode). In addition, the journal is set to "WAL" mode, as this is the most efficient for dealing with simultaneous access between different processes. In order to keep the database performance, synchronous mode is set to "off". The WAL journal will protect against incomplete transactions in any given client, however the database will not be protected against unexpected power loss from the OS (which is a fine trade off for performance, and also the same as the previous implementation). The use of a database cursor enabled to remove the _execute() wrapper. The cursor automatically makes sure that the query happens in an atomic transaction and commits when finished. This also removes the need for a "dirty" flag for the database and for explicit database syncing, which simplifies the code. (Bitbake rev: 385833243c495dc68ec26a963136c1ced3f272d0) Signed-off-by: Michael Opdenacker <michael.opdenacker@bootlin.com> Signed-off-by: Joshua Watt <JPEWhacker@gmail.com> Cc: Tim Orling <ticotimo@gmail.com> Cc: Thomas Petazzoni <thomas.petazzoni@bootlin.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
353 lines
14 KiB
Python
353 lines
14 KiB
Python
#
|
|
# Copyright BitBake Contributors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-only
|
|
#
|
|
|
|
import logging
|
|
import os.path
|
|
import errno
|
|
import prserv
|
|
import sqlite3
|
|
|
|
from contextlib import closing
|
|
from . import increase_revision, revision_greater, revision_smaller
|
|
|
|
logger = logging.getLogger("BitBake.PRserv")
|
|
|
|
#
|
|
# "No History" mode - for a given query tuple (version, pkgarch, checksum),
|
|
# the returned value will be the largest among all the values of the same
|
|
# (version, pkgarch). This means the PR value returned can NOT be decremented.
|
|
#
|
|
# "History" mode - Return a new higher value for previously unseen query
|
|
# tuple (version, pkgarch, checksum), otherwise return historical value.
|
|
# Value can decrement if returning to a previous build.
|
|
|
|
class PRTable(object):
|
|
def __init__(self, conn, table, read_only):
|
|
self.conn = conn
|
|
self.read_only = read_only
|
|
self.table = table
|
|
|
|
with closing(self.conn.cursor()) as cursor:
|
|
if self.read_only:
|
|
table_exists = cursor.execute(
|
|
"SELECT count(*) FROM sqlite_master \
|
|
WHERE type='table' AND name='%s'" % (self.table))
|
|
if not table_exists:
|
|
raise prserv.NotFoundError
|
|
else:
|
|
cursor.execute("CREATE TABLE IF NOT EXISTS %s \
|
|
(version TEXT NOT NULL, \
|
|
pkgarch TEXT NOT NULL, \
|
|
checksum TEXT NOT NULL, \
|
|
value TEXT, \
|
|
PRIMARY KEY (version, pkgarch, checksum, value));" % self.table)
|
|
self.conn.commit()
|
|
|
|
def _extremum_value(self, rows, is_max):
|
|
value = None
|
|
|
|
for row in rows:
|
|
current_value = row[0]
|
|
if value is None:
|
|
value = current_value
|
|
else:
|
|
if is_max:
|
|
is_new_extremum = revision_greater(current_value, value)
|
|
else:
|
|
is_new_extremum = revision_smaller(current_value, value)
|
|
if is_new_extremum:
|
|
value = current_value
|
|
return value
|
|
|
|
def _max_value(self, rows):
|
|
return self._extremum_value(rows, True)
|
|
|
|
def _min_value(self, rows):
|
|
return self._extremum_value(rows, False)
|
|
|
|
def test_package(self, version, pkgarch):
|
|
"""Returns whether the specified package version is found in the database for the specified architecture"""
|
|
|
|
# Just returns the value if found or None otherwise
|
|
with closing(self.conn.cursor()) as cursor:
|
|
data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=?;" % self.table,
|
|
(version, pkgarch))
|
|
row=data.fetchone()
|
|
if row is not None:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def test_value(self, version, pkgarch, value):
|
|
"""Returns whether the specified value is found in the database for the specified package and architecture"""
|
|
|
|
# Just returns the value if found or None otherwise
|
|
with closing(self.conn.cursor()) as cursor:
|
|
data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? and value=?;" % self.table,
|
|
(version, pkgarch, value))
|
|
row=data.fetchone()
|
|
if row is not None:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def find_package_max_value(self, version, pkgarch):
|
|
"""Returns the greatest value for (version, pkgarch), or None if not found. Doesn't create a new value"""
|
|
|
|
with closing(self.conn.cursor()) as cursor:
|
|
data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=?;" % (self.table),
|
|
(version, pkgarch))
|
|
rows = data.fetchall()
|
|
value = self._max_value(rows)
|
|
return value
|
|
|
|
def find_value(self, version, pkgarch, checksum, history=False):
|
|
"""Returns the value for the specified checksum if found or None otherwise."""
|
|
|
|
if history:
|
|
return self.find_min_value(version, pkgarch, checksum)
|
|
else:
|
|
return self.find_max_value(version, pkgarch, checksum)
|
|
|
|
|
|
def _find_extremum_value(self, version, pkgarch, checksum, is_max):
|
|
"""Returns the maximum (if is_max is True) or minimum (if is_max is False) value
|
|
for (version, pkgarch, checksum), or None if not found. Doesn't create a new value"""
|
|
|
|
with closing(self.conn.cursor()) as cursor:
|
|
data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND checksum=?;" % (self.table),
|
|
(version, pkgarch, checksum))
|
|
rows = data.fetchall()
|
|
return self._extremum_value(rows, is_max)
|
|
|
|
def find_max_value(self, version, pkgarch, checksum):
|
|
return self._find_extremum_value(version, pkgarch, checksum, True)
|
|
|
|
def find_min_value(self, version, pkgarch, checksum):
|
|
return self._find_extremum_value(version, pkgarch, checksum, False)
|
|
|
|
def find_new_subvalue(self, version, pkgarch, base):
|
|
"""Take and increase the greatest "<base>.y" value for (version, pkgarch), or return "<base>.0" if not found.
|
|
This doesn't store a new value."""
|
|
|
|
with closing(self.conn.cursor()) as cursor:
|
|
data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND value LIKE '%s.%%';" % (self.table, base),
|
|
(version, pkgarch))
|
|
rows = data.fetchall()
|
|
value = self._max_value(rows)
|
|
|
|
if value is not None:
|
|
return increase_revision(value)
|
|
else:
|
|
return base + ".0"
|
|
|
|
def store_value(self, version, pkgarch, checksum, value):
|
|
"""Store new value in the database"""
|
|
|
|
with closing(self.conn.cursor()) as cursor:
|
|
try:
|
|
cursor.execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table),
|
|
(version, pkgarch, checksum, value))
|
|
except sqlite3.IntegrityError as exc:
|
|
logger.error(str(exc))
|
|
self.conn.commit()
|
|
|
|
def _get_value(self, version, pkgarch, checksum, history):
|
|
|
|
max_value = self.find_package_max_value(version, pkgarch)
|
|
|
|
if max_value is None:
|
|
# version, pkgarch completely unknown. Return initial value.
|
|
return "0"
|
|
|
|
value = self.find_value(version, pkgarch, checksum, history)
|
|
|
|
if value is None:
|
|
# version, pkgarch found but not checksum. Create a new value from the maximum one
|
|
return increase_revision(max_value)
|
|
|
|
if history:
|
|
return value
|
|
|
|
# "no history" mode - If the value is not the maximum value for the package, need to increase it.
|
|
if max_value > value:
|
|
return increase_revision(max_value)
|
|
else:
|
|
return value
|
|
|
|
def get_value(self, version, pkgarch, checksum, history):
|
|
value = self._get_value(version, pkgarch, checksum, history)
|
|
if not self.read_only:
|
|
self.store_value(version, pkgarch, checksum, value)
|
|
return value
|
|
|
|
def _import_hist(self, version, pkgarch, checksum, value):
|
|
if self.read_only:
|
|
return None
|
|
|
|
val = None
|
|
with closing(self.conn.cursor()) as cursor:
|
|
data = cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? AND checksum=?;" % self.table,
|
|
(version, pkgarch, checksum))
|
|
row = data.fetchone()
|
|
if row is not None:
|
|
val=row[0]
|
|
else:
|
|
#no value found, try to insert
|
|
try:
|
|
cursor.execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table),
|
|
(version, pkgarch, checksum, value))
|
|
except sqlite3.IntegrityError as exc:
|
|
logger.error(str(exc))
|
|
|
|
self.conn.commit()
|
|
|
|
data = cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? AND checksum=?;" % self.table,
|
|
(version, pkgarch, checksum))
|
|
row = data.fetchone()
|
|
if row is not None:
|
|
val = row[0]
|
|
return val
|
|
|
|
def _import_no_hist(self, version, pkgarch, checksum, value):
|
|
if self.read_only:
|
|
return None
|
|
|
|
with closing(self.conn.cursor()) as cursor:
|
|
try:
|
|
#try to insert
|
|
cursor.execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table),
|
|
(version, pkgarch, checksum, value))
|
|
except sqlite3.IntegrityError as exc:
|
|
#already have the record, try to update
|
|
try:
|
|
cursor.execute("UPDATE %s SET value=? WHERE version=? AND pkgarch=? AND checksum=? AND value<?"
|
|
% (self.table),
|
|
(value, version, pkgarch, checksum, value))
|
|
except sqlite3.IntegrityError as exc:
|
|
logger.error(str(exc))
|
|
|
|
self.conn.commit()
|
|
|
|
data = cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? AND checksum=? AND value>=?;" % self.table,
|
|
(version, pkgarch, checksum, value))
|
|
row=data.fetchone()
|
|
if row is not None:
|
|
return row[0]
|
|
else:
|
|
return None
|
|
|
|
def importone(self, version, pkgarch, checksum, value, history=False):
|
|
if history:
|
|
return self._import_hist(version, pkgarch, checksum, value)
|
|
else:
|
|
return self._import_no_hist(version, pkgarch, checksum, value)
|
|
|
|
def export(self, version, pkgarch, checksum, colinfo, history=False):
|
|
metainfo = {}
|
|
with closing(self.conn.cursor()) as cursor:
|
|
#column info
|
|
if colinfo:
|
|
metainfo["tbl_name"] = self.table
|
|
metainfo["core_ver"] = prserv.__version__
|
|
metainfo["col_info"] = []
|
|
data = cursor.execute("PRAGMA table_info(%s);" % self.table)
|
|
for row in data:
|
|
col = {}
|
|
col["name"] = row["name"]
|
|
col["type"] = row["type"]
|
|
col["notnull"] = row["notnull"]
|
|
col["dflt_value"] = row["dflt_value"]
|
|
col["pk"] = row["pk"]
|
|
metainfo["col_info"].append(col)
|
|
|
|
#data info
|
|
datainfo = []
|
|
|
|
if history:
|
|
sqlstmt = "SELECT * FROM %s as T1 WHERE 1=1 " % self.table
|
|
else:
|
|
sqlstmt = "SELECT T1.version, T1.pkgarch, T1.checksum, T1.value FROM %s as T1, \
|
|
(SELECT version, pkgarch, max(value) as maxvalue FROM %s GROUP BY version, pkgarch) as T2 \
|
|
WHERE T1.version=T2.version AND T1.pkgarch=T2.pkgarch AND T1.value=T2.maxvalue " % (self.table, self.table)
|
|
sqlarg = []
|
|
where = ""
|
|
if version:
|
|
where += "AND T1.version=? "
|
|
sqlarg.append(str(version))
|
|
if pkgarch:
|
|
where += "AND T1.pkgarch=? "
|
|
sqlarg.append(str(pkgarch))
|
|
if checksum:
|
|
where += "AND T1.checksum=? "
|
|
sqlarg.append(str(checksum))
|
|
|
|
sqlstmt += where + ";"
|
|
|
|
if len(sqlarg):
|
|
data = cursor.execute(sqlstmt, tuple(sqlarg))
|
|
else:
|
|
data = cursor.execute(sqlstmt)
|
|
for row in data:
|
|
if row["version"]:
|
|
col = {}
|
|
col["version"] = row["version"]
|
|
col["pkgarch"] = row["pkgarch"]
|
|
col["checksum"] = row["checksum"]
|
|
col["value"] = row["value"]
|
|
datainfo.append(col)
|
|
return (metainfo, datainfo)
|
|
|
|
def dump_db(self, fd):
|
|
writeCount = 0
|
|
for line in self.conn.iterdump():
|
|
writeCount = writeCount + len(line) + 1
|
|
fd.write(line)
|
|
fd.write("\n")
|
|
return writeCount
|
|
|
|
class PRData(object):
|
|
"""Object representing the PR database"""
|
|
def __init__(self, filename, read_only=False):
|
|
self.filename=os.path.abspath(filename)
|
|
self.read_only = read_only
|
|
#build directory hierarchy
|
|
try:
|
|
os.makedirs(os.path.dirname(self.filename))
|
|
except OSError as e:
|
|
if e.errno != errno.EEXIST:
|
|
raise e
|
|
uri = "file:%s%s" % (self.filename, "?mode=ro" if self.read_only else "")
|
|
logger.debug("Opening PRServ database '%s'" % (uri))
|
|
self.connection=sqlite3.connect(uri, uri=True)
|
|
self.connection.row_factory=sqlite3.Row
|
|
self.connection.execute("PRAGMA synchronous = OFF;")
|
|
self.connection.execute("PRAGMA journal_mode = WAL;")
|
|
self.connection.commit()
|
|
self._tables={}
|
|
|
|
def disconnect(self):
|
|
self.connection.commit()
|
|
self.connection.close()
|
|
|
|
def __getitem__(self, tblname):
|
|
if not isinstance(tblname, str):
|
|
raise TypeError("tblname argument must be a string, not '%s'" %
|
|
type(tblname))
|
|
if tblname in self._tables:
|
|
return self._tables[tblname]
|
|
else:
|
|
tableobj = self._tables[tblname] = PRTable(self.connection, tblname, self.read_only)
|
|
return tableobj
|
|
|
|
def __delitem__(self, tblname):
|
|
if tblname in self._tables:
|
|
del self._tables[tblname]
|
|
logger.info("drop table %s" % (tblname))
|
|
self.connection.execute("DROP TABLE IF EXISTS %s;" % tblname)
|
|
self.connection.commit()
|