← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:db-plpython3 into launchpad:db-devel

 

Colin Watson has proposed merging ~cjwatson/launchpad:db-plpython3 into launchpad:db-devel.

Commit message:
Replace PL/Python 2 functions using PL/Python 3

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/418572

This is a prerequisite for bootstrapping Launchpad on Ubuntu 20.04, where PL/Python 2 is unavailable.

The effective diff introduced by this commit is as follows: https://paste.ubuntu.com/p/Q8wWqxG5dn/
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:db-plpython3 into launchpad:db-devel.
diff --git a/database/schema/patch-2210-46-0.sql b/database/schema/patch-2210-46-0.sql
new file mode 100644
index 0000000..1ae0f4f
--- /dev/null
+++ b/database/schema/patch-2210-46-0.sql
@@ -0,0 +1,765 @@
+-- Copyright 2021 Canonical Ltd.  This software is licensed under the
+-- GNU Affero General Public License version 3 (see the file LICENSE).
+
+SET client_min_messages=ERROR;
+
+-- Replace all PL/Python2 functions with PL/Python3 equivalents.
+
+CREATE EXTENSION IF NOT EXISTS plpython3u WITH SCHEMA pg_catalog;
+
+COMMENT ON EXTENSION plpython3u IS 'PL/Python3U untrusted procedural language';
+
+CREATE OR REPLACE FUNCTION _ftq(text) RETURNS text
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $_$
+        import re
+
+        # I think this method would be more robust if we used a real
+        # tokenizer and parser to generate the query string, but we need
+        # something suitable for use as a stored procedure which currently
+        # means no external dependencies.
+
+        query = args[0]
+        ## plpy.debug('1 query is %s' % repr(query))
+
+        # Replace tsquery operators with ' '. '<' begins all the phrase
+        # search operators, and a standalone '>' is fine.
+        query = re.sub('[|&!<]', ' ', query)
+
+        # Normalize whitespace
+        query = re.sub("\s+"," ", query)
+
+        # Convert AND, OR, NOT to tsearch2 punctuation
+        query = re.sub(r"\bAND\b", "&", query)
+        query = re.sub(r"\bOR\b", "|", query)
+        query = re.sub(r"\bNOT\b", " !", query)
+        ## plpy.debug('2 query is %s' % repr(query))
+
+        # Deal with unwanted punctuation.
+        # ':' is used in queries to specify a weight of a word.
+        # '\' is treated differently in to_tsvector() and to_tsquery().
+        punctuation = r'[:\\]'
+        query = re.sub(r"%s+" % (punctuation,), " ", query)
+        ## plpy.debug('3 query is %s' % repr(query))
+
+        # Now that we have handle case sensitive booleans, convert to lowercase
+        query = query.lower()
+
+        # Remove unpartnered bracket on the left and right
+        query = re.sub(r"(?ux) ^ ( [^(]* ) \)", r"(\1)", query)
+        query = re.sub(r"(?ux) \( ( [^)]* ) $", r"(\1)", query)
+
+        # Remove spurious brackets
+        query = re.sub(r"\(([^\&\|]*?)\)", r" \1 ", query)
+        ## plpy.debug('5 query is %s' % repr(query))
+
+        # Insert & between tokens without an existing boolean operator
+        # ( not proceeded by (|&!
+        query = re.sub(r"(?<![\(\|\&\!])\s*\(", "&(", query)
+        ## plpy.debug('6 query is %s' % repr(query))
+        # ) not followed by )|&
+        query = re.sub(r"\)(?!\s*(\)|\||\&|\s*$))", ")&", query)
+        ## plpy.debug('6.1 query is %s' % repr(query))
+        # Whitespace not proceded by (|&! not followed by &|
+        query = re.sub(r"(?<![\(\|\&\!\s])\s+(?![\&\|\s])", "&", query)
+        ## plpy.debug('7 query is %s' % repr(query))
+
+        # Detect and repair syntax errors - we are lenient because
+        # this input is generally from users.
+
+        # Fix unbalanced brackets
+        openings = query.count("(")
+        closings = query.count(")")
+        if openings > closings:
+            query = query + " ) "*(openings-closings)
+        elif closings > openings:
+            query = " ( "*(closings-openings) + query
+        ## plpy.debug('8 query is %s' % repr(query))
+
+        # Strip ' character that do not have letters on both sides
+        query = re.sub(r"((?<!\w)'|'(?!\w))", "", query)
+
+        # Brackets containing nothing but whitespace and booleans, recursive
+        last = ""
+        while last != query:
+            last = query
+            query = re.sub(r"\([\s\&\|\!]*\)", "", query)
+        ## plpy.debug('9 query is %s' % repr(query))
+
+        # An & or | following a (
+        query = re.sub(r"(?<=\()[\&\|\s]+", "", query)
+        ## plpy.debug('10 query is %s' % repr(query))
+
+        # An &, | or ! immediatly before a )
+        query = re.sub(r"[\&\|\!\s]*[\&\|\!]+\s*(?=\))", "", query)
+        ## plpy.debug('11 query is %s' % repr(query))
+
+        # An &,| or ! followed by another boolean.
+        query = re.sub(r"(?ux) \s* ( [\&\|\!] ) [\s\&\|]+", r"\1", query)
+        ## plpy.debug('12 query is %s' % repr(query))
+
+        # Leading & or |
+        query = re.sub(r"^[\s\&\|]+", "", query)
+        ## plpy.debug('13 query is %s' % repr(query))
+
+        # Trailing &, | or !
+        query = re.sub(r"[\&\|\!\s]+$", "", query)
+        ## plpy.debug('14 query is %s' % repr(query))
+
+        # If we have nothing but whitespace and tsearch2 operators,
+        # return NULL.
+        if re.search(r"^[\&\|\!\s\(\)]*$", query) is not None:
+            return None
+
+        ## plpy.debug('15 query is %s' % repr(query))
+
+        return query or None
+        $_$;
+
+CREATE OR REPLACE FUNCTION assert_patch_applied(major integer, minor integer, patch integer) RETURNS boolean
+    LANGUAGE plpython3u STABLE
+    AS $$
+    rv = plpy.execute("""
+        SELECT * FROM LaunchpadDatabaseRevision
+        WHERE major=%d AND minor=%d AND patch=%d
+        """ % (major, minor, patch))
+    if len(rv) == 0:
+        raise Exception(
+            'patch-%d-%02d-%d not applied.' % (major, minor, patch))
+    else:
+        return True
+$$;
+
+CREATE OR REPLACE FUNCTION valid_bug_name(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $_$
+    import re
+    name = args[0]
+    pat = r"^[a-z][a-z0-9+\.\-]+$"
+    if re.match(pat, name):
+        return 1
+    return 0
+$_$;
+
+CREATE OR REPLACE FUNCTION valid_name(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    import re
+    name = args[0]
+    pat = r"^[a-z0-9][a-z0-9\+\.\-]*\Z"
+    if re.match(pat, name):
+        return 1
+    return 0
+$$;
+
+CREATE OR REPLACE FUNCTION debversion_sort_key(version text) RETURNS text
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $_$
+    # If this method is altered, then any functional indexes using it
+    # need to be rebuilt.
+    import re
+
+    VERRE = re.compile("(?:([0-9]+):)?(.+?)(?:-([^-]+))?$")
+
+    MAP = "0123456789ABCDEFGHIJKLMNOPQRSTUV"
+
+    epoch, version, release = VERRE.match(args[0]).groups()
+    key = []
+    for part, part_weight in ((epoch, 3000), (version, 2000), (release, 1000)):
+        if not part:
+            continue
+        i = 0
+        l = len(part)
+        while i != l:
+            c = part[i]
+            if c.isdigit():
+                key.append(part_weight)
+                j = i
+                while i != l and part[i].isdigit(): i += 1
+                key.append(part_weight+int(part[j:i] or "0"))
+            elif c == "~":
+                key.append(0)
+                i += 1
+            elif c.isalpha():
+                key.append(part_weight+ord(c))
+                i += 1
+            else:
+                key.append(part_weight+256+ord(c))
+                i += 1
+        if not key or key[-1] != part_weight:
+            key.append(part_weight)
+            key.append(part_weight)
+    key.append(1)
+
+    # Encode our key and return it
+    #
+    result = []
+    for value in key:
+        if not value:
+            result.append("000")
+        else:
+            element = []
+            while value:
+                element.insert(0, MAP[value & 0x1F])
+                value >>= 5
+            element_len = len(element)
+            if element_len < 3:
+                element.insert(0, "0"*(3-element_len))
+            elif element_len == 3:
+                pass
+            elif element_len < 35:
+                element.insert(0, MAP[element_len-4])
+                element.insert(0, "X")
+            elif element_len < 1027:
+                element.insert(0, MAP[(element_len-4) & 0x1F])
+                element.insert(0, MAP[(element_len-4) & 0x3E0])
+                element.insert(0, "Y")
+            else:
+                raise ValueError("Number too large")
+            result.extend(element)
+    return "".join(result)
+$_$;
+
+CREATE OR REPLACE FUNCTION ftiupdate() RETURNS trigger
+    LANGUAGE plpython3u
+    AS $_$
+    new = TD["new"]
+    args = TD["args"][:]
+
+    # Short circuit if none of the relevant columns have been
+    # modified and fti is not being set to NULL (setting the fti
+    # column to NULL is thus how we can force a rebuild of the fti
+    # column).
+    if TD["event"] == "UPDATE" and new["fti"] != None:
+        old = TD["old"]
+        relevant_modification = False
+        for column_name in args[::2]:
+            if new[column_name] != old[column_name]:
+                relevant_modification = True
+                break
+        if not relevant_modification:
+            return "OK"
+
+    # Generate an SQL statement that turns the requested
+    # column values into a weighted tsvector
+    sql = []
+    for i in range(0, len(args), 2):
+        sql.append(
+                "setweight(to_tsvector('default', coalesce("
+                "substring(ltrim($%d) from 1 for 2500),'')),"
+                "CAST($%d AS \"char\"))" % (i + 1, i + 2))
+        args[i] = new[args[i]]
+
+    sql = "SELECT %s AS fti" % "||".join(sql)
+
+    # Execute and store in the fti column
+    plan = plpy.prepare(sql, ["text", "char"] * (len(args) // 2))
+    new["fti"] = plpy.execute(plan, args, 1)[0]["fti"]
+
+    # Tell PostgreSQL we have modified the data
+    return "MODIFY"
+$_$;
+
+CREATE OR REPLACE FUNCTION ftq(text) RETURNS tsquery
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $_$
+        p = plpy.prepare(
+            "SELECT to_tsquery('default', _ftq($1)) AS x", ["text"])
+        query = plpy.execute(p, args, 1)[0]["x"]
+        return query or None
+        $_$;
+
+CREATE OR REPLACE FUNCTION milestone_sort_key(dateexpected timestamp without time zone, name text) RETURNS text
+    LANGUAGE plpython3u IMMUTABLE
+    AS $$
+    # If this method is altered, then any functional indexes using it
+    # need to be rebuilt.
+    import re
+    import datetime
+
+    date_expected, name = args
+
+    def substitute_filled_numbers(match):
+        return match.group(0).zfill(5)
+
+    name = re.sub('\d+', substitute_filled_numbers, name)
+    if date_expected is None:
+        # NULL dates are considered to be in the future.
+        date_expected = datetime.datetime(datetime.MAXYEAR, 1, 1)
+    return '%s %s' % (date_expected, name)
+$$;
+
+CREATE OR REPLACE FUNCTION mv_validpersonorteamcache_emailaddress() RETURNS trigger
+    LANGUAGE plpython3u SECURITY DEFINER
+    AS $_$
+    # This trigger function keeps the ValidPersonOrTeamCache materialized
+    # view in sync when updates are made to the EmailAddress table.
+    # Note that if the corresponding person is a team, changes to this table
+    # have no effect.
+    PREF = 4 # Constant indicating preferred email address
+
+    if "delete_plan" not in SD:
+        param_types = ["int4"]
+
+        SD["is_team"] = plpy.prepare("""
+            SELECT teamowner IS NOT NULL AS is_team FROM Person WHERE id = $1
+            """, param_types)
+
+        SD["delete_plan"] = plpy.prepare("""
+            DELETE FROM ValidPersonOrTeamCache WHERE id = $1
+            """, param_types)
+
+        SD["insert_plan"] = plpy.prepare("""
+            INSERT INTO ValidPersonOrTeamCache (id) VALUES ($1)
+            """, param_types)
+
+        SD["maybe_insert_plan"] = plpy.prepare("""
+            INSERT INTO ValidPersonOrTeamCache (id)
+            SELECT Person.id
+            FROM Person
+                JOIN EmailAddress ON Person.id = EmailAddress.person
+                LEFT OUTER JOIN ValidPersonOrTeamCache
+                    ON Person.id = ValidPersonOrTeamCache.id
+            WHERE Person.id = $1
+                AND ValidPersonOrTeamCache.id IS NULL
+                AND status = %(PREF)d
+                AND merged IS NULL
+                -- AND password IS NOT NULL
+            """ % vars(), param_types)
+
+    def is_team(person_id):
+        """Return true if person_id corresponds to a team"""
+        if person_id is None:
+            return False
+        return plpy.execute(SD["is_team"], [person_id], 1)[0]["is_team"]
+
+    class NoneDict:
+        def __getitem__(self, key):
+            return None
+
+    old = TD["old"] or NoneDict()
+    new = TD["new"] or NoneDict()
+
+    #plpy.info("old.id     == %s" % old["id"])
+    #plpy.info("old.person == %s" % old["person"])
+    #plpy.info("old.status == %s" % old["status"])
+    #plpy.info("new.id     == %s" % new["id"])
+    #plpy.info("new.person == %s" % new["person"])
+    #plpy.info("new.status == %s" % new["status"])
+
+    # Short circuit if neither person nor status has changed
+    if old["person"] == new["person"] and old["status"] == new["status"]:
+        return
+
+    # Short circuit if we are not mucking around with preferred email
+    # addresses
+    if old["status"] != PREF and new["status"] != PREF:
+        return
+
+    # Note that we have a constraint ensuring that there is only one
+    # status == PREF email address per person at any point in time.
+    # This simplifies our logic, as we know that if old.status == PREF,
+    # old.person does not have any other preferred email addresses.
+    # Also if new.status == PREF, we know new.person previously did not
+    # have a preferred email address.
+
+    if old["person"] != new["person"]:
+        if old["status"] == PREF and not is_team(old["person"]):
+            # old.person is no longer valid, unless they are a team
+            plpy.execute(SD["delete_plan"], [old["person"]])
+        if new["status"] == PREF and not is_team(new["person"]):
+            # new["person"] is now valid, or unchanged if they are a team
+            plpy.execute(SD["insert_plan"], [new["person"]])
+
+    elif old["status"] == PREF and not is_team(old["person"]):
+        # No longer valid, or unchanged if they are a team
+        plpy.execute(SD["delete_plan"], [old["person"]])
+
+    elif new["status"] == PREF and not is_team(new["person"]):
+        # May now be valid, or unchanged if they are a team.
+        plpy.execute(SD["maybe_insert_plan"], [new["person"]])
+$_$;
+
+CREATE OR REPLACE FUNCTION mv_validpersonorteamcache_person() RETURNS trigger
+    LANGUAGE plpython3u SECURITY DEFINER
+    AS $_$
+    # This trigger function could be simplified by simply issuing
+    # one DELETE followed by one INSERT statement. However, we want to minimize
+    # expensive writes so we use this more complex logic.
+    PREF = 4 # Constant indicating preferred email address
+
+    if "delete_plan" not in SD:
+        param_types = ["int4"]
+
+        SD["delete_plan"] = plpy.prepare("""
+            DELETE FROM ValidPersonOrTeamCache WHERE id = $1
+            """, param_types)
+
+        SD["maybe_insert_plan"] = plpy.prepare("""
+            INSERT INTO ValidPersonOrTeamCache (id)
+            SELECT Person.id
+            FROM Person
+                LEFT OUTER JOIN EmailAddress
+                    ON Person.id = EmailAddress.person AND status = %(PREF)d
+                LEFT OUTER JOIN ValidPersonOrTeamCache
+                    ON Person.id = ValidPersonOrTeamCache.id
+            WHERE Person.id = $1
+                AND ValidPersonOrTeamCache.id IS NULL
+                AND merged IS NULL
+                AND (teamowner IS NOT NULL OR EmailAddress.id IS NOT NULL)
+            """ % vars(), param_types)
+
+    new = TD["new"]
+    old = TD["old"]
+
+    # We should always have new, as this is not a DELETE trigger
+    assert new is not None, 'New is None'
+
+    person_id = new["id"]
+    query_params = [person_id] # All the same
+
+    # Short circuit if this is a new person (not team), as it cannot
+    # be valid until a status == 4 EmailAddress entry has been created
+    # (unless it is a team, in which case it is valid on creation)
+    if old is None:
+        if new["teamowner"] is not None:
+            plpy.execute(SD["maybe_insert_plan"], query_params)
+        return
+
+    # Short circuit if there are no relevant changes
+    if (new["teamowner"] == old["teamowner"]
+        and new["merged"] == old["merged"]):
+        return
+
+    # This function is only dealing with updates to the Person table.
+    # This means we do not have to worry about EmailAddress changes here
+
+    if (new["merged"] is not None or new["teamowner"] is None):
+        plpy.execute(SD["delete_plan"], query_params)
+    else:
+        plpy.execute(SD["maybe_insert_plan"], query_params)
+$_$;
+
+CREATE OR REPLACE FUNCTION name_blacklist_match(text, integer) RETURNS integer
+    LANGUAGE plpython3u STABLE STRICT SECURITY DEFINER
+    SET search_path TO 'public'
+    AS $_$
+    import re
+    name = args[0]
+    user_id = args[1]
+
+    # Initialize shared storage, shared between invocations.
+    if "regexp_select_plan" not in SD:
+
+        # All the blacklist regexps except the ones we are an admin
+        # for. These we do not check since they are not blacklisted to us.
+        SD["regexp_select_plan"] = plpy.prepare("""
+            SELECT id, regexp FROM NameBlacklist
+            WHERE admin IS NULL OR admin NOT IN (
+                SELECT team FROM TeamParticipation
+                WHERE person = $1)
+            ORDER BY id
+            """, ["integer"])
+
+        # Storage for compiled regexps
+        SD["compiled"] = {}
+
+        # admins is a celebrity and its id is immutable.
+        admins_id = plpy.execute(
+            "SELECT id FROM Person WHERE name='admins'")[0]["id"]
+
+        SD["admin_select_plan"] = plpy.prepare("""
+            SELECT TRUE FROM TeamParticipation
+            WHERE
+                TeamParticipation.team = %d
+                AND TeamParticipation.person = $1
+            LIMIT 1
+            """ % admins_id, ["integer"])
+
+        # All the blacklist regexps except those that have an admin because
+        # members of ~admin can use any name that any other admin can use.
+        SD["admin_regexp_select_plan"] = plpy.prepare("""
+            SELECT id, regexp FROM NameBlacklist
+            WHERE admin IS NULL
+            ORDER BY id
+            """, ["integer"])
+
+
+    compiled = SD["compiled"]
+
+    # Names are never blacklisted for Lauchpad admins.
+    if user_id is not None and plpy.execute(
+        SD["admin_select_plan"], [user_id]).nrows() > 0:
+        blacklist_plan = "admin_regexp_select_plan"
+    else:
+        blacklist_plan = "regexp_select_plan"
+
+    for row in plpy.execute(SD[blacklist_plan], [user_id]):
+        regexp_id = row["id"]
+        regexp_txt = row["regexp"]
+        if (compiled.get(regexp_id) is None
+            or compiled[regexp_id][0] != regexp_txt):
+            regexp = re.compile(regexp_txt, re.IGNORECASE | re.VERBOSE)
+            compiled[regexp_id] = (regexp_txt, regexp)
+        else:
+            regexp = compiled[regexp_id][1]
+        if regexp.search(name) is not None:
+            return regexp_id
+    return None
+$_$;
+
+CREATE OR REPLACE FUNCTION person_sort_key(displayname text, name text) RETURNS text
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    # NB: If this implementation is changed, the person_sort_idx needs to be
+    # rebuilt along with any other indexes using it.
+    import re
+
+    try:
+        strip_re = SD["strip_re"]
+    except KeyError:
+        strip_re = re.compile("(?:[^\w\s]|[\d_])")
+        SD["strip_re"] = strip_re
+
+    displayname, name = args
+
+    # Strip noise out of displayname. We do not have to bother with
+    # name, as we know it is just plain ascii.
+    displayname = strip_re.sub('', displayname.lower())
+    return "%s, %s" % (displayname.strip(), name)
+$$;
+
+CREATE OR REPLACE FUNCTION sane_version(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $_$
+    import re
+    if re.search("""^(?ix)
+        [0-9a-z]
+        ( [0-9a-z] | [0-9a-z.-]*[0-9a-z] )*
+        $""", args[0]):
+        return 1
+    return 0
+$_$;
+
+CREATE OR REPLACE FUNCTION sha1(text) RETURNS character
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    import hashlib
+    return hashlib.sha1(args[0].encode()).hexdigest()
+$$;
+
+CREATE OR REPLACE FUNCTION ulower(text) RETURNS text
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    return args[0].lower()
+$$;
+
+CREATE OR REPLACE FUNCTION update_database_stats() RETURNS void
+    LANGUAGE plpython3u SECURITY DEFINER
+    SET search_path TO 'public'
+    AS $_$
+    import re
+    import subprocess
+
+    # Prune DatabaseTableStats and insert current data.
+    # First, detect if the statistics have been reset.
+    stats_reset = plpy.execute("""
+        SELECT *
+        FROM
+            pg_catalog.pg_stat_user_tables AS NowStat,
+            DatabaseTableStats AS LastStat
+        WHERE
+            LastStat.date_created = (
+                SELECT max(date_created) FROM DatabaseTableStats)
+            AND NowStat.schemaname = LastStat.schemaname
+            AND NowStat.relname = LastStat.relname
+            AND (
+                NowStat.seq_scan < LastStat.seq_scan
+                OR NowStat.idx_scan < LastStat.idx_scan
+                OR NowStat.n_tup_ins < LastStat.n_tup_ins
+                OR NowStat.n_tup_upd < LastStat.n_tup_upd
+                OR NowStat.n_tup_del < LastStat.n_tup_del
+                OR NowStat.n_tup_hot_upd < LastStat.n_tup_hot_upd)
+        LIMIT 1
+        """, 1).nrows() > 0
+    if stats_reset:
+        # The database stats have been reset. We cannot calculate
+        # deltas because we do not know when this happened. So we trash
+        # our records as they are now useless to us. We could be more
+        # sophisticated about this, but this should only happen
+        # when an admin explicitly resets the statistics or if the
+        # database is rebuilt.
+        plpy.notice("Stats wraparound. Purging DatabaseTableStats")
+        plpy.execute("DELETE FROM DatabaseTableStats")
+    else:
+        plpy.execute("""
+            DELETE FROM DatabaseTableStats
+            WHERE date_created < (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
+                - CAST('21 days' AS interval));
+            """)
+    # Insert current data.
+    plpy.execute("""
+        INSERT INTO DatabaseTableStats
+            SELECT
+                CURRENT_TIMESTAMP AT TIME ZONE 'UTC',
+                schemaname, relname, seq_scan, seq_tup_read,
+                coalesce(idx_scan, 0), coalesce(idx_tup_fetch, 0),
+                n_tup_ins, n_tup_upd, n_tup_del,
+                n_tup_hot_upd, n_live_tup, n_dead_tup, last_vacuum,
+                last_autovacuum, last_analyze, last_autoanalyze
+            FROM pg_catalog.pg_stat_user_tables;
+        """)
+
+    # Prune DatabaseCpuStats. Calculate CPU utilization information
+    # and insert current data.
+    plpy.execute("""
+        DELETE FROM DatabaseCpuStats
+        WHERE date_created < (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
+            - CAST('21 days' AS interval));
+        """)
+    dbname = plpy.execute(
+        "SELECT current_database() AS dbname", 1)[0]['dbname']
+    ps = subprocess.Popen(
+        ["ps", "-C", "postgres", "--no-headers", "-o", "cp,args"],
+        stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT)
+    stdout, stderr = ps.communicate()
+    cpus = {}
+    # We make the username match non-greedy so the trailing \d eats
+    # trailing digits from the database username. This collapses
+    # lpnet1, lpnet2 etc. into just lpnet.
+    ps_re = re.compile(
+        r"(?m)^\s*(\d+)\spostgres:\s(\w+?)\d*\s%s\s" % dbname)
+    for ps_match in ps_re.finditer(stdout):
+        cpu, username = ps_match.groups()
+        cpus[username] = int(cpu) + cpus.setdefault(username, 0)
+    cpu_ins = plpy.prepare(
+        "INSERT INTO DatabaseCpuStats (username, cpu) VALUES ($1, $2)",
+        ["text", "integer"])
+    for cpu_tuple in cpus.items():
+        plpy.execute(cpu_ins, cpu_tuple)
+$_$;
+
+CREATE OR REPLACE FUNCTION valid_absolute_url(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    from urllib.parse import urlparse, uses_netloc
+    # Extend list of schemes that specify netloc.
+    if 'git' not in uses_netloc:
+        uses_netloc.insert(0, 'bzr')
+        uses_netloc.insert(0, 'bzr+ssh')
+        uses_netloc.insert(0, 'ssh') # Mercurial
+    (scheme, netloc, path, params, query, fragment) = urlparse(args[0])
+    return bool(scheme and netloc)
+$$;
+
+CREATE OR REPLACE FUNCTION valid_branch_name(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    import re
+    name = args[0]
+    pat = r"^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z"
+    if re.match(pat, name):
+        return 1
+    return 0
+$$;
+
+CREATE OR REPLACE FUNCTION valid_cve(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $_$
+    import re
+    name = args[0]
+    pat = r"^(19|20)\d{2}-\d{4,}$"
+    if re.match(pat, name):
+        return 1
+    return 0
+$_$;
+
+CREATE OR REPLACE FUNCTION valid_debian_version(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $_$
+    import re
+    m = re.search("""^(?ix)
+        ([0-9]+:)?
+        ([0-9a-z][a-z0-9+:.~-]*?)
+        (-[a-z0-9+.~]+)?
+        $""", args[0])
+    if m is None:
+        return 0
+    epoch, version, revision = m.groups()
+    if not epoch:
+        # Can''t contain : if no epoch
+        if ":" in version:
+            return 0
+    if not revision:
+        # Can''t contain - if no revision
+        if "-" in version:
+            return 0
+    return 1
+$_$;
+
+CREATE OR REPLACE FUNCTION valid_fingerprint(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    import re
+    if re.match(r"[\dA-F]{40}", args[0]) is not None:
+        return 1
+    else:
+        return 0
+$$;
+
+CREATE OR REPLACE FUNCTION valid_git_repository_name(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    import re
+    name = args[0]
+    pat = r"^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z"
+    if not name.endswith(".git") and re.match(pat, name):
+        return 1
+    return 0
+$$;
+
+CREATE OR REPLACE FUNCTION valid_keyid(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    import re
+    if re.match(r"[\dA-F]{8}", args[0]) is not None:
+        return 1
+    else:
+        return 0
+$$;
+
+CREATE OR REPLACE FUNCTION valid_regexp(text) RETURNS boolean
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    import re
+    try:
+        re.compile(args[0])
+    except Exception:
+        return False
+    else:
+        return True
+$$;
+
+CREATE OR REPLACE FUNCTION version_sort_key(version text) RETURNS text
+    LANGUAGE plpython3u IMMUTABLE STRICT
+    AS $$
+    # If this method is altered, then any functional indexes using it
+    # need to be rebuilt.
+    import re
+
+    [version] = args
+
+    def substitute_filled_numbers(match):
+        # Prepend "~" so that version numbers will show up first
+        # when sorted descending, i.e. [3, 2c, 2b, 1, c, b, a] instead
+        # of [c, b, a, 3, 2c, 2b, 1]. "~" has the highest ASCII value
+        # of visible ASCII characters.
+        return '~' + match.group(0).zfill(5)
+
+    return re.sub('\d+', substitute_filled_numbers, version)
+$$;
+
+DROP EXTENSION plpythonu;
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2210, 46, 0);