xref: /openbmc/openbmc/poky/bitbake/lib/prserv/db.py (revision f1e5d6968976c2341c6d554bfcc8895f1b33c26b)
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