#
# 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
# Creating the table even if the server is read-only.
# This avoids a race condition if a shared database
# is accessed by a read-only server first.
with closing(self.conn.cursor()) as cursor:
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_checksum_value(self, version, pkgarch, checksum, value):
"""Returns whether the specified value is found in the database for the specified package, architecture and checksum"""
with closing(self.conn.cursor()) as cursor:
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 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 ".y" value for (version, pkgarch), or return ".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 value in the database"""
if not self.read_only and not self.test_checksum_value(version, pkgarch, checksum, value):
with closing(self.conn.cursor()) as cursor:
cursor.execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table),
(version, pkgarch, checksum, value))
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 importone(self, version, pkgarch, checksum, value):
self.store_value(version, pkgarch, checksum, value)
return 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()