1# 2# Copyright BitBake Contributors 3# 4# SPDX-License-Identifier: GPL-2.0-only 5# 6 7import logging 8import os.path 9import errno 10import prserv 11import sqlite3 12 13from contextlib import closing 14from . import increase_revision, revision_greater, revision_smaller 15 16logger = logging.getLogger("BitBake.PRserv") 17 18# 19# "No History" mode - for a given query tuple (version, pkgarch, checksum), 20# the returned value will be the largest among all the values of the same 21# (version, pkgarch). This means the PR value returned can NOT be decremented. 22# 23# "History" mode - Return a new higher value for previously unseen query 24# tuple (version, pkgarch, checksum), otherwise return historical value. 25# Value can decrement if returning to a previous build. 26 27class PRTable(object): 28 def __init__(self, conn, table, read_only): 29 self.conn = conn 30 self.read_only = read_only 31 self.table = table 32 33 # Creating the table even if the server is read-only. 34 # This avoids a race condition if a shared database 35 # is accessed by a read-only server first. 36 37 with closing(self.conn.cursor()) as cursor: 38 cursor.execute("CREATE TABLE IF NOT EXISTS %s \ 39 (version TEXT NOT NULL, \ 40 pkgarch TEXT NOT NULL, \ 41 checksum TEXT NOT NULL, \ 42 value TEXT, \ 43 PRIMARY KEY (version, pkgarch, checksum, value));" % self.table) 44 self.conn.commit() 45 46 def _extremum_value(self, rows, is_max): 47 value = None 48 49 for row in rows: 50 current_value = row[0] 51 if value is None: 52 value = current_value 53 else: 54 if is_max: 55 is_new_extremum = revision_greater(current_value, value) 56 else: 57 is_new_extremum = revision_smaller(current_value, value) 58 if is_new_extremum: 59 value = current_value 60 return value 61 62 def _max_value(self, rows): 63 return self._extremum_value(rows, True) 64 65 def _min_value(self, rows): 66 return self._extremum_value(rows, False) 67 68 def test_package(self, version, pkgarch): 69 """Returns whether the specified package version is found in the database for the specified architecture""" 70 71 # Just returns the value if found or None otherwise 72 with closing(self.conn.cursor()) as cursor: 73 data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=?;" % self.table, 74 (version, pkgarch)) 75 row=data.fetchone() 76 if row is not None: 77 return True 78 else: 79 return False 80 81 def test_checksum_value(self, version, pkgarch, checksum, value): 82 """Returns whether the specified value is found in the database for the specified package, architecture and checksum""" 83 84 with closing(self.conn.cursor()) as cursor: 85 data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? and checksum=? and value=?;" % self.table, 86 (version, pkgarch, checksum, value)) 87 row=data.fetchone() 88 if row is not None: 89 return True 90 else: 91 return False 92 93 def test_value(self, version, pkgarch, value): 94 """Returns whether the specified value is found in the database for the specified package and architecture""" 95 96 # Just returns the value if found or None otherwise 97 with closing(self.conn.cursor()) as cursor: 98 data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? and value=?;" % self.table, 99 (version, pkgarch, value)) 100 row=data.fetchone() 101 if row is not None: 102 return True 103 else: 104 return False 105 106 107 def find_package_max_value(self, version, pkgarch): 108 """Returns the greatest value for (version, pkgarch), or None if not found. Doesn't create a new value""" 109 110 with closing(self.conn.cursor()) as cursor: 111 data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=?;" % (self.table), 112 (version, pkgarch)) 113 rows = data.fetchall() 114 value = self._max_value(rows) 115 return value 116 117 def find_value(self, version, pkgarch, checksum, history=False): 118 """Returns the value for the specified checksum if found or None otherwise.""" 119 120 if history: 121 return self.find_min_value(version, pkgarch, checksum) 122 else: 123 return self.find_max_value(version, pkgarch, checksum) 124 125 126 def _find_extremum_value(self, version, pkgarch, checksum, is_max): 127 """Returns the maximum (if is_max is True) or minimum (if is_max is False) value 128 for (version, pkgarch, checksum), or None if not found. Doesn't create a new value""" 129 130 with closing(self.conn.cursor()) as cursor: 131 data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND checksum=?;" % (self.table), 132 (version, pkgarch, checksum)) 133 rows = data.fetchall() 134 return self._extremum_value(rows, is_max) 135 136 def find_max_value(self, version, pkgarch, checksum): 137 return self._find_extremum_value(version, pkgarch, checksum, True) 138 139 def find_min_value(self, version, pkgarch, checksum): 140 return self._find_extremum_value(version, pkgarch, checksum, False) 141 142 def find_new_subvalue(self, version, pkgarch, base): 143 """Take and increase the greatest "<base>.y" value for (version, pkgarch), or return "<base>.0" if not found. 144 This doesn't store a new value.""" 145 146 with closing(self.conn.cursor()) as cursor: 147 data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND value LIKE '%s.%%';" % (self.table, base), 148 (version, pkgarch)) 149 rows = data.fetchall() 150 value = self._max_value(rows) 151 152 if value is not None: 153 return increase_revision(value) 154 else: 155 return base + ".0" 156 157 def store_value(self, version, pkgarch, checksum, value): 158 """Store value in the database""" 159 160 if not self.read_only and not self.test_checksum_value(version, pkgarch, checksum, value): 161 with closing(self.conn.cursor()) as cursor: 162 cursor.execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table), 163 (version, pkgarch, checksum, value)) 164 self.conn.commit() 165 166 def _get_value(self, version, pkgarch, checksum, history): 167 168 max_value = self.find_package_max_value(version, pkgarch) 169 170 if max_value is None: 171 # version, pkgarch completely unknown. Return initial value. 172 return "0" 173 174 value = self.find_value(version, pkgarch, checksum, history) 175 176 if value is None: 177 # version, pkgarch found but not checksum. Create a new value from the maximum one 178 return increase_revision(max_value) 179 180 if history: 181 return value 182 183 # "no history" mode - If the value is not the maximum value for the package, need to increase it. 184 if max_value > value: 185 return increase_revision(max_value) 186 else: 187 return value 188 189 def get_value(self, version, pkgarch, checksum, history): 190 value = self._get_value(version, pkgarch, checksum, history) 191 if not self.read_only: 192 self.store_value(version, pkgarch, checksum, value) 193 return value 194 195 def importone(self, version, pkgarch, checksum, value): 196 self.store_value(version, pkgarch, checksum, value) 197 return value 198 199 def export(self, version, pkgarch, checksum, colinfo, history=False): 200 metainfo = {} 201 with closing(self.conn.cursor()) as cursor: 202 #column info 203 if colinfo: 204 metainfo["tbl_name"] = self.table 205 metainfo["core_ver"] = prserv.__version__ 206 metainfo["col_info"] = [] 207 data = cursor.execute("PRAGMA table_info(%s);" % self.table) 208 for row in data: 209 col = {} 210 col["name"] = row["name"] 211 col["type"] = row["type"] 212 col["notnull"] = row["notnull"] 213 col["dflt_value"] = row["dflt_value"] 214 col["pk"] = row["pk"] 215 metainfo["col_info"].append(col) 216 217 #data info 218 datainfo = [] 219 220 if history: 221 sqlstmt = "SELECT * FROM %s as T1 WHERE 1=1 " % self.table 222 else: 223 sqlstmt = "SELECT T1.version, T1.pkgarch, T1.checksum, T1.value FROM %s as T1, \ 224 (SELECT version, pkgarch, max(value) as maxvalue FROM %s GROUP BY version, pkgarch) as T2 \ 225 WHERE T1.version=T2.version AND T1.pkgarch=T2.pkgarch AND T1.value=T2.maxvalue " % (self.table, self.table) 226 sqlarg = [] 227 where = "" 228 if version: 229 where += "AND T1.version=? " 230 sqlarg.append(str(version)) 231 if pkgarch: 232 where += "AND T1.pkgarch=? " 233 sqlarg.append(str(pkgarch)) 234 if checksum: 235 where += "AND T1.checksum=? " 236 sqlarg.append(str(checksum)) 237 238 sqlstmt += where + ";" 239 240 if len(sqlarg): 241 data = cursor.execute(sqlstmt, tuple(sqlarg)) 242 else: 243 data = cursor.execute(sqlstmt) 244 for row in data: 245 if row["version"]: 246 col = {} 247 col["version"] = row["version"] 248 col["pkgarch"] = row["pkgarch"] 249 col["checksum"] = row["checksum"] 250 col["value"] = row["value"] 251 datainfo.append(col) 252 return (metainfo, datainfo) 253 254 def dump_db(self, fd): 255 writeCount = 0 256 for line in self.conn.iterdump(): 257 writeCount = writeCount + len(line) + 1 258 fd.write(line) 259 fd.write("\n") 260 return writeCount 261 262class PRData(object): 263 """Object representing the PR database""" 264 def __init__(self, filename, read_only=False): 265 self.filename=os.path.abspath(filename) 266 self.read_only = read_only 267 #build directory hierarchy 268 try: 269 os.makedirs(os.path.dirname(self.filename)) 270 except OSError as e: 271 if e.errno != errno.EEXIST: 272 raise e 273 uri = "file:%s%s" % (self.filename, "?mode=ro" if self.read_only else "") 274 logger.debug("Opening PRServ database '%s'" % (uri)) 275 self.connection=sqlite3.connect(uri, uri=True) 276 self.connection.row_factory=sqlite3.Row 277 self.connection.execute("PRAGMA synchronous = OFF;") 278 self.connection.execute("PRAGMA journal_mode = WAL;") 279 self.connection.commit() 280 self._tables={} 281 282 def disconnect(self): 283 self.connection.commit() 284 self.connection.close() 285 286 def __getitem__(self, tblname): 287 if not isinstance(tblname, str): 288 raise TypeError("tblname argument must be a string, not '%s'" % 289 type(tblname)) 290 if tblname in self._tables: 291 return self._tables[tblname] 292 else: 293 tableobj = self._tables[tblname] = PRTable(self.connection, tblname, self.read_only) 294 return tableobj 295 296 def __delitem__(self, tblname): 297 if tblname in self._tables: 298 del self._tables[tblname] 299 logger.info("drop table %s" % (tblname)) 300 self.connection.execute("DROP TABLE IF EXISTS %s;" % tblname) 301 self.connection.commit() 302