feat: Switch to postgres

Migrate from MariaDB to PostgreSQL.

Signed-off-by: moson <moson@archlinux.org>
This commit is contained in:
moson 2023-11-30 15:13:42 +01:00
parent 4637b2edba
commit fa5dd2ca2c
No known key found for this signature in database
GPG key ID: 4A4760AB4EE15296
64 changed files with 560 additions and 615 deletions

1
.env
View file

@ -1,6 +1,5 @@
FASTAPI_BACKEND="uvicorn" FASTAPI_BACKEND="uvicorn"
FASTAPI_WORKERS=2 FASTAPI_WORKERS=2
MARIADB_SOCKET_DIR="/var/run/mysqld/"
AURWEB_FASTAPI_PREFIX=https://localhost:8444 AURWEB_FASTAPI_PREFIX=https://localhost:8444
AURWEB_SSHD_PREFIX=ssh://aur@localhost:2222 AURWEB_SSHD_PREFIX=ssh://aur@localhost:2222
GIT_DATA_DIR="./aur.git/" GIT_DATA_DIR="./aur.git/"

View file

@ -8,7 +8,7 @@ cache:
- .pre-commit - .pre-commit
variables: variables:
AUR_CONFIG: conf/config # Default MySQL config setup in before_script. AUR_CONFIG: conf/config # Default PostgresSQL config setup in before_script.
DB_HOST: localhost DB_HOST: localhost
TEST_RECURSION_LIMIT: 10000 TEST_RECURSION_LIMIT: 10000
CURRENT_DIR: "$(pwd)" CURRENT_DIR: "$(pwd)"
@ -40,12 +40,12 @@ test:
- source .venv/bin/activate # Enable our virtualenv cache - source .venv/bin/activate # Enable our virtualenv cache
- ./docker/scripts/install-python-deps.sh - ./docker/scripts/install-python-deps.sh
- useradd -U -d /aurweb -c 'AUR User' aur - useradd -U -d /aurweb -c 'AUR User' aur
- ./docker/mariadb-entrypoint.sh - ./docker/postgres-entrypoint.sh
- (cd '/usr' && /usr/bin/mysqld_safe --datadir='/var/lib/mysql') & - su postgres -c '/usr/bin/postgres -D /var/lib/postgres/data' &
- 'until : > /dev/tcp/127.0.0.1/3306; do sleep 1s; done' - 'until : > /dev/tcp/127.0.0.1/5432; do sleep 1s; done'
- cp -v conf/config.dev conf/config - cp -v conf/config.dev conf/config
- sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config - sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config
- ./docker/test-mysql-entrypoint.sh # Create mysql AUR_CONFIG. - ./docker/test-postgres-entrypoint.sh # Create postgres AUR_CONFIG.
- make -C po all install # Compile translations. - make -C po all install # Compile translations.
- make -C doc # Compile asciidoc. - make -C doc # Compile asciidoc.
- make -C test clean # Cleanup coverage. - make -C test clean # Cleanup coverage.

View file

@ -91,7 +91,7 @@ browser if desired.
Accessible services (on the host): Accessible services (on the host):
- https://localhost:8444 (python via nginx) - https://localhost:8444 (python via nginx)
- localhost:13306 (mariadb) - localhost:15432 (postgresql)
- localhost:16379 (redis) - localhost:16379 (redis)
Docker services, by default, are setup to be hot reloaded when source code Docker services, by default, are setup to be hot reloaded when source code

View file

@ -14,7 +14,7 @@ read the instructions below.
$ cd aurweb $ cd aurweb
$ poetry install $ poetry install
2) Setup a web server with MySQL. The following block can be used with nginx: 2) Setup a web server with PostgreSQL. The following block can be used with nginx:
server { server {
# https is preferred and can be done easily with LetsEncrypt # https is preferred and can be done easily with LetsEncrypt
@ -100,7 +100,7 @@ read the instructions below.
6b) Setup Services 6b) Setup Services
aurweb utilizes the following systemd services: aurweb utilizes the following systemd services:
- mariadb - postgresql
- redis (optional, requires [options] cache 'redis') - redis (optional, requires [options] cache 'redis')
- `examples/aurweb.service` - `examples/aurweb.service`

47
TESTING
View file

@ -31,10 +31,10 @@ Containerized environment
6) [Optionally] populate the database with dummy data: 6) [Optionally] populate the database with dummy data:
# docker compose exec mariadb /bin/bash # docker compose exec postgres /bin/bash
# pacman -S --noconfirm words fortune-mod # pacman -S --noconfirm words fortune-mod
# poetry run schema/gendummydata.py dummy_data.sql # poetry run schema/gendummydata.py dummy_data.sql
# mariadb -uaur -paur aurweb < dummy_data.sql # su postgres -q -c 'psql aurweb < dummy_data.sql'
# exit # exit
Inspect `dummy_data.sql` for test credentials. Inspect `dummy_data.sql` for test credentials.
@ -62,7 +62,7 @@ INSTALL.
2) Install the necessary packages: 2) Install the necessary packages:
# pacman -S --needed python-poetry mariadb words fortune-mod nginx # pacman -S --needed python-poetry postgresql words fortune-mod nginx
3) Install the package/dependencies via `poetry`: 3) Install the package/dependencies via `poetry`:
@ -76,21 +76,24 @@ INSTALL.
Note that when the upstream config.dev is updated, you should compare it to Note that when the upstream config.dev is updated, you should compare it to
your conf/config, or regenerate your configuration with the command above. your conf/config, or regenerate your configuration with the command above.
5) Set up mariadb: 5) Set up postgres:
# mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql # su postgres
# systemctl start mariadb $ pg_ctl initdb -D /var/lib/postgres/data
# mariadb -u root $ pg_ctl start -D /var/lib/postgres/data
> CREATE USER 'aur'@'localhost' IDENTIFIED BY 'aur'; $ psql
> GRANT ALL ON *.* TO 'aur'@'localhost' WITH GRANT OPTION; > create database aurweb;
> CREATE DATABASE aurweb; > create role aur superuser login password 'aur';
> exit > exit
For the sake of simplicity in this example we just created a superuser account.
You might want to set up more granular permissions...
6) Prepare a database and insert dummy data: 6) Prepare a database and insert dummy data:
$ AUR_CONFIG=conf/config poetry run python -m aurweb.initdb $ AUR_CONFIG=conf/config poetry run python -m aurweb.initdb
$ poetry run schema/gendummydata.py dummy_data.sql $ poetry run schema/gendummydata.py dummy_data.sql
$ mariadb -uaur -paur aurweb < dummy_data.sql $ psql -U aur aurweb < dummy_data.sql
7) Run the test server: 7) Run the test server:
@ -121,7 +124,7 @@ In case you did the bare-metal install, steps 2, 3, 4 and 5 should be skipped.
1) Install the necessary packages: 1) Install the necessary packages:
# pacman -S --needed python-poetry mariadb-libs asciidoc openssh # pacman -S --needed python-poetry postgresql-libs asciidoc openssh
2) Install the package/dependencies via `poetry`: 2) Install the package/dependencies via `poetry`:
@ -135,24 +138,24 @@ In case you did the bare-metal install, steps 2, 3, 4 and 5 should be skipped.
Note that when the upstream config.dev is updated, you should compare it to Note that when the upstream config.dev is updated, you should compare it to
your conf/config, or regenerate your configuration with the command above. your conf/config, or regenerate your configuration with the command above.
4) Edit the config file conf/config and change the mysql/mariadb portion 4) Edit the config file conf/config and change the postgres portion
We can make use of our mariadb docker container instead of having to install We can make use of our postgres docker container instead of having to install
mariadb. Change the config as follows: postgres. Change the config as follows:
--------------------------------------------------------------------- ---------------------------------------------------------------------
; MySQL database information. User defaults to root for containerized ; PostgreSQL database information. User defaults to root for containerized
; testing with mysqldb. This should be set to a non-root user. ; testing with postgres. This should be set to a non-root user.
user = root user = aur
password = aur password = aur
host = 127.0.0.1 host = 127.0.0.1
port = 13306 port = 15432
;socket = /var/run/mysqld/mysqld.sock ;socket = /run/postgresql
--------------------------------------------------------------------- ---------------------------------------------------------------------
5) Start our mariadb docker container 5) Start our postgres docker container
# docker compose start mariadb # docker compose start postgres
6) Set environment variables 6) Set environment variables

View file

@ -50,7 +50,7 @@ class AnonymousUser:
LangPreference = aurweb.config.get("options", "default_lang") LangPreference = aurweb.config.get("options", "default_lang")
Timezone = aurweb.config.get("options", "default_timezone") Timezone = aurweb.config.get("options", "default_timezone")
Suspended = 0 Suspended = False
InactivityTS = 0 InactivityTS = 0
# A stub ssh_pub_key relationship. # A stub ssh_pub_key relationship.
@ -120,7 +120,7 @@ class BasicAuthBackend(AuthenticationBackend):
# At this point, we cannot have an invalid user if the record # At this point, we cannot have an invalid user if the record
# exists, due to ForeignKey constraints in the schema upheld # exists, due to ForeignKey constraints in the schema upheld
# by mysqlclient. # by the database system.
user = db.query(User).filter(User.ID == record.UsersID).first() user = db.query(User).filter(User.ID == record.UsersID).first()
user.nonce = util.make_nonce() user.nonce = util.make_nonce()
user.authenticated = True user.authenticated = True

View file

@ -1,5 +1,7 @@
from sqlalchemy.orm import Session
# Supported database drivers. # Supported database drivers.
DRIVERS = {"mysql": "mysql+mysqldb"} DRIVERS = {"postgres": "postgresql+psycopg2"}
def make_random_value(table: str, column: str, length: int): def make_random_value(table: str, column: str, length: int):
@ -65,7 +67,7 @@ def name() -> str:
_sessions = dict() _sessions = dict()
def get_session(engine=None): def get_session(engine=None) -> Session:
"""Return aurweb.db's global session.""" """Return aurweb.db's global session."""
dbname = name() dbname = name()
@ -221,22 +223,21 @@ def get_sqlalchemy_url():
constructor = URL.create constructor = URL.create
aur_db_backend = aurweb.config.get("database", "backend") aur_db_backend = aurweb.config.get("database", "backend")
if aur_db_backend == "mysql": if aur_db_backend == "postgres":
param_query = {}
port = aurweb.config.get_with_fallback("database", "port", None) port = aurweb.config.get_with_fallback("database", "port", None)
host = aurweb.config.get_with_fallback("database", "host", None)
socket = None
if not port: if not port:
param_query["unix_socket"] = aurweb.config.get("database", "socket") socket = aurweb.config.get("database", "socket")
return constructor( return constructor(
DRIVERS.get(aur_db_backend), DRIVERS.get(aur_db_backend),
username=aurweb.config.get("database", "user"), username=aurweb.config.get("database", "user"),
password=aurweb.config.get_with_fallback( password=aurweb.config.get_with_fallback(
"database", "password", fallback=None "database", "password", fallback=None
), ),
host=aurweb.config.get("database", "host"), host=socket if socket else host,
database=name(), database=name(),
port=port, port=port,
query=param_query,
) )
elif aur_db_backend == "sqlite": elif aur_db_backend == "sqlite":
return constructor( return constructor(
@ -352,7 +353,7 @@ class ConnectionExecutor:
backend = backend or aurweb.config.get("database", "backend") backend = backend or aurweb.config.get("database", "backend")
self._conn = conn self._conn = conn
if backend == "mysql": if backend == "postgres":
self._paramstyle = "format" self._paramstyle = "format"
elif backend == "sqlite": elif backend == "sqlite":
import sqlite3 import sqlite3
@ -393,20 +394,21 @@ class Connection:
aur_db_backend = aurweb.config.get("database", "backend") aur_db_backend = aurweb.config.get("database", "backend")
if aur_db_backend == "mysql": if aur_db_backend == "postgres":
import MySQLdb import psycopg2
aur_db_host = aurweb.config.get("database", "host") aur_db_host = aurweb.config.get_with_fallback("database", "host", None)
aur_db_name = name() aur_db_name = name()
aur_db_user = aurweb.config.get("database", "user") aur_db_user = aurweb.config.get("database", "user")
aur_db_pass = aurweb.config.get_with_fallback("database", "password", str()) aur_db_pass = aurweb.config.get_with_fallback("database", "password", str())
aur_db_socket = aurweb.config.get("database", "socket") aur_db_socket = aurweb.config.get_with_fallback("database", "socket", None)
self._conn = MySQLdb.connect( aur_db_port = aurweb.config.get_with_fallback("database", "port", None)
host=aur_db_host, self._conn = psycopg2.connect(
host=aur_db_host if not aur_db_socket else aur_db_socket,
user=aur_db_user, user=aur_db_user,
passwd=aur_db_pass, password=aur_db_pass,
db=aur_db_name, dbname=aur_db_name,
unix_socket=aur_db_socket, port=aur_db_port if not aur_db_socket else None,
) )
elif aur_db_backend == "sqlite": # pragma: no cover elif aur_db_backend == "sqlite": # pragma: no cover
# TODO: SQLite support has been removed in FastAPI. It remains # TODO: SQLite support has been removed in FastAPI. It remains

View file

@ -39,7 +39,7 @@ def main():
cur = conn.execute( cur = conn.execute(
"SELECT Users.Username, Users.AccountTypeID FROM Users " "SELECT Users.Username, Users.AccountTypeID FROM Users "
"INNER JOIN SSHPubKeys ON SSHPubKeys.UserID = Users.ID " "INNER JOIN SSHPubKeys ON SSHPubKeys.UserID = Users.ID "
"WHERE SSHPubKeys.PubKey = ? AND Users.Suspended = 0 " "WHERE SSHPubKeys.PubKey = ? AND Users.Suspended = False "
"AND NOT Users.Passwd = ''", "AND NOT Users.Passwd = ''",
(keytype + " " + keytext,), (keytype + " " + keytext,),
) )

View file

@ -63,10 +63,10 @@ def create_pkgbase(conn, pkgbase, user):
cur = conn.execute( cur = conn.execute(
"INSERT INTO PackageBases (Name, SubmittedTS, " "INSERT INTO PackageBases (Name, SubmittedTS, "
+ "ModifiedTS, SubmitterUID, MaintainerUID, " + "ModifiedTS, SubmitterUID, MaintainerUID, "
+ "FlaggerComment) VALUES (?, ?, ?, ?, ?, '')", + "FlaggerComment) VALUES (?, ?, ?, ?, ?, '') RETURNING id",
[pkgbase, now, now, userid, userid], [pkgbase, now, now, userid, userid],
) )
pkgbase_id = cur.lastrowid pkgbase_id = cur.fetchone()[0]
cur = conn.execute( cur = conn.execute(
"INSERT INTO PackageNotifications " + "(PackageBaseID, UserID) VALUES (?, ?)", "INSERT INTO PackageNotifications " + "(PackageBaseID, UserID) VALUES (?, ?)",
@ -135,11 +135,11 @@ def save_metadata(metadata, conn, user): # noqa: C901
cur = conn.execute( cur = conn.execute(
"INSERT INTO Packages (PackageBaseID, Name, " "INSERT INTO Packages (PackageBaseID, Name, "
+ "Version, Description, URL) " + "Version, Description, URL) "
+ "VALUES (?, ?, ?, ?, ?)", + "VALUES (?, ?, ?, ?, ?) RETURNING id",
[pkgbase_id, pkginfo["pkgname"], ver, pkginfo["pkgdesc"], pkginfo["url"]], [pkgbase_id, pkginfo["pkgname"], ver, pkginfo["pkgdesc"], pkginfo["url"]],
) )
pkgid = cur.fetchone()[0]
conn.commit() conn.commit()
pkgid = cur.lastrowid
# Add package sources. # Add package sources.
for source_info in extract_arch_fields(pkginfo, "source"): for source_info in extract_arch_fields(pkginfo, "source"):
@ -188,10 +188,11 @@ def save_metadata(metadata, conn, user): # noqa: C901
licenseid = row[0] licenseid = row[0]
else: else:
cur = conn.execute( cur = conn.execute(
"INSERT INTO Licenses (Name) " + "VALUES (?)", [license] "INSERT INTO Licenses (Name) " + "VALUES (?) RETURNING id",
[license],
) )
licenseid = cur.fetchone()[0]
conn.commit() conn.commit()
licenseid = cur.lastrowid
conn.execute( conn.execute(
"INSERT INTO PackageLicenses (PackageID, " "INSERT INTO PackageLicenses (PackageID, "
+ "LicenseID) VALUES (?, ?)", + "LicenseID) VALUES (?, ?)",
@ -201,16 +202,16 @@ def save_metadata(metadata, conn, user): # noqa: C901
# Add package groups. # Add package groups.
if "groups" in pkginfo: if "groups" in pkginfo:
for group in pkginfo["groups"]: for group in pkginfo["groups"]:
cur = conn.execute("SELECT ID FROM `Groups` WHERE Name = ?", [group]) cur = conn.execute("SELECT ID FROM Groups WHERE Name = ?", [group])
row = cur.fetchone() row = cur.fetchone()
if row: if row:
groupid = row[0] groupid = row[0]
else: else:
cur = conn.execute( cur = conn.execute(
"INSERT INTO `Groups` (Name) VALUES (?)", [group] "INSERT INTO Groups (Name) VALUES (?) RETURNING id", [group]
) )
groupid = cur.fetchone()[0]
conn.commit() conn.commit()
groupid = cur.lastrowid
conn.execute( conn.execute(
"INSERT INTO PackageGroups (PackageID, " "GroupID) VALUES (?, ?)", "INSERT INTO PackageGroups (PackageID, " "GroupID) VALUES (?, ?)",
[pkgid, groupid], [pkgid, groupid],

View file

@ -12,35 +12,35 @@ def feed_initial_data(conn):
conn.execute( conn.execute(
aurweb.schema.AccountTypes.insert(), aurweb.schema.AccountTypes.insert(),
[ [
{"ID": 1, "AccountType": "User"}, {"AccountType": "User"},
{"ID": 2, "AccountType": "Package Maintainer"}, {"AccountType": "Package Maintainer"},
{"ID": 3, "AccountType": "Developer"}, {"AccountType": "Developer"},
{"ID": 4, "AccountType": "Package Maintainer & Developer"}, {"AccountType": "Package Maintainer & Developer"},
], ],
) )
conn.execute( conn.execute(
aurweb.schema.DependencyTypes.insert(), aurweb.schema.DependencyTypes.insert(),
[ [
{"ID": 1, "Name": "depends"}, {"Name": "depends"},
{"ID": 2, "Name": "makedepends"}, {"Name": "makedepends"},
{"ID": 3, "Name": "checkdepends"}, {"Name": "checkdepends"},
{"ID": 4, "Name": "optdepends"}, {"Name": "optdepends"},
], ],
) )
conn.execute( conn.execute(
aurweb.schema.RelationTypes.insert(), aurweb.schema.RelationTypes.insert(),
[ [
{"ID": 1, "Name": "conflicts"}, {"Name": "conflicts"},
{"ID": 2, "Name": "provides"}, {"Name": "provides"},
{"ID": 3, "Name": "replaces"}, {"Name": "replaces"},
], ],
) )
conn.execute( conn.execute(
aurweb.schema.RequestTypes.insert(), aurweb.schema.RequestTypes.insert(),
[ [
{"ID": 1, "Name": "deletion"}, {"Name": "deletion"},
{"ID": 2, "Name": "orphan"}, {"Name": "orphan"},
{"ID": 3, "Name": "merge"}, {"Name": "merge"},
], ],
) )
@ -57,8 +57,9 @@ def run(args):
alembic_config.attributes["configure_logger"] = False alembic_config.attributes["configure_logger"] = False
engine = aurweb.db.get_engine(echo=(args.verbose >= 1)) engine = aurweb.db.get_engine(echo=(args.verbose >= 1))
aurweb.schema.metadata.create_all(engine)
conn = engine.connect() conn = engine.connect()
# conn.execute("CREATE COLLATION ci (provider = icu, locale = 'und-u-ks-level2', deterministic = false)") # noqa: E501
aurweb.schema.metadata.create_all(engine)
feed_initial_data(conn) feed_initial_data(conn)
conn.close() conn.close()

View file

@ -6,7 +6,8 @@ from aurweb import util
def to_dict(model): def to_dict(model):
return {c.name: getattr(model, c.name) for c in model.__table__.columns} return {c.origname: getattr(model, c.origname) for c in model.__table__.columns}
# return {c.name: getattr(model, c.name) for c in model.__table__.columns}
def to_json(model, indent: int = None): def to_json(model, indent: int = None):

View file

@ -21,6 +21,13 @@ CLOSED_ID = 1
ACCEPTED_ID = 2 ACCEPTED_ID = 2
REJECTED_ID = 3 REJECTED_ID = 3
STATUS_DISPLAY = {
PENDING_ID: PENDING,
CLOSED_ID: CLOSED,
ACCEPTED_ID: ACCEPTED,
REJECTED_ID: REJECTED,
}
class PackageRequest(Base): class PackageRequest(Base):
__table__ = schema.PackageRequests __table__ = schema.PackageRequests
@ -51,13 +58,6 @@ class PackageRequest(Base):
foreign_keys=[__table__.c.ClosedUID], foreign_keys=[__table__.c.ClosedUID],
) )
STATUS_DISPLAY = {
PENDING_ID: PENDING,
CLOSED_ID: CLOSED,
ACCEPTED_ID: ACCEPTED,
REJECTED_ID: REJECTED,
}
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@ -105,7 +105,7 @@ class PackageRequest(Base):
def status_display(self) -> str: def status_display(self) -> str:
"""Return a display string for the Status column.""" """Return a display string for the Status column."""
return self.STATUS_DISPLAY[self.Status] return STATUS_DISPLAY[self.Status]
def ml_message_id_hash(self) -> str: def ml_message_id_hash(self) -> str:
"""Return the X-Message-ID-Hash that is used in the mailing list archive.""" """Return the X-Message-ID-Hash that is used in the mailing list archive."""

View file

@ -1,6 +1,6 @@
from typing import Set from typing import Set
from sqlalchemy import and_, case, or_, orm from sqlalchemy import and_, case, func, or_, orm
from aurweb import db, models from aurweb import db, models
from aurweb.models import Group, Package, PackageBase, User from aurweb.models import Group, Package, PackageBase, User
@ -106,7 +106,7 @@ class PackageSearch:
self.query = self.query.filter( self.query = self.query.filter(
or_( or_(
Package.Name.like(f"%{keywords}%"), Package.Name.like(f"%{keywords}%"),
Package.Description.like(f"%{keywords}%"), func.lower(Package.Description).like(f"%{keywords}%"),
) )
) )
return self return self
@ -136,9 +136,9 @@ class PackageSearch:
self._join_user() self._join_user()
self._join_keywords() self._join_keywords()
keywords = set(k.lower() for k in keywords) keywords = set(k.lower() for k in keywords)
self.query = self.query.filter(PackageKeyword.Keyword.in_(keywords)).group_by( self.query = self.query.filter(
models.Package.Name func.lower(PackageKeyword.Keyword).in_(keywords)
) ).distinct()
return self return self
@ -146,7 +146,10 @@ class PackageSearch:
self._join_user() self._join_user()
if keywords: if keywords:
self.query = self.query.filter( self.query = self.query.filter(
and_(User.Username == keywords, User.ID == PackageBase.MaintainerUID) and_(
func.lower(User.Username) == keywords,
User.ID == PackageBase.MaintainerUID,
)
) )
else: else:
self.query = self.query.filter(PackageBase.MaintainerUID.is_(None)) self.query = self.query.filter(PackageBase.MaintainerUID.is_(None))
@ -155,7 +158,7 @@ class PackageSearch:
def _search_by_comaintainer(self, keywords: str) -> orm.Query: def _search_by_comaintainer(self, keywords: str) -> orm.Query:
self._join_user() self._join_user()
self._join_comaint() self._join_comaint()
user = db.query(User).filter(User.Username == keywords).first() user = db.query(User).filter(func.lower(User.Username) == keywords).first()
uid = 0 if not user else user.ID uid = 0 if not user else user.ID
self.query = self.query.filter(PackageComaintainer.UsersID == uid) self.query = self.query.filter(PackageComaintainer.UsersID == uid)
return self return self
@ -163,7 +166,7 @@ class PackageSearch:
def _search_by_co_or_maintainer(self, keywords: str) -> orm.Query: def _search_by_co_or_maintainer(self, keywords: str) -> orm.Query:
self._join_user() self._join_user()
self._join_comaint(True) self._join_comaint(True)
user = db.query(User).filter(User.Username == keywords).first() user = db.query(User).filter(func.lower(User.Username) == keywords).first()
uid = 0 if not user else user.ID uid = 0 if not user else user.ID
self.query = self.query.filter( self.query = self.query.filter(
or_(PackageComaintainer.UsersID == uid, User.ID == uid) or_(PackageComaintainer.UsersID == uid, User.ID == uid)
@ -174,7 +177,7 @@ class PackageSearch:
self._join_user() self._join_user()
uid = 0 uid = 0
user = db.query(User).filter(User.Username == keywords).first() user = db.query(User).filter(func.lower(User.Username) == keywords).first()
if user: if user:
uid = user.ID uid = user.ID

View file

@ -102,7 +102,7 @@ def get_pkg_or_base(
:raises HTTPException: With status code 404 if record doesn't exist :raises HTTPException: With status code 404 if record doesn't exist
:return: {Package,PackageBase} instance :return: {Package,PackageBase} instance
""" """
instance = db.query(cls).filter(cls.Name == name).first() instance = db.query(cls).filter(cls.Name == name.lower()).first()
if not instance: if not instance:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND) raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return instance return instance

View file

@ -4,7 +4,7 @@ from fastapi import Request
from sqlalchemy import and_ from sqlalchemy import and_
from aurweb import config, db, defaults, l10n, time, util from aurweb import config, db, defaults, l10n, time, util
from aurweb.models import PackageBase, User from aurweb.models import PackageBase, PackageKeyword, User
from aurweb.models.package_base import popularity from aurweb.models.package_base import popularity
from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_comaintainer import PackageComaintainer
from aurweb.models.package_comment import PackageComment from aurweb.models.package_comment import PackageComment
@ -46,7 +46,7 @@ def make_context(
context["unflaggers"].extend([pkgbase.Maintainer, pkgbase.Flagger]) context["unflaggers"].extend([pkgbase.Maintainer, pkgbase.Flagger])
context["packages_count"] = pkgbase.packages.count() context["packages_count"] = pkgbase.packages.count()
context["keywords"] = pkgbase.keywords context["keywords"] = pkgbase.keywords.order_by(PackageKeyword.Keyword)
context["comments_total"] = pkgbase.comments.order_by( context["comments_total"] = pkgbase.comments.order_by(
PackageComment.CommentTS.desc() PackageComment.CommentTS.desc()
).count() ).count()

View file

@ -47,7 +47,7 @@ async def passreset_post(
# The user parameter being required, we can match against # The user parameter being required, we can match against
criteria = or_(models.User.Username == user, models.User.Email == user) criteria = or_(models.User.Username == user, models.User.Email == user)
db_user = db.query(models.User, and_(criteria, models.User.Suspended == 0)).first() db_user = db.query(models.User, and_(criteria, ~models.User.Suspended)).first()
if db_user is None: if db_user is None:
context["errors"] = ["Invalid e-mail."] context["errors"] = ["Invalid e-mail."]
return render_template( return render_template(
@ -584,11 +584,11 @@ async def accounts_post(
v v
for k, v in [ for k, v in [
(account_type_id is not None, models.AccountType.ID == account_type_id), (account_type_id is not None, models.AccountType.ID == account_type_id),
(bool(U), models.User.Username.like(f"%{U}%")), (bool(U), models.User.Username.ilike(f"%{U}%")),
(bool(S), models.User.Suspended == S), (bool(S), models.User.Suspended == S),
(bool(E), models.User.Email.like(f"%{E}%")), (bool(E), models.User.Email.ilike(f"%{E}%")),
(bool(R), models.User.RealName.like(f"%{R}%")), (bool(R), models.User.RealName.ilike(f"%{R}%")),
(bool(I), models.User.IRCNick.like(f"%{I}%")), (bool(I), models.User.IRCNick.ilike(f"%{I}%")),
(bool(K), models.User.PGPKey.like(f"%{K}%")), (bool(K), models.User.PGPKey.like(f"%{K}%")),
] ]
if k if k

View file

@ -2,7 +2,7 @@ from http import HTTPStatus
from fastapi import APIRouter, Form, HTTPException, Request from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy import or_ from sqlalchemy import func, or_
import aurweb.config import aurweb.config
from aurweb import cookies, db from aurweb import cookies, db
@ -57,8 +57,8 @@ async def login_post(
db.query(User) db.query(User)
.filter( .filter(
or_( or_(
User.Username == user, func.lower(User.Username) == user.lower(),
User.Email == user, func.lower(User.Email) == user.lower(),
) )
) )
.first() .first()

View file

@ -124,7 +124,7 @@ async def package_maintainer(
) )
) )
.with_entities(models.Vote.UserID, last_vote, models.User.Username) .with_entities(models.Vote.UserID, last_vote, models.User.Username)
.group_by(models.Vote.UserID) .group_by(models.Vote.UserID, models.User.Username)
.order_by(last_vote.desc(), models.User.Username.asc()) .order_by(last_vote.desc(), models.User.Username.asc())
) )
context["last_votes_by_pm"] = last_votes_by_pm.all() context["last_votes_by_pm"] = last_votes_by_pm.all()
@ -371,7 +371,7 @@ async def package_maintainer_addvote_post(
db.query(User) db.query(User)
.filter( .filter(
and_( and_(
User.Suspended == 0, ~User.Suspended,
User.InactivityTS.isnot(None), User.InactivityTS.isnot(None),
User.AccountTypeID.in_(types), User.AccountTypeID.in_(types),
) )

View file

@ -54,6 +54,7 @@ async def packages_get(
# This means that for any sentences separated by spaces, # This means that for any sentences separated by spaces,
# they are used as if they were ANDed. # they are used as if they were ANDed.
keywords = context["K"] = request.query_params.get("K", str()) keywords = context["K"] = request.query_params.get("K", str())
keywords = keywords.lower()
keywords = keywords.split(" ") keywords = keywords.split(" ")
if search_by == "k": if search_by == "k":

View file

@ -95,7 +95,9 @@ async def requests( # noqa: C901
# Name filter (contains) # Name filter (contains)
if filter_pkg_name: if filter_pkg_name:
filtered = filtered.filter(PackageBase.Name.like(f"%{filter_pkg_name}%")) filtered = filtered.filter(
PackageBase.Name.like(f"%{filter_pkg_name.lower()}%")
)
# Additionally filter for requests made from package maintainer # Additionally filter for requests made from package maintainer
if filter_maintainer_requests: if filter_maintainer_requests:

View file

@ -218,7 +218,7 @@ class RPC:
models.User.Username.label("Maintainer"), models.User.Username.label("Maintainer"),
Submitter.Username.label("Submitter"), Submitter.Username.label("Submitter"),
) )
.group_by(models.Package.ID) .distinct()
) )
return query return query
@ -465,6 +465,9 @@ class RPC:
# Convert by to its aliased value if it has one. # Convert by to its aliased value if it has one.
by = RPC.BY_ALIASES.get(by, by) by = RPC.BY_ALIASES.get(by, by)
# lowercase all args
args = [arg.lower() for arg in args]
# Process the requested handler. # Process the requested handler.
try: try:
results = self._handle_callback(by, args) results = self._handle_callback(by, args)

View file

@ -7,7 +7,6 @@ usually be automatically generated. See `migrations/README` for details.
from sqlalchemy import ( from sqlalchemy import (
CHAR,
TIMESTAMP, TIMESTAMP,
Column, Column,
ForeignKey, ForeignKey,
@ -16,19 +15,23 @@ from sqlalchemy import (
String, String,
Table, Table,
Text, Text,
event,
text, text,
) )
from sqlalchemy.dialects.mysql import BIGINT, DECIMAL, INTEGER, TINYINT from sqlalchemy.dialects.postgresql import BIGINT, BOOLEAN, INTEGER, NUMERIC, SMALLINT
from sqlalchemy.ext.compiler import compiles from sqlalchemy.ext.compiler import compiles
import aurweb.config import aurweb.config
# from sqlalchemy import event
db_backend = aurweb.config.get("database", "backend") db_backend = aurweb.config.get("database", "backend")
@compiles(TINYINT, "sqlite") @compiles(SMALLINT, "sqlite")
def compile_tinyint_sqlite(type_, compiler, **kw): # pragma: no cover def compile_smallint_sqlite(type_, compiler, **kw): # pragma: no cover
"""TINYINT is not supported on SQLite. Substitute it with INTEGER.""" """SMALLINT is not supported on SQLite. Substitute it with INTEGER."""
return "INTEGER" return "INTEGER"
@ -43,17 +46,26 @@ def compile_bigint_sqlite(type_, compiler, **kw): # pragma: no cover
return "INTEGER" return "INTEGER"
@event.listens_for(Column, "before_parent_attach")
def attach_column(column: Column, parent, **kw):
column.origname = column.name
column.name = column.name.lower()
@event.listens_for(Index, "before_parent_attach")
def attach_index(index, parent, **kw):
index.name = index.name.lower()
metadata = MetaData() metadata = MetaData()
# Define the Account Types for the AUR. # Define the Account Types for the AUR.
AccountTypes = Table( AccountTypes = Table(
"AccountTypes", "AccountTypes",
metadata, metadata,
Column("ID", TINYINT(unsigned=True), primary_key=True), Column("ID", SMALLINT(), primary_key=True),
Column("AccountType", String(32), nullable=False, server_default=text("''")), Column("AccountType", String(32), nullable=False, server_default=text("''")),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -61,62 +73,51 @@ AccountTypes = Table(
Users = Table( Users = Table(
"Users", "Users",
metadata, metadata,
Column("ID", INTEGER(unsigned=True), primary_key=True), Column("ID", INTEGER(), primary_key=True),
Column( Column(
"AccountTypeID", "AccountTypeID",
ForeignKey("AccountTypes.ID", ondelete="NO ACTION"), ForeignKey("AccountTypes.ID", ondelete="NO ACTION"),
nullable=False, nullable=False,
server_default=text("1"), server_default=text("1"),
), ),
Column( Column("Suspended", BOOLEAN(), nullable=False, server_default=text("False")),
"Suspended", TINYINT(unsigned=True), nullable=False, server_default=text("0")
),
Column("Username", String(32), nullable=False, unique=True), Column("Username", String(32), nullable=False, unique=True),
Column("Email", String(254), nullable=False, unique=True), Column("Email", String(254), nullable=False, unique=True),
Column("BackupEmail", String(254)), Column("BackupEmail", String(254)),
Column( Column("HideEmail", BOOLEAN(), nullable=False, server_default=text("False")),
"HideEmail", TINYINT(unsigned=True), nullable=False, server_default=text("0")
),
Column("Passwd", String(255), nullable=False), Column("Passwd", String(255), nullable=False),
Column("Salt", CHAR(32), nullable=False, server_default=text("''")), Column("Salt", String(32), nullable=False, server_default=text("''")),
Column("ResetKey", CHAR(32), nullable=False, server_default=text("''")), Column("ResetKey", String(32), nullable=False, server_default=text("''")),
Column("RealName", String(64), nullable=False, server_default=text("''")), Column("RealName", String(64), nullable=False, server_default=text("''")),
Column("LangPreference", String(6), nullable=False, server_default=text("'en'")), Column("LangPreference", String(6), nullable=False, server_default=text("'en'")),
Column("Timezone", String(32), nullable=False, server_default=text("'UTC'")), Column("Timezone", String(32), nullable=False, server_default=text("'UTC'")),
Column("Homepage", Text), Column("Homepage", Text),
Column("IRCNick", String(32), nullable=False, server_default=text("''")), Column("IRCNick", String(32), nullable=False, server_default=text("''")),
Column("PGPKey", String(40)), Column("PGPKey", String(40)),
Column( Column("LastLogin", BIGINT(), nullable=False, server_default=text("0")),
"LastLogin", BIGINT(unsigned=True), nullable=False, server_default=text("0")
),
Column("LastLoginIPAddress", String(45)), Column("LastLoginIPAddress", String(45)),
Column( Column("LastSSHLogin", BIGINT(), nullable=False, server_default=text("0")),
"LastSSHLogin", BIGINT(unsigned=True), nullable=False, server_default=text("0")
),
Column("LastSSHLoginIPAddress", String(45)), Column("LastSSHLoginIPAddress", String(45)),
Column( Column("InactivityTS", BIGINT(), nullable=False, server_default=text("0")),
"InactivityTS", BIGINT(unsigned=True), nullable=False, server_default=text("0")
),
Column( Column(
"RegistrationTS", "RegistrationTS",
TIMESTAMP, TIMESTAMP,
nullable=False,
server_default=text("CURRENT_TIMESTAMP"), server_default=text("CURRENT_TIMESTAMP"),
), ),
Column("CommentNotify", TINYINT(1), nullable=False, server_default=text("1")), Column("CommentNotify", BOOLEAN(), nullable=False, server_default=text("True")),
Column("UpdateNotify", TINYINT(1), nullable=False, server_default=text("0")), Column("UpdateNotify", BOOLEAN(), nullable=False, server_default=text("False")),
Column("OwnershipNotify", TINYINT(1), nullable=False, server_default=text("1")), Column("OwnershipNotify", BOOLEAN(), nullable=False, server_default=text("True")),
Column("SSOAccountID", String(255), nullable=True, unique=True), Column("SSOAccountID", String(255), nullable=True, unique=True),
Index("UsersAccountTypeID", "AccountTypeID"), Index("UsersAccountTypeID", "AccountTypeID"),
Column( Column(
"HideDeletedComments", "HideDeletedComments",
TINYINT(unsigned=True), BOOLEAN(),
nullable=False, nullable=False,
server_default=text("0"), server_default=text("False"),
), ),
mysql_engine="InnoDB", Index("UsernameLowerUnique", text("lower(username)"), unique=True),
mysql_charset="utf8mb4", Index("EmailLowerUnique", text("lower(email)"), unique=True),
mysql_collate="utf8mb4_general_ci", quote=False,
) )
@ -127,9 +128,7 @@ SSHPubKeys = Table(
Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False),
Column("Fingerprint", String(44), primary_key=True), Column("Fingerprint", String(44), primary_key=True),
Column("PubKey", String(4096), nullable=False), Column("PubKey", String(4096), nullable=False),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_bin",
) )
@ -138,11 +137,9 @@ Sessions = Table(
"Sessions", "Sessions",
metadata, metadata,
Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False),
Column("SessionID", CHAR(32), nullable=False, unique=True), Column("SessionID", String(32), nullable=False, unique=True),
Column("LastUpdateTS", BIGINT(unsigned=True), nullable=False), Column("LastUpdateTS", BIGINT(), nullable=False),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_bin",
) )
@ -150,14 +147,12 @@ Sessions = Table(
PackageBases = Table( PackageBases = Table(
"PackageBases", "PackageBases",
metadata, metadata,
Column("ID", INTEGER(unsigned=True), primary_key=True), Column("ID", INTEGER(), primary_key=True),
Column("Name", String(255), nullable=False, unique=True), Column("Name", String(255), nullable=False, unique=True),
Column( Column("NumVotes", INTEGER(), nullable=False, server_default=text("0")),
"NumVotes", INTEGER(unsigned=True), nullable=False, server_default=text("0")
),
Column( Column(
"Popularity", "Popularity",
DECIMAL(10, 6, unsigned=True) if db_backend == "mysql" else String(17), NUMERIC(10, 6) if db_backend == "postgres" else String(17),
nullable=False, nullable=False,
server_default=text("0"), server_default=text("0"),
), ),
@ -167,10 +162,10 @@ PackageBases = Table(
nullable=False, nullable=False,
server_default=text("'1970-01-01 00:00:01.000000'"), server_default=text("'1970-01-01 00:00:01.000000'"),
), ),
Column("OutOfDateTS", BIGINT(unsigned=True)), Column("OutOfDateTS", BIGINT()),
Column("FlaggerComment", Text, nullable=False), Column("FlaggerComment", Text, nullable=False),
Column("SubmittedTS", BIGINT(unsigned=True), nullable=False), Column("SubmittedTS", BIGINT(), nullable=False),
Column("ModifiedTS", BIGINT(unsigned=True), nullable=False), Column("ModifiedTS", BIGINT(), nullable=False),
Column( Column(
"FlaggerUID", ForeignKey("Users.ID", ondelete="SET NULL") "FlaggerUID", ForeignKey("Users.ID", ondelete="SET NULL")
), # who flagged the package out-of-date? ), # who flagged the package out-of-date?
@ -184,9 +179,8 @@ PackageBases = Table(
Index("BasesNumVotes", "NumVotes"), Index("BasesNumVotes", "NumVotes"),
Index("BasesPackagerUID", "PackagerUID"), Index("BasesPackagerUID", "PackagerUID"),
Index("BasesSubmitterUID", "SubmitterUID"), Index("BasesSubmitterUID", "SubmitterUID"),
mysql_engine="InnoDB", Index("BasesNameLowerUnique", text("lower(name)"), unique=True),
mysql_charset="utf8mb4", quote=False,
mysql_collate="utf8mb4_general_ci",
) )
@ -208,9 +202,7 @@ PackageKeywords = Table(
server_default=text("''"), server_default=text("''"),
), ),
Index("KeywordsPackageBaseID", "PackageBaseID"), Index("KeywordsPackageBaseID", "PackageBaseID"),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -218,7 +210,7 @@ PackageKeywords = Table(
Packages = Table( Packages = Table(
"Packages", "Packages",
metadata, metadata,
Column("ID", INTEGER(unsigned=True), primary_key=True), Column("ID", INTEGER(), primary_key=True),
Column( Column(
"PackageBaseID", "PackageBaseID",
ForeignKey("PackageBases.ID", ondelete="CASCADE"), ForeignKey("PackageBases.ID", ondelete="CASCADE"),
@ -228,9 +220,8 @@ Packages = Table(
Column("Version", String(255), nullable=False, server_default=text("''")), Column("Version", String(255), nullable=False, server_default=text("''")),
Column("Description", String(255)), Column("Description", String(255)),
Column("URL", String(8000)), Column("URL", String(8000)),
mysql_engine="InnoDB", Index("PackagesNameLowerUnique", text("lower(name)"), unique=True),
mysql_charset="utf8mb4", quote=False,
mysql_collate="utf8mb4_general_ci",
) )
@ -238,11 +229,9 @@ Packages = Table(
Licenses = Table( Licenses = Table(
"Licenses", "Licenses",
metadata, metadata,
Column("ID", INTEGER(unsigned=True), primary_key=True), Column("ID", INTEGER(), primary_key=True),
Column("Name", String(255), nullable=False, unique=True), Column("Name", String(255), nullable=False, unique=True),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -262,7 +251,7 @@ PackageLicenses = Table(
primary_key=True, primary_key=True,
nullable=True, nullable=True,
), ),
mysql_engine="InnoDB", quote=False,
) )
@ -270,11 +259,9 @@ PackageLicenses = Table(
Groups = Table( Groups = Table(
"Groups", "Groups",
metadata, metadata,
Column("ID", INTEGER(unsigned=True), primary_key=True), Column("ID", INTEGER(), primary_key=True),
Column("Name", String(255), nullable=False, unique=True), Column("Name", String(255), nullable=False, unique=True),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -294,7 +281,7 @@ PackageGroups = Table(
primary_key=True, primary_key=True,
nullable=True, nullable=True,
), ),
mysql_engine="InnoDB", quote=False,
) )
@ -302,11 +289,9 @@ PackageGroups = Table(
DependencyTypes = Table( DependencyTypes = Table(
"DependencyTypes", "DependencyTypes",
metadata, metadata,
Column("ID", TINYINT(unsigned=True), primary_key=True), Column("ID", SMALLINT(), primary_key=True),
Column("Name", String(32), nullable=False, server_default=text("''")), Column("Name", String(32), nullable=False, server_default=text("''")),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -326,9 +311,7 @@ PackageDepends = Table(
Column("DepArch", String(255)), Column("DepArch", String(255)),
Index("DependsDepName", "DepName"), Index("DependsDepName", "DepName"),
Index("DependsPackageID", "PackageID"), Index("DependsPackageID", "PackageID"),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -336,11 +319,9 @@ PackageDepends = Table(
RelationTypes = Table( RelationTypes = Table(
"RelationTypes", "RelationTypes",
metadata, metadata,
Column("ID", TINYINT(unsigned=True), primary_key=True), Column("ID", SMALLINT(), primary_key=True),
Column("Name", String(32), nullable=False, server_default=text("''")), Column("Name", String(32), nullable=False, server_default=text("''")),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -359,9 +340,7 @@ PackageRelations = Table(
Column("RelArch", String(255)), Column("RelArch", String(255)),
Index("RelationsPackageID", "PackageID"), Index("RelationsPackageID", "PackageID"),
Index("RelationsRelName", "RelName"), Index("RelationsRelName", "RelName"),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -373,9 +352,7 @@ PackageSources = Table(
Column("Source", String(8000), nullable=False, server_default=text("'/dev/null'")), Column("Source", String(8000), nullable=False, server_default=text("'/dev/null'")),
Column("SourceArch", String(255)), Column("SourceArch", String(255)),
Index("SourcesPackageID", "PackageID"), Index("SourcesPackageID", "PackageID"),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -389,11 +366,11 @@ PackageVotes = Table(
ForeignKey("PackageBases.ID", ondelete="CASCADE"), ForeignKey("PackageBases.ID", ondelete="CASCADE"),
nullable=False, nullable=False,
), ),
Column("VoteTS", BIGINT(unsigned=True), nullable=False), Column("VoteTS", BIGINT(), nullable=False),
Index("VoteUsersIDPackageID", "UsersID", "PackageBaseID", unique=True), Index("VoteUsersIDPackageID", "UsersID", "PackageBaseID", unique=True),
Index("VotesPackageBaseID", "PackageBaseID"), Index("VotesPackageBaseID", "PackageBaseID"),
Index("VotesUsersID", "UsersID"), Index("VotesUsersID", "UsersID"),
mysql_engine="InnoDB", quote=False,
) )
@ -401,7 +378,7 @@ PackageVotes = Table(
PackageComments = Table( PackageComments = Table(
"PackageComments", "PackageComments",
metadata, metadata,
Column("ID", BIGINT(unsigned=True), primary_key=True), Column("ID", BIGINT(), primary_key=True),
Column( Column(
"PackageBaseID", "PackageBaseID",
ForeignKey("PackageBases.ID", ondelete="CASCADE"), ForeignKey("PackageBases.ID", ondelete="CASCADE"),
@ -410,19 +387,15 @@ PackageComments = Table(
Column("UsersID", ForeignKey("Users.ID", ondelete="SET NULL")), Column("UsersID", ForeignKey("Users.ID", ondelete="SET NULL")),
Column("Comments", Text, nullable=False), Column("Comments", Text, nullable=False),
Column("RenderedComment", Text, nullable=False), Column("RenderedComment", Text, nullable=False),
Column( Column("CommentTS", BIGINT(), nullable=False, server_default=text("0")),
"CommentTS", BIGINT(unsigned=True), nullable=False, server_default=text("0") Column("EditedTS", BIGINT()),
),
Column("EditedTS", BIGINT(unsigned=True)),
Column("EditedUsersID", ForeignKey("Users.ID", ondelete="SET NULL")), Column("EditedUsersID", ForeignKey("Users.ID", ondelete="SET NULL")),
Column("DelTS", BIGINT(unsigned=True)), Column("DelTS", BIGINT()),
Column("DelUsersID", ForeignKey("Users.ID", ondelete="CASCADE")), Column("DelUsersID", ForeignKey("Users.ID", ondelete="CASCADE")),
Column("PinnedTS", BIGINT(unsigned=True), nullable=False, server_default=text("0")), Column("PinnedTS", BIGINT(), nullable=False, server_default=text("0")),
Index("CommentsPackageBaseID", "PackageBaseID"), Index("CommentsPackageBaseID", "PackageBaseID"),
Index("CommentsUsersID", "UsersID"), Index("CommentsUsersID", "UsersID"),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -436,10 +409,10 @@ PackageComaintainers = Table(
ForeignKey("PackageBases.ID", ondelete="CASCADE"), ForeignKey("PackageBases.ID", ondelete="CASCADE"),
nullable=False, nullable=False,
), ),
Column("Priority", INTEGER(unsigned=True), nullable=False), Column("Priority", INTEGER(), nullable=False),
Index("ComaintainersPackageBaseID", "PackageBaseID"), Index("ComaintainersPackageBaseID", "PackageBaseID"),
Index("ComaintainersUsersID", "UsersID"), Index("ComaintainersUsersID", "UsersID"),
mysql_engine="InnoDB", quote=False,
) )
@ -454,7 +427,7 @@ PackageNotifications = Table(
), ),
Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False),
Index("NotifyUserIDPkgID", "UserID", "PackageBaseID", unique=True), Index("NotifyUserIDPkgID", "UserID", "PackageBaseID", unique=True),
mysql_engine="InnoDB", quote=False,
) )
@ -462,11 +435,9 @@ PackageNotifications = Table(
PackageBlacklist = Table( PackageBlacklist = Table(
"PackageBlacklist", "PackageBlacklist",
metadata, metadata,
Column("ID", INTEGER(unsigned=True), primary_key=True), Column("ID", INTEGER(), primary_key=True),
Column("Name", String(64), nullable=False, unique=True), Column("Name", String(64), nullable=False, unique=True),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -474,14 +445,12 @@ PackageBlacklist = Table(
OfficialProviders = Table( OfficialProviders = Table(
"OfficialProviders", "OfficialProviders",
metadata, metadata,
Column("ID", INTEGER(unsigned=True), primary_key=True), Column("ID", INTEGER(), primary_key=True),
Column("Name", String(64), nullable=False), Column("Name", String(64), nullable=False),
Column("Repo", String(64), nullable=False), Column("Repo", String(64), nullable=False),
Column("Provides", String(64), nullable=False), Column("Provides", String(64), nullable=False),
Index("ProviderNameProvides", "Name", "Provides", unique=True), Index("ProviderNameProvides", "Name", "Provides", unique=True),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_bin",
) )
@ -489,11 +458,9 @@ OfficialProviders = Table(
RequestTypes = Table( RequestTypes = Table(
"RequestTypes", "RequestTypes",
metadata, metadata,
Column("ID", TINYINT(unsigned=True), primary_key=True), Column("ID", SMALLINT(), primary_key=True),
Column("Name", String(32), nullable=False, server_default=text("''")), Column("Name", String(32), nullable=False, server_default=text("''")),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -501,7 +468,7 @@ RequestTypes = Table(
PackageRequests = Table( PackageRequests = Table(
"PackageRequests", "PackageRequests",
metadata, metadata,
Column("ID", BIGINT(unsigned=True), primary_key=True), Column("ID", BIGINT(), primary_key=True),
Column( Column(
"ReqTypeID", ForeignKey("RequestTypes.ID", ondelete="NO ACTION"), nullable=False "ReqTypeID", ForeignKey("RequestTypes.ID", ondelete="NO ACTION"), nullable=False
), ),
@ -511,17 +478,13 @@ PackageRequests = Table(
Column("UsersID", ForeignKey("Users.ID", ondelete="SET NULL")), Column("UsersID", ForeignKey("Users.ID", ondelete="SET NULL")),
Column("Comments", Text, nullable=False), Column("Comments", Text, nullable=False),
Column("ClosureComment", Text, nullable=False), Column("ClosureComment", Text, nullable=False),
Column( Column("RequestTS", BIGINT(), nullable=False, server_default=text("0")),
"RequestTS", BIGINT(unsigned=True), nullable=False, server_default=text("0") Column("ClosedTS", BIGINT()),
),
Column("ClosedTS", BIGINT(unsigned=True)),
Column("ClosedUID", ForeignKey("Users.ID", ondelete="SET NULL")), Column("ClosedUID", ForeignKey("Users.ID", ondelete="SET NULL")),
Column("Status", TINYINT(unsigned=True), nullable=False, server_default=text("0")), Column("Status", SMALLINT(), nullable=False, server_default=text("0")),
Index("RequestsPackageBaseID", "PackageBaseID"), Index("RequestsPackageBaseID", "PackageBaseID"),
Index("RequestsUsersID", "UsersID"), Index("RequestsUsersID", "UsersID"),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -529,31 +492,27 @@ PackageRequests = Table(
VoteInfo = Table( VoteInfo = Table(
"VoteInfo", "VoteInfo",
metadata, metadata,
Column("ID", INTEGER(unsigned=True), primary_key=True), Column("ID", INTEGER(), primary_key=True),
Column("Agenda", Text, nullable=False), Column("Agenda", Text, nullable=False),
Column("User", String(32), nullable=False), Column("User", String(32), nullable=False),
Column("Submitted", BIGINT(unsigned=True), nullable=False), Column("Submitted", BIGINT(), nullable=False),
Column("End", BIGINT(unsigned=True), nullable=False), Column("End", BIGINT(), nullable=False),
Column( Column(
"Quorum", "Quorum",
DECIMAL(2, 2, unsigned=True) if db_backend == "mysql" else String(5), NUMERIC(2, 2) if db_backend == "postgres" else String(5),
nullable=False, nullable=False,
), ),
Column("SubmitterID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), Column("SubmitterID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False),
Column("Yes", INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), Column("Yes", INTEGER(), nullable=False, server_default=text("'0'")),
Column("No", INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), Column("No", INTEGER(), nullable=False, server_default=text("'0'")),
Column( Column("Abstain", INTEGER(), nullable=False, server_default=text("'0'")),
"Abstain", INTEGER(unsigned=True), nullable=False, server_default=text("'0'")
),
Column( Column(
"ActiveUsers", "ActiveUsers",
INTEGER(unsigned=True), INTEGER(),
nullable=False, nullable=False,
server_default=text("'0'"), server_default=text("'0'"),
), ),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -563,7 +522,7 @@ Votes = Table(
metadata, metadata,
Column("VoteID", ForeignKey("VoteInfo.ID", ondelete="CASCADE"), nullable=False), Column("VoteID", ForeignKey("VoteInfo.ID", ondelete="CASCADE"), nullable=False),
Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False),
mysql_engine="InnoDB", quote=False,
) )
@ -573,9 +532,7 @@ Bans = Table(
metadata, metadata,
Column("IPAddress", String(45), primary_key=True), Column("IPAddress", String(45), primary_key=True),
Column("BanTS", TIMESTAMP, nullable=False), Column("BanTS", TIMESTAMP, nullable=False),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -583,15 +540,11 @@ Bans = Table(
Terms = Table( Terms = Table(
"Terms", "Terms",
metadata, metadata,
Column("ID", INTEGER(unsigned=True), primary_key=True), Column("ID", INTEGER(), primary_key=True),
Column("Description", String(255), nullable=False), Column("Description", String(255), nullable=False),
Column("URL", String(8000), nullable=False), Column("URL", String(8000), nullable=False),
Column( Column("Revision", INTEGER(), nullable=False, server_default=text("1")),
"Revision", INTEGER(unsigned=True), nullable=False, server_default=text("1") quote=False,
),
mysql_engine="InnoDB",
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )
@ -601,10 +554,8 @@ AcceptedTerms = Table(
metadata, metadata,
Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False),
Column("TermsID", ForeignKey("Terms.ID", ondelete="CASCADE"), nullable=False), Column("TermsID", ForeignKey("Terms.ID", ondelete="CASCADE"), nullable=False),
Column( Column("Revision", INTEGER(), nullable=False, server_default=text("0")),
"Revision", INTEGER(unsigned=True), nullable=False, server_default=text("0") quote=False,
),
mysql_engine="InnoDB",
) )
@ -613,10 +564,8 @@ ApiRateLimit = Table(
"ApiRateLimit", "ApiRateLimit",
metadata, metadata,
Column("IP", String(45), primary_key=True, unique=True, default=str()), Column("IP", String(45), primary_key=True, unique=True, default=str()),
Column("Requests", INTEGER(11), nullable=False), Column("Requests", INTEGER(), nullable=False),
Column("WindowStart", BIGINT(20), nullable=False), Column("WindowStart", BIGINT(), nullable=False),
Index("ApiRateLimitWindowStart", "WindowStart"), Index("ApiRateLimitWindowStart", "WindowStart"),
mysql_engine="InnoDB", quote=False,
mysql_charset="utf8mb4",
mysql_collate="utf8mb4_general_ci",
) )

View file

@ -136,7 +136,7 @@ class ResetKeyNotification(Notification):
def __init__(self, uid): def __init__(self, uid):
user = ( user = (
db.query(User) db.query(User)
.filter(and_(User.ID == uid, User.Suspended == 0)) .filter(and_(User.ID == uid, ~User.Suspended))
.with_entities( .with_entities(
User.Username, User.Username,
User.Email, User.Email,
@ -206,10 +206,10 @@ class CommentNotification(Notification):
.join(PackageNotification) .join(PackageNotification)
.filter( .filter(
and_( and_(
User.CommentNotify == 1, User.CommentNotify,
PackageNotification.UserID != uid, PackageNotification.UserID != uid,
PackageNotification.PackageBaseID == pkgbase_id, PackageNotification.PackageBaseID == pkgbase_id,
User.Suspended == 0, ~User.Suspended,
) )
) )
.with_entities(User.Email, User.LangPreference) .with_entities(User.Email, User.LangPreference)
@ -271,10 +271,10 @@ class UpdateNotification(Notification):
.join(PackageNotification) .join(PackageNotification)
.filter( .filter(
and_( and_(
User.UpdateNotify == 1, User.UpdateNotify,
PackageNotification.UserID != uid, PackageNotification.UserID != uid,
PackageNotification.PackageBaseID == pkgbase_id, PackageNotification.PackageBaseID == pkgbase_id,
User.Suspended == 0, ~User.Suspended,
) )
) )
.with_entities(User.Email, User.LangPreference) .with_entities(User.Email, User.LangPreference)
@ -334,7 +334,7 @@ class FlagNotification(Notification):
PackageBase.ID == PackageComaintainer.PackageBaseID, PackageBase.ID == PackageComaintainer.PackageBaseID,
), ),
) )
.filter(and_(PackageBase.ID == pkgbase_id, User.Suspended == 0)) .filter(and_(PackageBase.ID == pkgbase_id, ~User.Suspended))
.with_entities(User.Email, User.LangPreference) .with_entities(User.Email, User.LangPreference)
.distinct() .distinct()
.order_by(User.Email) .order_by(User.Email)
@ -385,10 +385,10 @@ class OwnershipEventNotification(Notification):
.join(PackageNotification) .join(PackageNotification)
.filter( .filter(
and_( and_(
User.OwnershipNotify == 1, User.OwnershipNotify,
PackageNotification.UserID != uid, PackageNotification.UserID != uid,
PackageNotification.PackageBaseID == pkgbase_id, PackageNotification.PackageBaseID == pkgbase_id,
User.Suspended == 0, ~User.Suspended,
) )
) )
.with_entities(User.Email, User.LangPreference) .with_entities(User.Email, User.LangPreference)
@ -504,7 +504,7 @@ class DeleteNotification(Notification):
and_( and_(
PackageNotification.UserID != uid, PackageNotification.UserID != uid,
PackageNotification.PackageBaseID == old_pkgbase_id, PackageNotification.PackageBaseID == old_pkgbase_id,
User.Suspended == 0, ~User.Suspended,
) )
) )
.with_entities(User.Email, User.LangPreference) .with_entities(User.Email, User.LangPreference)
@ -580,12 +580,12 @@ class RequestOpenNotification(Notification):
User.ID == PackageComaintainer.UsersID, User.ID == PackageComaintainer.UsersID,
), ),
) )
.filter(and_(PackageRequest.ID == reqid, User.Suspended == 0)) .filter(and_(PackageRequest.ID == reqid, ~User.Suspended))
.with_entities(User.Email, User.HideEmail) .with_entities(User.Email, User.HideEmail)
.distinct() .distinct()
) )
self._cc = [u.Email for u in query if u.HideEmail == 0] self._cc = [u.Email for u in query if not u.HideEmail]
self._bcc = [u.Email for u in query if u.HideEmail == 1] self._bcc = [u.Email for u in query if u.HideEmail]
pkgreq = ( pkgreq = (
db.query(PackageRequest.Comments).filter(PackageRequest.ID == reqid).first() db.query(PackageRequest.Comments).filter(PackageRequest.ID == reqid).first()
@ -671,12 +671,12 @@ class RequestCloseNotification(Notification):
User.ID == PackageComaintainer.UsersID, User.ID == PackageComaintainer.UsersID,
), ),
) )
.filter(and_(PackageRequest.ID == reqid, User.Suspended == 0)) .filter(and_(PackageRequest.ID == reqid, ~User.Suspended))
.with_entities(User.Email, User.HideEmail) .with_entities(User.Email, User.HideEmail)
.distinct() .distinct()
) )
self._cc = [u.Email for u in query if u.HideEmail == 0] self._cc = [u.Email for u in query if not u.HideEmail]
self._bcc = [u.Email for u in query if u.HideEmail == 1] self._bcc = [u.Email for u in query if u.HideEmail]
pkgreq = ( pkgreq = (
db.query(PackageRequest) db.query(PackageRequest)
@ -755,7 +755,7 @@ class VoteReminderNotification(Notification):
and_( and_(
User.AccountTypeID.in_((2, 4)), User.AccountTypeID.in_((2, 4)),
~User.ID.in_(subquery), ~User.ID.in_(subquery),
User.Suspended == 0, ~User.Suspended,
) )
) )
.with_entities(User.Email, User.LangPreference) .with_entities(User.Email, User.LangPreference)

View file

@ -13,6 +13,7 @@ from aurweb.models.package_request import (
CLOSED_ID, CLOSED_ID,
PENDING_ID, PENDING_ID,
REJECTED_ID, REJECTED_ID,
STATUS_DISPLAY,
) )
from aurweb.prometheus import PACKAGES, REQUESTS, USERS from aurweb.prometheus import PACKAGES, REQUESTS, USERS
@ -143,10 +144,13 @@ def update_prometheus_metrics():
.query(PackageRequest, func.count(PackageRequest.ID), RequestType.Name) .query(PackageRequest, func.count(PackageRequest.ID), RequestType.Name)
.join(RequestType) .join(RequestType)
.group_by(RequestType.Name, PackageRequest.Status) .group_by(RequestType.Name, PackageRequest.Status)
.with_entities(
PackageRequest.Status, func.count(PackageRequest.ID), RequestType.Name
)
) )
results = db_query_cache("request_metrics", query, cache_expire) results = db_query_cache("request_metrics", query, cache_expire)
for record in results: for record in results:
status = record[0].status_display() status = STATUS_DISPLAY[record[0]]
count = record[1] count = record[1]
rtype = record[2] rtype = record[2]
REQUESTS.labels(type=rtype, status=status).set(count) REQUESTS.labels(type=rtype, status=status).set(count)

View file

@ -56,8 +56,8 @@ def setup_test_db(*args):
models.User.__tablename__, models.User.__tablename__,
] ]
aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 0") aurweb.db.get_session().execute("SET session_replication_role = 'replica'")
for table in tables: for table in tables:
aurweb.db.get_session().execute(f"DELETE FROM {table}") aurweb.db.get_session().execute(f"DELETE FROM {table}")
aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 1") aurweb.db.get_session().execute("SET session_replication_role = 'origin';")
aurweb.db.get_session().expunge_all() aurweb.db.get_session().expunge_all()

View file

@ -1,6 +1,7 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import func
from aurweb import db from aurweb import db
from aurweb.models import User from aurweb.models import User
@ -13,7 +14,7 @@ def get_user_by_name(username: str) -> User:
:param username: User.Username :param username: User.Username
:return: User instance :return: User instance
""" """
user = db.query(User).filter(User.Username == username).first() user = db.query(User).filter(func.lower(User.Username) == username.lower()).first()
if not user: if not user:
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
return db.refresh(user) return db.refresh(user)

View file

@ -7,7 +7,7 @@ All functions in this module raise aurweb.exceptions.ValidationError
when encountering invalid criteria and return silently otherwise. when encountering invalid criteria and return silently otherwise.
""" """
from fastapi import Request from fastapi import Request
from sqlalchemy import and_ from sqlalchemy import and_, func
from aurweb import aur_logging, config, db, l10n, models, time, util from aurweb import aur_logging, config, db, l10n, models, time, util
from aurweb.auth import creds from aurweb.auth import creds
@ -157,7 +157,11 @@ def username_in_use(
) -> None: ) -> None:
exists = ( exists = (
db.query(models.User) db.query(models.User)
.filter(and_(models.User.ID != user.ID, models.User.Username == U)) .filter(
and_(
models.User.ID != user.ID, func.lower(models.User.Username) == U.lower()
)
)
.exists() .exists()
) )
if db.query(exists).scalar(): if db.query(exists).scalar():
@ -175,7 +179,9 @@ def email_in_use(
) -> None: ) -> None:
exists = ( exists = (
db.query(models.User) db.query(models.User)
.filter(and_(models.User.ID != user.ID, models.User.Email == E)) .filter(
and_(models.User.ID != user.ID, func.lower(models.User.Email) == E.lower())
)
.exists() .exists()
) )
if db.query(exists).scalar(): if db.query(exists).scalar():

View file

@ -1,9 +1,9 @@
[database] [database]
backend = mysql backend = postgres
host = localhost ;host = localhost
socket = /var/run/mysqld/mysqld.sock socket = /run/postgresql
;port = 3306 ;port = 5432
name = AUR name = aurweb
user = aur user = aur
;password = aur ;password = aur

View file

@ -6,19 +6,13 @@
; development-specific options too. ; development-specific options too.
[database] [database]
; FastAPI options: mysql. backend = postgres
backend = mysql ;host = localhost
socket = /run/postgresql
; If using sqlite, set name to the database file path. ;port = 5432
name = aurweb name = aurweb
user = aur
; MySQL database information. User defaults to root for containerized password = aur
; testing with mysqldb. This should be set to a non-root user.
user = root
;password = aur
host = localhost
;port = 3306
socket = /var/run/mysqld/mysqld.sock
[options] [options]
aurwebdir = YOUR_AUR_ROOT aurwebdir = YOUR_AUR_ROOT

View file

@ -62,7 +62,7 @@ Services
|---------------------|-----------------| |---------------------|-----------------|
| [ca](#ca) | | | [ca](#ca) | |
| [cron](#cron) | | | [cron](#cron) | |
| [mariadb](#mariadb) | 127.0.0.1:13306 | | [postgres](#postgres) | 127.0.0.1:15432 |
| [git](#git) | 127.0.0.1:2222 | | [git](#git) | 127.0.0.1:2222 |
| redis | 127.0.0.1:16379 | | redis | 127.0.0.1:16379 |
| [fastapi](#fastapi) | 127.0.0.1:18000 | | [fastapi](#fastapi) | 127.0.0.1:18000 |
@ -88,13 +88,10 @@ anchors or browsers for SSL verification.
The _cron_ service includes all scripts recommended in `doc/maintenance.txt`. The _cron_ service includes all scripts recommended in `doc/maintenance.txt`.
#### mariadb #### postgres
- When used with the [default](#default) profile, a Docker-driven - When used with the [default](#default) profile, a Docker-driven
mariadb service is used. postgresql service is used.
- When used with the [aur-dev](#aur-dev) profile, `MARIADB_SOCKET_DIR`
(defaulted to `/var/run/mysqld/`) can be defined to bind-mount a
host-driven mariadb socket to the container.
#### git #### git

View file

@ -9,7 +9,7 @@ services:
redis: redis:
restart: always restart: always
mariadb: postgres:
restart: always restart: always
git: git:
@ -37,7 +37,7 @@ services:
cron: cron:
volumes: volumes:
# Exclude ./aurweb:/aurweb in production. # Exclude ./aurweb:/aurweb in production.
- mariadb_run:/var/run/mysqld - postgres_run:/run/postgresql
- archives:/var/lib/aurweb/archives - archives:/var/lib/aurweb/archives
fastapi: fastapi:
@ -60,8 +60,8 @@ services:
- smartgit_run:/var/run/smartgit - smartgit_run:/var/run/smartgit
volumes: volumes:
mariadb_run: {} # Share /var/run/mysqld postgres_run: {}
mariadb_data: {} # Share /var/lib/mysql postgres_data: {}
git_data: {} # Share aurweb/aur.git git_data: {} # Share aurweb/aur.git
smartgit_run: {} smartgit_run: {}
data: {} data: {}

View file

@ -6,9 +6,9 @@ services:
- ./data:/data - ./data:/data
- step:/root/.step - step:/root/.step
mariadb_init: postgres_init:
depends_on: depends_on:
mariadb: postgres:
condition: service_healthy condition: service_healthy
git: git:
@ -22,7 +22,7 @@ services:
- ./data:/data - ./data:/data
- smartgit_run:/var/run/smartgit - smartgit_run:/var/run/smartgit
depends_on: depends_on:
mariadb: postgres:
condition: service_healthy condition: service_healthy
fastapi: fastapi:

View file

@ -3,10 +3,10 @@
# #
# Notable services: # Notable services:
# - `sharness` - Run sharness test suites # - `sharness` - Run sharness test suites
# - `pytest-mysql` - Run pytest suites with MariaDB # - `pytest-postgres` - Run pytest suites with PostgreSQL
# - `pytest-sqlite` - Run pytest suites with SQLite # - `pytest-sqlite` - Run pytest suites with SQLite
# - `test` - Run sharness, pytest-mysql and pytest-sqlite # - `test` - Run sharness, pytest-postgres and pytest-sqlite
# - `mariadb` - `port 13306` - MariaDB server for docker # - `postgres` - `port 15432` - PostgreSQL server for docker
# - `ca` - Certificate Authority generation # - `ca` - Certificate Authority generation
# - `git` - `port 2222` - Git over SSH server # - `git` - `port 2222` - Git over SSH server
# - `fastapi` - hypercorn service for aurweb's FastAPI app # - `fastapi` - hypercorn service for aurweb's FastAPI app
@ -45,53 +45,34 @@ services:
ports: ports:
- "127.0.0.1:16379:6379" - "127.0.0.1:16379:6379"
mariadb: postgres:
image: aurweb:latest image: aurweb:latest
init: true init: true
entrypoint: /docker/mariadb-entrypoint.sh entrypoint: /docker/postgres-entrypoint.sh
command: /usr/bin/mysqld_safe --datadir=/var/lib/mysql command: su postgres -c '/usr/bin/postgres -D /var/lib/postgres/data'
ports: ports:
# This will expose mariadbd on 127.0.0.1:13306 in the host. - "127.0.0.1:15432:5432"
# Ex: `mysql -uaur -paur -h 127.0.0.1 -P 13306 aurweb`
- "127.0.0.1:13306:3306"
volumes: volumes:
- mariadb_run:/var/run/mysqld # Bind socket in this volume. - postgres_run:/run/postgresql
- mariadb_data:/var/lib/mysql - postgres_data:/var/lib/postgres
healthcheck: healthcheck:
test: "bash /docker/health/mariadb.sh" test: "bash /docker/health/postgres.sh"
interval: 3s interval: 3s
shm_size: 2gb
mariadb_init: postgres_init:
image: aurweb:latest image: aurweb:latest
init: true init: true
environment: environment:
- AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0}
entrypoint: /docker/mariadb-init-entrypoint.sh entrypoint: /docker/postgres-init-entrypoint.sh
command: echo "MariaDB tables initialized." command: echo "Postgres tables initialized."
volumes: volumes:
- mariadb_run:/var/run/mysqld - postgres_run:/run/postgresql
depends_on: depends_on:
mariadb: postgres:
condition: service_healthy condition: service_healthy
mariadb_test:
# Test database.
image: aurweb:latest
init: true
environment:
- MARIADB_PRIVILEGED=1
entrypoint: /docker/mariadb-entrypoint.sh
command: /usr/bin/mysqld_safe --datadir=/var/lib/mysql
ports:
# This will expose mariadbd on 127.0.0.1:13307 in the host.
# Ex: `mysql -uaur -paur -h 127.0.0.1 -P 13306 aurweb`
- "127.0.0.1:13307:3306"
volumes:
- mariadb_test_run:/var/run/mysqld # Bind socket in this volume.
healthcheck:
test: "bash /docker/health/mariadb.sh"
interval: 3s
git: git:
image: aurweb:latest image: aurweb:latest
init: true init: true
@ -107,10 +88,10 @@ services:
test: "bash /docker/health/sshd.sh" test: "bash /docker/health/sshd.sh"
interval: 3s interval: 3s
depends_on: depends_on:
mariadb_init: postgres_init:
condition: service_started condition: service_started
volumes: volumes:
- mariadb_run:/var/run/mysqld - postgres_run:/run/postgresql
smartgit: smartgit:
image: aurweb:latest image: aurweb:latest
@ -152,11 +133,11 @@ services:
entrypoint: /docker/cron-entrypoint.sh entrypoint: /docker/cron-entrypoint.sh
command: /docker/scripts/run-cron.sh command: /docker/scripts/run-cron.sh
depends_on: depends_on:
mariadb_init: postgres_init:
condition: service_started condition: service_started
volumes: volumes:
- ./aurweb:/aurweb/aurweb - ./aurweb:/aurweb/aurweb
- mariadb_run:/var/run/mysqld - postgres_run:/run/postgresql
- archives:/var/lib/aurweb/archives - archives:/var/lib/aurweb/archives
fastapi: fastapi:
@ -184,7 +165,7 @@ services:
condition: service_started condition: service_started
volumes: volumes:
- archives:/var/lib/aurweb/archives - archives:/var/lib/aurweb/archives
- mariadb_run:/var/run/mysqld - postgres_run:/run/postgresql
ports: ports:
- "127.0.0.1:18000:8000" - "127.0.0.1:18000:8000"
@ -222,7 +203,7 @@ services:
stdin_open: true stdin_open: true
tty: true tty: true
depends_on: depends_on:
mariadb_test: postgres:
condition: service_healthy condition: service_healthy
volumes: volumes:
- ./data:/data - ./data:/data
@ -231,7 +212,7 @@ services:
- ./test:/aurweb/test - ./test:/aurweb/test
- ./templates:/aurweb/templates - ./templates:/aurweb/templates
pytest-mysql: pytest-postgres:
image: aurweb:latest image: aurweb:latest
profiles: ["dev"] profiles: ["dev"]
init: true init: true
@ -240,17 +221,17 @@ services:
- TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT} - TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT}
- PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus
- LOG_CONFIG=logging.test.conf - LOG_CONFIG=logging.test.conf
entrypoint: /docker/test-mysql-entrypoint.sh entrypoint: /docker/test-postgres-entrypoint.sh
command: /docker/scripts/run-pytests.sh clean command: /docker/scripts/run-pytests.sh clean
stdin_open: true stdin_open: true
tty: true tty: true
depends_on: depends_on:
mariadb_test: postgres:
condition: service_healthy condition: service_healthy
tmpfs: tmpfs:
- /tmp - /tmp
volumes: volumes:
- mariadb_test_run:/var/run/mysqld - postgres_run:/run/postgresql
- ./data:/data - ./data:/data
- ./aurweb:/aurweb/aurweb - ./aurweb:/aurweb/aurweb
- ./migrations:/aurweb/migrations - ./migrations:/aurweb/migrations
@ -266,15 +247,15 @@ services:
- TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT} - TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT}
- PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus
- LOG_CONFIG=logging.test.conf - LOG_CONFIG=logging.test.conf
entrypoint: /docker/test-mysql-entrypoint.sh entrypoint: /docker/test-postgres-entrypoint.sh
command: /docker/scripts/run-tests.sh command: /docker/scripts/run-tests.sh
stdin_open: true stdin_open: true
tty: true tty: true
depends_on: depends_on:
mariadb_test: postgres:
condition: service_healthy condition: service_healthy
volumes: volumes:
- mariadb_test_run:/var/run/mysqld - postgres_run:/run/postgresql
- ./data:/data - ./data:/data
- ./aurweb:/aurweb/aurweb - ./aurweb:/aurweb/aurweb
- ./migrations:/aurweb/migrations - ./migrations:/aurweb/migrations
@ -282,9 +263,8 @@ services:
- ./templates:/aurweb/templates - ./templates:/aurweb/templates
volumes: volumes:
mariadb_test_run: {} postgres_run: {}
mariadb_run: {} # Share /var/run/mysqld/mysqld.sock postgres_data: {}
mariadb_data: {} # Share /var/lib/mysql
git_data: {} # Share aurweb/aur.git git_data: {} # Share aurweb/aur.git
smartgit_run: {} smartgit_run: {}
archives: {} archives: {}

View file

@ -47,7 +47,7 @@ Luckily such data can be generated.
docker compose exec fastapi /bin/bash docker compose exec fastapi /bin/bash
pacman -S words fortune-mod pacman -S words fortune-mod
./schema/gendummydata.py dummy.sql ./schema/gendummydata.py dummy.sql
mysql aurweb < dummy.sql su postgres -q -c 'psql aurweb < dummy.sql'
``` ```
The generation script may prompt you to install other Arch packages before it The generation script may prompt you to install other Arch packages before it

View file

@ -71,7 +71,7 @@ start_step_ca() {
kill_step_ca() { kill_step_ca() {
# Stop the step-ca web server. # Stop the step-ca web server.
killall step-ca >/dev/null 2>&1 || /bin/true killall -w step-ca >/dev/null 2>&1 || /bin/true
} }
install_step_ca() { install_step_ca() {
@ -105,8 +105,6 @@ if [ ! -d /root/.step/config ]; then
echo -n "WARN: Your certificates are being regenerated to resolve " echo -n "WARN: Your certificates are being regenerated to resolve "
echo -n "an inconsistent step-ca state. You will need to re-import " echo -n "an inconsistent step-ca state. You will need to re-import "
echo "the root CA certificate into your browser." echo "the root CA certificate into your browser."
else
exec "$@"
fi fi
# Set permissions to /data to rwx for everybody. # Set permissions to /data to rwx for everybody.

View file

@ -2,7 +2,7 @@
set -eou pipefail set -eou pipefail
# Setup the DB. # Setup the DB.
NO_INITDB=1 /docker/mariadb-init-entrypoint.sh /docker/postgres-init-entrypoint.sh
# Create aurblup's directory. # Create aurblup's directory.
AURBLUP_DIR="/aurweb/aurblup/" AURBLUP_DIR="/aurweb/aurblup/"

View file

@ -2,7 +2,7 @@
set -eou pipefail set -eou pipefail
# Setup database. # Setup database.
NO_INITDB=1 /docker/mariadb-init-entrypoint.sh /docker/postgres-init-entrypoint.sh
# Setup some other options. # Setup some other options.
aurweb-config set options cache 'redis' aurweb-config set options cache 'redis'

View file

@ -39,7 +39,7 @@ Match User aur
EOF EOF
# Setup database. # Setup database.
NO_INITDB=1 /docker/mariadb-init-entrypoint.sh /docker/postgres-init-entrypoint.sh
# Setup some other options. # Setup some other options.
aurweb-config set serve repo-path '/aurweb/aur.git/' aurweb-config set serve repo-path '/aurweb/aur.git/'

View file

@ -1,2 +0,0 @@
#!/bin/bash
exec mysqladmin ping --silent

2
docker/health/postgres.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
exec su postgres -c 'pg_isready'

View file

@ -1,31 +0,0 @@
#!/bin/bash
set -eou pipefail
MYSQL_DATA=/var/lib/mysql
mariadb-install-db --user=mysql --basedir=/usr --datadir=$MYSQL_DATA
# Start it up.
mysqld_safe --datadir=$MYSQL_DATA --skip-networking &
while ! mysqladmin ping 2>/dev/null; do
sleep 1s
done
# Configure databases.
DATABASE="aurweb" # Persistent database for fastapi.
echo "Taking care of primary database '${DATABASE}'..."
mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'localhost' IDENTIFIED BY 'aur';"
mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';"
mysql -u root -e "CREATE DATABASE IF NOT EXISTS $DATABASE;"
mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';"
mysql -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'localhost';"
mysql -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'%';"
mysql -u root -e "CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'aur';"
mysql -u root -e "GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION;"
mysqladmin -uroot shutdown
exec "$@"

View file

@ -1,17 +0,0 @@
#!/bin/bash
set -eou pipefail
# Setup a config for our mysql db.
aurweb-config set database name 'aurweb'
aurweb-config set database user 'aur'
aurweb-config set database password 'aur'
aurweb-config set database host 'localhost'
aurweb-config set database socket '/var/run/mysqld/mysqld.sock'
aurweb-config unset database port
if [ ! -z ${NO_INITDB+x} ]; then
exec "$@"
fi
python -m aurweb.initdb 2>/dev/null || /bin/true
exec "$@"

34
docker/postgres-entrypoint.sh Executable file
View file

@ -0,0 +1,34 @@
#!/bin/bash
set -eou pipefail
PGDATA=/var/lib/postgres/data
DATABASE="aurweb"
# Initialize and setup postgres
if [ ! -f "$PGDATA/../init" ]; then
echo "Preparing postgres instance..."
touch $PGDATA/../init
# Init db directory
su postgres -c "pg_ctl initdb -D $PGDATA"
su postgres -c "echo \"listen_addresses='*'\" >> $PGDATA/postgresql.conf"
su postgres -c "echo \"host all all 0.0.0.0/0 scram-sha-256\" >> $PGDATA/pg_hba.conf"
install -d -o postgres -g postgres /run/postgresql
# Start postgres
su postgres -c "pg_ctl start -D $PGDATA"
# Configure database & user
echo "Taking care of primary database '$DATABASE'..."
su postgres -c "psql -c \"create database $DATABASE;\""
su postgres -c "psql -c \"create role aur superuser login password 'aur';\"";
# Provision database
python -m aurweb.initdb 2>/dev/null || /bin/true
# Stop postgres
su postgres -c "pg_ctl stop -D $PGDATA"
fi
exec "$@"

View file

@ -0,0 +1,12 @@
#!/bin/bash
set -eou pipefail
# Setup a config for our postgres db via socket connection.
aurweb-config set database name 'aurweb'
aurweb-config set database user 'aur'
aurweb-config set database socket '/run/postgresql'
aurweb-config unset database host
aurweb-config unset database port
aurweb-config unset database password
exec "$@"

View file

@ -14,7 +14,7 @@ pacman -Sy --noconfirm --noprogressbar archlinux-keyring
# Install other OS dependencies. # Install other OS dependencies.
pacman -Syu --noconfirm --noprogressbar \ pacman -Syu --noconfirm --noprogressbar \
--cachedir .pkg-cache git gpgme nginx redis openssh \ --cachedir .pkg-cache git gpgme nginx redis openssh \
mariadb mariadb-libs cgit-aurweb uwsgi uwsgi-plugin-cgi \ postgresql cgit-aurweb uwsgi uwsgi-plugin-cgi \
python-pip pyalpm python-srcinfo curl libeatmydata cronie \ python-pip pyalpm python-srcinfo curl libeatmydata cronie \
python-poetry python-poetry-core step-cli step-ca asciidoc \ python-poetry python-poetry-core step-cli step-ca asciidoc \
python-virtualenv python-pre-commit python-virtualenv python-pre-commit

View file

@ -8,7 +8,7 @@ make -C test clean
# Run sharness tests. # Run sharness tests.
bash $dir/run-sharness.sh bash $dir/run-sharness.sh
# Run Python tests with MariaDB database. # Run Python tests with PostgreSQL database.
# Pass --silence to avoid reporting coverage. We will do that below. # Pass --silence to avoid reporting coverage. We will do that below.
bash $dir/run-pytests.sh --no-coverage bash $dir/run-pytests.sh --no-coverage

View file

@ -1,19 +0,0 @@
#!/bin/bash
set -eou pipefail
# We use the root user for testing in Docker.
# The test user must be able to create databases and drop them.
aurweb-config set database user 'root'
aurweb-config set database host 'localhost'
aurweb-config set database socket '/var/run/mysqld/mysqld.sock'
# Remove possibly problematic configuration options.
# We depend on the database socket within Docker and
# being run as the root user.
aurweb-config unset database password
aurweb-config unset database port
# Setup notifications for testing.
aurweb-config set notifications sendmail "$(pwd)/util/sendmail"
exec "$@"

View file

@ -0,0 +1,15 @@
#!/bin/bash
set -eou pipefail
# Setup a config for our postgres db via socket connection.
aurweb-config set database name 'aurweb'
aurweb-config set database user 'aur'
aurweb-config set database socket '/run/postgresql'
aurweb-config unset database host
aurweb-config unset database port
aurweb-config unset database password
# Setup notifications for testing.
aurweb-config set notifications sendmail "$(pwd)/util/sendmail"
exec "$@"

View file

@ -2,6 +2,6 @@
set -eou pipefail set -eou pipefail
dir="$(dirname $0)" dir="$(dirname $0)"
bash $dir/test-mysql-entrypoint.sh bash $dir/test-postgres-entrypoint.sh
exec "$@" exec "$@"

40
poetry.lock generated
View file

@ -1089,22 +1089,6 @@ files = [
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
] ]
[[package]]
name = "mysqlclient"
version = "2.2.0"
description = "Python interface to MySQL"
optional = false
python-versions = ">=3.8"
files = [
{file = "mysqlclient-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:68837b6bb23170acffb43ae411e47533a560b6360c06dac39aa55700972c93b2"},
{file = "mysqlclient-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5670679ff1be1cc3fef0fa81bf39f0cd70605ba121141050f02743eb878ac114"},
{file = "mysqlclient-2.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:004fe1d30d2c2ff8072f8ea513bcec235fd9b896f70dad369461d0ad7e570e98"},
{file = "mysqlclient-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9c6b142836c7dba4f723bf9c93cc46b6e5081d65b2af807f400dda9eb85a16d0"},
{file = "mysqlclient-2.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:955dba905a7443ce4788c63fdb9f8d688316260cf60b20ff51ac3b1c77616ede"},
{file = "mysqlclient-2.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:530ece9995a36cadb6211b9787f0c9e05cdab6702549bdb4236af5e9b535ed6a"},
{file = "mysqlclient-2.2.0.tar.gz", hash = "sha256:04368445f9c487d8abb7a878e3d23e923e6072c04a6c320f9e0dc8a82efba14e"},
]
[[package]] [[package]]
name = "orjson" name = "orjson"
version = "3.9.10" version = "3.9.10"
@ -1286,6 +1270,28 @@ files = [
{file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"}, {file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"},
] ]
[[package]]
name = "psycopg2"
version = "2.9.9"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = false
python-versions = ">=3.7"
files = [
{file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"},
{file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"},
{file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"},
{file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"},
{file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"},
{file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"},
{file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"},
{file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"},
{file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"},
{file = "psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"},
{file = "psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"},
{file = "psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"},
{file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"},
]
[[package]] [[package]]
name = "pyalpm" name = "pyalpm"
version = "0.10.6" version = "0.10.6"
@ -2007,4 +2013,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.9,<3.12" python-versions = ">=3.9,<3.12"
content-hash = "3c931b9e7957fc045d5e2356688606356f730c7a814958eb64ba9d5079f670e9" content-hash = "7cc2869b398d51b38a3849b2dfcc0e11fb82333eca0a0658d310ee67da373588"

View file

@ -78,7 +78,6 @@ paginate = "^0.5.6"
# SQL # SQL
alembic = "^1.12.1" alembic = "^1.12.1"
mysqlclient = "^2.2.0"
Authlib = "^1.2.1" Authlib = "^1.2.1"
Jinja2 = "^3.1.2" Jinja2 = "^3.1.2"
Markdown = "^3.5.1" Markdown = "^3.5.1"
@ -97,6 +96,7 @@ pyalpm = "^0.10.6"
fastapi = "^0.104.1" fastapi = "^0.104.1"
srcinfo = "^0.1.2" srcinfo = "^0.1.2"
tomlkit = "^0.12.0" tomlkit = "^0.12.0"
psycopg2 = {extras = ["c"], version = "^2.9.7"}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
coverage = "^7.3.2" coverage = "^7.3.2"

View file

@ -357,7 +357,7 @@ for t in range(0, OPEN_PROPOSALS + CLOSE_PROPOSALS):
user = user_keys[random.randrange(0, len(user_keys))] user = user_keys[random.randrange(0, len(user_keys))]
suid = packagemaintainers[random.randrange(0, len(packagemaintainers))] suid = packagemaintainers[random.randrange(0, len(packagemaintainers))]
s = ( s = (
"INSERT INTO VoteInfo (Agenda, User, Submitted, End," 'INSERT INTO VoteInfo (Agenda, "user", Submitted, "end",'
" Quorum, SubmitterID) VALUES ('%s', '%s', %d, %d, 0.0, %d);\n" " Quorum, SubmitterID) VALUES ('%s', '%s', %d, %d, 0.0, %d);\n"
) )
s = s % (genFortune(), user, start, end, suid) s = s % (genFortune(), user, start, end, suid)

View file

@ -43,7 +43,7 @@
<div> <div>
<input type="text" <input type="text"
name="keywords" name="keywords"
value="{{ pkgbase.keywords | join(' ', attribute='Keyword') }}" value="{{ keywords | join(' ', attribute='Keyword') }}"
/> />
<input type="submit" value="{{ 'Update' | tr }}"/> <input type="submit" value="{{ 'Update' | tr }}"/>
</div> </div>
@ -51,7 +51,7 @@
</td> </td>
{% else %} {% else %}
<td> <td>
{% for keyword in pkgbase.keywords.all() %} {% for keyword in keywords.all() %}
<a class="keyword" <a class="keyword"
href="/packages/?K={{ keyword.Keyword }}&amp;SeB=k" href="/packages/?K={{ keyword.Keyword }}&amp;SeB=k"
> >

View file

@ -60,7 +60,7 @@ other user:
GRANT ALL ON *.* TO 'user'@'localhost' WITH GRANT OPTION GRANT ALL ON *.* TO 'user'@'localhost' WITH GRANT OPTION
The aurweb platform is intended to use the `mysql` backend, but The aurweb platform is intended to use the `postgresql` backend, but
the `sqlite` backend is still used for sharness tests. These tests the `sqlite` backend is still used for sharness tests. These tests
will soon be replaced with pytest suites and `sqlite` removed. will soon be replaced with pytest suites and `sqlite` removed.

View file

@ -52,6 +52,7 @@ from sqlalchemy.orm import scoped_session
import aurweb.config import aurweb.config
import aurweb.db import aurweb.db
import aurweb.schema
from aurweb import aur_logging, initdb, testing from aurweb import aur_logging, initdb, testing
from aurweb.testing.email import Email from aurweb.testing.email import Email
from aurweb.testing.git import GitRepository from aurweb.testing.git import GitRepository
@ -68,25 +69,28 @@ values.ValueClass = values.MutexValue
def test_engine() -> Engine: def test_engine() -> Engine:
""" """
Return a privileged SQLAlchemy engine with no database. Return a privileged SQLAlchemy engine with default database.
This method is particularly useful for providing an engine that This method is particularly useful for providing an engine that
can be used to create and drop databases from an SQL server. can be used to create and drop databases from an SQL server.
:return: SQLAlchemy Engine instance (not connected to a database) :return: SQLAlchemy Engine instance (connected to a default)
""" """
unix_socket = aurweb.config.get_with_fallback("database", "socket", None) socket = aurweb.config.get_with_fallback("database", "socket", None)
host = aurweb.config.get_with_fallback("database", "host", None)
port = aurweb.config.get_with_fallback("database", "port", None)
kwargs = { kwargs = {
"database": aurweb.config.get("database", "name"),
"username": aurweb.config.get("database", "user"), "username": aurweb.config.get("database", "user"),
"password": aurweb.config.get_with_fallback("database", "password", None), "password": aurweb.config.get_with_fallback("database", "password", None),
"host": aurweb.config.get("database", "host"), "host": socket if socket else host,
"port": aurweb.config.get_with_fallback("database", "port", None), "port": port if not socket else None,
"query": {"unix_socket": unix_socket},
} }
backend = aurweb.config.get("database", "backend") backend = aurweb.config.get("database", "backend")
driver = aurweb.db.DRIVERS.get(backend) driver = aurweb.db.DRIVERS.get(backend)
return create_engine(URL.create(driver, **kwargs)) return create_engine(URL.create(driver, **kwargs), isolation_level="AUTOCOMMIT")
class AlembicArgs: class AlembicArgs:
@ -116,7 +120,7 @@ def _create_database(engine: Engine, dbname: str) -> None:
# a ProgrammingError. Just drop the database and try # a ProgrammingError. Just drop the database and try
# again. If at that point things still fail, any # again. If at that point things still fail, any
# exception will be propogated up to the caller. # exception will be propogated up to the caller.
conn.execute(f"DROP DATABASE {dbname}") conn.execute(f"DROP DATABASE {dbname} WITH (FORCE)")
conn.execute(f"CREATE DATABASE {dbname}") conn.execute(f"CREATE DATABASE {dbname}")
conn.close() conn.close()
initdb.run(AlembicArgs) initdb.run(AlembicArgs)
@ -129,9 +133,8 @@ def _drop_database(engine: Engine, dbname: str) -> None:
:param engine: Engine returned by test_engine() :param engine: Engine returned by test_engine()
:param dbname: Database name to drop :param dbname: Database name to drop
""" """
aurweb.schema.metadata.drop_all(bind=engine)
conn = engine.connect() conn = engine.connect()
conn.execute(f"DROP DATABASE {dbname}") conn.execute(f"DROP DATABASE {dbname} WITH (FORCE)")
conn.close() conn.close()
@ -178,6 +181,10 @@ def db_session(setup_database: None) -> scoped_session:
session.close() session.close()
aurweb.db.pop_session(dbname) aurweb.db.pop_session(dbname)
# Dispose engine and close connections
aurweb.db.get_engine(dbname).dispose()
aurweb.db.pop_engine(dbname)
@pytest.fixture @pytest.fixture
def db_test(db_session: scoped_session) -> None: def db_test(db_session: scoped_session) -> None:

View file

@ -14,7 +14,7 @@ from aurweb.models.user import User
from aurweb.testing.html import get_errors from aurweb.testing.html import get_errors
# Some test global constants. # Some test global constants.
TEST_USERNAME = "test" TEST_USERNAME = "Test"
TEST_EMAIL = "test@example.org" TEST_EMAIL = "test@example.org"
TEST_REFERER = { TEST_REFERER = {
"referer": aurweb.config.get("options", "aur_location") + "/login", "referer": aurweb.config.get("options", "aur_location") + "/login",
@ -54,36 +54,37 @@ def user() -> User:
def test_login_logout(client: TestClient, user: User): def test_login_logout(client: TestClient, user: User):
post_data = {"user": "test", "passwd": "testPassword", "next": "/"} for username in ["test", "TEst"]:
post_data = {"user": username, "passwd": "testPassword", "next": "/"}
with client as request: with client as request:
# First, let's test get /login. # First, let's test get /login.
response = request.get("/login") response = request.get("/login")
assert response.status_code == int(HTTPStatus.OK) assert response.status_code == int(HTTPStatus.OK)
response = request.post("/login", data=post_data) response = request.post("/login", data=post_data)
assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.status_code == int(HTTPStatus.SEE_OTHER)
# Simulate following the redirect location from above's response. # Simulate following the redirect location from above's response.
response = request.get(response.headers.get("location")) response = request.get(response.headers.get("location"))
assert response.status_code == int(HTTPStatus.OK) assert response.status_code == int(HTTPStatus.OK)
response = request.post("/logout", data=post_data) response = request.post("/logout", data=post_data)
assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.status_code == int(HTTPStatus.SEE_OTHER)
request.cookies = {"AURSID": response.cookies.get("AURSID")} request.cookies = {"AURSID": response.cookies.get("AURSID")}
response = request.post( response = request.post(
"/logout", "/logout",
data=post_data, data=post_data,
) )
assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.status_code == int(HTTPStatus.SEE_OTHER)
assert "AURSID" not in response.cookies assert "AURSID" not in response.cookies
def test_login_suspended(client: TestClient, user: User): def test_login_suspended(client: TestClient, user: User):
with db.begin(): with db.begin():
user.Suspended = 1 user.Suspended = True
data = {"user": user.Username, "passwd": "testPassword", "next": "/"} data = {"user": user.Username, "passwd": "testPassword", "next": "/"}
with client as request: with client as request:
@ -184,23 +185,23 @@ def test_secure_login(getboolean: mock.Mock, client: TestClient, user: User):
def test_authenticated_login(client: TestClient, user: User): def test_authenticated_login(client: TestClient, user: User):
post_data = {"user": user.Username, "passwd": "testPassword", "next": "/"} for username in [user.Username.lower(), user.Username.upper()]:
post_data = {"user": username, "passwd": "testPassword", "next": "/"}
with client as request: with client as request:
# Try to login. # Try to login.
response = request.post("/login", data=post_data) request.cookies = {}
assert response.status_code == int(HTTPStatus.SEE_OTHER) response = request.post("/login", data=post_data)
assert response.headers.get("location") == "/" assert response.status_code == int(HTTPStatus.SEE_OTHER)
assert response.headers.get("location") == "/"
# Now, let's verify that we get the logged in rendering # Now, let's verify that we get the logged in rendering
# when requesting GET /login as an authenticated user. # when requesting GET /login as an authenticated user.
# Now, let's verify that we receive 403 Forbidden when we request.cookies = response.cookies
# try to get /login as an authenticated user. response = request.get("/login")
request.cookies = response.cookies
response = request.get("/login")
assert response.status_code == int(HTTPStatus.OK) assert response.status_code == int(HTTPStatus.OK)
assert "Logged-in as: <strong>test</strong>" in response.text assert f"Logged-in as: <strong>{user.Username}</strong>" in response.text
def test_unauthenticated_logout_unauthorized(client: TestClient): def test_unauthenticated_logout_unauthorized(client: TestClient):
@ -370,5 +371,4 @@ def test_generate_unique_sid_exhausted(
assert re.search(expr, caplog.text) assert re.search(expr, caplog.text)
assert "IntegrityError" in caplog.text assert "IntegrityError" in caplog.text
expr = r"Duplicate entry .+ for key .+SessionID.+" assert "duplicate key value" in response.text
assert re.search(expr, response.text)

View file

@ -93,9 +93,9 @@ def make_temp_sqlite_config():
) )
def make_temp_mysql_config(): def make_temp_postgres_config():
return make_temp_config( return make_temp_config(
(r"backend = .*", "backend = mysql"), (r"name = .*", "name = aurweb_test") (r"backend = .*", "backend = postgres"), (r"name = .*", "name = aurweb_test")
) )
@ -114,8 +114,8 @@ def test_sqlalchemy_sqlite_url():
aurweb.config.rehash() aurweb.config.rehash()
def test_sqlalchemy_mysql_url(): def test_sqlalchemy_postgres_url():
tmpctx, tmp = make_temp_mysql_config() tmpctx, tmp = make_temp_postgres_config()
with tmpctx: with tmpctx:
with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}):
aurweb.config.rehash() aurweb.config.rehash()
@ -123,8 +123,8 @@ def test_sqlalchemy_mysql_url():
aurweb.config.rehash() aurweb.config.rehash()
def test_sqlalchemy_mysql_port_url(): def test_sqlalchemy_postgres_port_url():
tmpctx, tmp = make_temp_config((r";port = 3306", "port = 3306")) tmpctx, tmp = make_temp_config((r";port = 5432", "port = 5432"))
with tmpctx: with tmpctx:
with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}):
@ -133,7 +133,7 @@ def test_sqlalchemy_mysql_port_url():
aurweb.config.rehash() aurweb.config.rehash()
def test_sqlalchemy_mysql_socket_url(): def test_sqlalchemy_postgres_socket_url():
tmpctx, tmp = make_temp_config() tmpctx, tmp = make_temp_config()
with tmpctx: with tmpctx:
@ -170,16 +170,6 @@ def test_connection_class_unsupported_backend():
aurweb.config.rehash() aurweb.config.rehash()
@mock.patch("MySQLdb.connect", mock.MagicMock(return_value=True))
def test_connection_mysql():
tmpctx, tmp = make_temp_mysql_config()
with tmpctx:
with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}):
aurweb.config.rehash()
db.Connection()
aurweb.config.rehash()
def test_create_delete(): def test_create_delete():
with db.begin(): with db.begin():
account_type = db.create(AccountType, AccountType="test") account_type = db.create(AccountType, AccountType="test")
@ -212,8 +202,8 @@ def test_add_commit():
db.delete(account_type) db.delete(account_type)
def test_connection_executor_mysql_paramstyle(): def test_connection_executor_postgres_paramstyle():
executor = db.ConnectionExecutor(None, backend="mysql") executor = db.ConnectionExecutor(None, backend="postgres")
assert executor.paramstyle() == "format" assert executor.paramstyle() == "format"

View file

@ -20,7 +20,7 @@ def test_run():
from aurweb.schema import metadata from aurweb.schema import metadata
aurweb.db.kill_engine() aurweb.db.kill_engine()
metadata.drop_all(aurweb.db.get_engine()) metadata.drop_all(aurweb.db.get_engine(), checkfirst=False)
aurweb.initdb.run(Args()) aurweb.initdb.run(Args())
# Check that constant table rows got added via initdb. # Check that constant table rows got added via initdb.

View file

@ -226,7 +226,7 @@ please go to the package page [2] and select "Disable notifications".
def test_update(user: User, user2: User, pkgbases: list[PackageBase]): def test_update(user: User, user2: User, pkgbases: list[PackageBase]):
pkgbase = pkgbases[0] pkgbase = pkgbases[0]
with db.begin(): with db.begin():
user.UpdateNotify = 1 user.UpdateNotify = True
notif = notify.UpdateNotification(user2.ID, pkgbase.ID) notif = notify.UpdateNotification(user2.ID, pkgbase.ID)
notif.send() notif.send()
@ -330,7 +330,7 @@ You were removed from the co-maintainer list of {pkgbase.Name} [1].
def test_suspended_ownership_change(user: User, pkgbases: list[PackageBase]): def test_suspended_ownership_change(user: User, pkgbases: list[PackageBase]):
with db.begin(): with db.begin():
user.Suspended = 1 user.Suspended = True
pkgbase = pkgbases[0] pkgbase = pkgbases[0]
notif = notify.ComaintainerAddNotification(user.ID, pkgbase.ID) notif = notify.ComaintainerAddNotification(user.ID, pkgbase.ID)
@ -486,7 +486,7 @@ def test_open_close_request_hidden_email(
# Enable the "HideEmail" option for our requester # Enable the "HideEmail" option for our requester
with db.begin(): with db.begin():
user2.HideEmail = 1 user2.HideEmail = True
# Send an open request notification. # Send an open request notification.
notif = notify.RequestOpenNotification( notif = notify.RequestOpenNotification(

View file

@ -350,7 +350,7 @@ def test_pm_index_table_paging(client, pm_user):
VoteInfo, VoteInfo,
Agenda=f"Agenda #{i}", Agenda=f"Agenda #{i}",
User=pm_user.Username, User=pm_user.Username,
Submitted=(ts - 5), Submitted=(ts - 5 - i),
End=(ts + 1000), End=(ts + 1000),
Quorum=0.0, Quorum=0.0,
Submitter=pm_user, Submitter=pm_user,
@ -362,7 +362,7 @@ def test_pm_index_table_paging(client, pm_user):
VoteInfo, VoteInfo,
Agenda=f"Agenda #{25 + i}", Agenda=f"Agenda #{25 + i}",
User=pm_user.Username, User=pm_user.Username,
Submitted=(ts - 1000), Submitted=(ts - 1000 - i),
End=(ts - 5), End=(ts - 5),
Quorum=0.0, Quorum=0.0,
Submitter=pm_user, Submitter=pm_user,

View file

@ -742,14 +742,15 @@ def test_packages_empty(client: TestClient):
def test_packages_search_by_name(client: TestClient, packages: list[Package]): def test_packages_search_by_name(client: TestClient, packages: list[Package]):
with client as request: for keyword in ["pkg_", "PkG_"]:
response = request.get("/packages", params={"SeB": "n", "K": "pkg_"}) with client as request:
assert response.status_code == int(HTTPStatus.OK) response = request.get("/packages", params={"SeB": "n", "K": keyword})
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text) root = parse_root(response.text)
rows = root.xpath('//table[@class="results"]/tbody/tr') rows = root.xpath('//table[@class="results"]/tbody/tr')
assert len(rows) == 50 # Default per-page assert len(rows) == 50 # Default per-page
def test_packages_search_by_exact_name(client: TestClient, packages: list[Package]): def test_packages_search_by_exact_name(client: TestClient, packages: list[Package]):
@ -763,26 +764,28 @@ def test_packages_search_by_exact_name(client: TestClient, packages: list[Packag
# There is no package named exactly 'pkg_', we get 0 results. # There is no package named exactly 'pkg_', we get 0 results.
assert len(rows) == 0 assert len(rows) == 0
with client as request: for keyword in ["pkg_1", "PkG_1"]:
response = request.get("/packages", params={"SeB": "N", "K": "pkg_1"}) with client as request:
assert response.status_code == int(HTTPStatus.OK) response = request.get("/packages", params={"SeB": "N", "K": keyword})
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text) root = parse_root(response.text)
rows = root.xpath('//table[@class="results"]/tbody/tr') rows = root.xpath('//table[@class="results"]/tbody/tr')
# There's just one package named 'pkg_1', we get 1 result. # There's just one package named 'pkg_1', we get 1 result.
assert len(rows) == 1 assert len(rows) == 1
def test_packages_search_by_pkgbase(client: TestClient, packages: list[Package]): def test_packages_search_by_pkgbase(client: TestClient, packages: list[Package]):
with client as request: for keyword in ["pkg_", "PkG_"]:
response = request.get("/packages", params={"SeB": "b", "K": "pkg_"}) with client as request:
assert response.status_code == int(HTTPStatus.OK) response = request.get("/packages", params={"SeB": "b", "K": "pkg_"})
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text) root = parse_root(response.text)
rows = root.xpath('//table[@class="results"]/tbody/tr') rows = root.xpath('//table[@class="results"]/tbody/tr')
assert len(rows) == 50 assert len(rows) == 50
def test_packages_search_by_exact_pkgbase(client: TestClient, packages: list[Package]): def test_packages_search_by_exact_pkgbase(client: TestClient, packages: list[Package]):
@ -794,13 +797,14 @@ def test_packages_search_by_exact_pkgbase(client: TestClient, packages: list[Pac
rows = root.xpath('//table[@class="results"]/tbody/tr') rows = root.xpath('//table[@class="results"]/tbody/tr')
assert len(rows) == 0 assert len(rows) == 0
with client as request: for keyword in ["pkg_1", "PkG_1"]:
response = request.get("/packages", params={"SeB": "B", "K": "pkg_1"}) with client as request:
assert response.status_code == int(HTTPStatus.OK) response = request.get("/packages", params={"SeB": "B", "K": "pkg_1"})
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text) root = parse_root(response.text)
rows = root.xpath('//table[@class="results"]/tbody/tr') rows = root.xpath('//table[@class="results"]/tbody/tr')
assert len(rows) == 1 assert len(rows) == 1
def test_packages_search_by_keywords(client: TestClient, packages: list[Package]): def test_packages_search_by_keywords(client: TestClient, packages: list[Package]):
@ -821,15 +825,16 @@ def test_packages_search_by_keywords(client: TestClient, packages: list[Package]
) )
# And request packages with that keyword, we should get 1 result. # And request packages with that keyword, we should get 1 result.
with client as request: for keyword in ["testkeyword", "TestKeyWord"]:
# clear fakeredis cache with client as request:
cache._redis.flushall() # clear fakeredis cache
response = request.get("/packages", params={"SeB": "k", "K": "testKeyword"}) cache._redis.flushall()
assert response.status_code == int(HTTPStatus.OK) response = request.get("/packages", params={"SeB": "k", "K": keyword})
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text) root = parse_root(response.text)
rows = root.xpath('//table[@class="results"]/tbody/tr') rows = root.xpath('//table[@class="results"]/tbody/tr')
assert len(rows) == 1 assert len(rows) == 1
# Now let's add another keyword to the same package # Now let's add another keyword to the same package
with db.begin(): with db.begin():
@ -854,14 +859,13 @@ def test_packages_search_by_maintainer(
): ):
# We should expect that searching by `package`'s maintainer # We should expect that searching by `package`'s maintainer
# returns `package` in the results. # returns `package` in the results.
with client as request: for keyword in [maintainer.Username, maintainer.Username.upper()]:
response = request.get( with client as request:
"/packages", params={"SeB": "m", "K": maintainer.Username} response = request.get("/packages", params={"SeB": "m", "K": keyword})
) assert response.status_code == int(HTTPStatus.OK)
assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text)
root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr')
rows = root.xpath('//table[@class="results"]/tbody/tr') assert len(rows) == 1
assert len(rows) == 1
# Search again by maintainer with no keywords given. # Search again by maintainer with no keywords given.
# This kind of search returns all orphans instead. # This kind of search returns all orphans instead.
@ -912,17 +916,16 @@ def test_packages_search_by_comaintainer(
) )
# Then test that it's returned by our search. # Then test that it's returned by our search.
with client as request: for keyword in [maintainer.Username, maintainer.Username.upper()]:
# clear fakeredis cache with client as request:
cache._redis.flushall() # clear fakeredis cache
response = request.get( cache._redis.flushall()
"/packages", params={"SeB": "c", "K": maintainer.Username} response = request.get("/packages", params={"SeB": "c", "K": keyword})
) assert response.status_code == int(HTTPStatus.OK)
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text) root = parse_root(response.text)
rows = root.xpath('//table[@class="results"]/tbody/tr') rows = root.xpath('//table[@class="results"]/tbody/tr')
assert len(rows) == 1 assert len(rows) == 1
def test_packages_search_by_co_or_maintainer( def test_packages_search_by_co_or_maintainer(
@ -954,27 +957,27 @@ def test_packages_search_by_co_or_maintainer(
PackageComaintainer, PackageBase=package.PackageBase, User=user, Priority=1 PackageComaintainer, PackageBase=package.PackageBase, User=user, Priority=1
) )
with client as request: for keyword in [user.Username, user.Username.upper()]:
response = request.get("/packages", params={"SeB": "M", "K": user.Username}) with client as request:
assert response.status_code == int(HTTPStatus.OK) response = request.get("/packages", params={"SeB": "M", "K": keyword})
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text) root = parse_root(response.text)
rows = root.xpath('//table[@class="results"]/tbody/tr') rows = root.xpath('//table[@class="results"]/tbody/tr')
assert len(rows) == 1 assert len(rows) == 1
def test_packages_search_by_submitter( def test_packages_search_by_submitter(
client: TestClient, maintainer: User, package: Package client: TestClient, maintainer: User, package: Package
): ):
with client as request: for keyword in [maintainer.Username, maintainer.Username.upper()]:
response = request.get( with client as request:
"/packages", params={"SeB": "s", "K": maintainer.Username} response = request.get("/packages", params={"SeB": "s", "K": keyword})
) assert response.status_code == int(HTTPStatus.OK)
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text) root = parse_root(response.text)
rows = root.xpath('//table[@class="results"]/tbody/tr') rows = root.xpath('//table[@class="results"]/tbody/tr')
assert len(rows) == 1 assert len(rows) == 1
def test_packages_sort_by_name(client: TestClient, packages: list[Package]): def test_packages_sort_by_name(client: TestClient, packages: list[Package]):

View file

@ -153,7 +153,7 @@ def test_pkg_required(package: Package):
# We want to make sure "Package" data is included # We want to make sure "Package" data is included
# to avoid lazy-loading the information for each dependency # to avoid lazy-loading the information for each dependency
qry = util.pkg_required("test", list()) qry = util.pkg_required("test", list())
assert "Packages_ID" in str(qry) assert "packages_id" in str(qry).lower()
# We should have 1 record # We should have 1 record
assert qry.count() == 1 assert qry.count() == 1

View file

@ -428,7 +428,7 @@ def test_pkgbase_comments(
# create notification # create notification
with db.begin(): with db.begin():
user.CommentNotify = 1 user.CommentNotify = True
db.create(PackageNotification, PackageBase=package.PackageBase, User=user) db.create(PackageNotification, PackageBase=package.PackageBase, User=user)
# post a comment # post a comment

View file

@ -149,15 +149,15 @@ def assert_multiple_keys(pks):
def test_hash_query(): def test_hash_query():
# No conditions # No conditions
query = db.query(User) query = db.query(User)
assert util.hash_query(query) == "75e76026b7d576536e745ec22892cf8f5d7b5d62" assert util.hash_query(query) == "ebbf077df70d97a1584f91d0dd6ec61e43aa101f"
# With where clause # With where clause
query = db.query(User).filter(User.Username == "bla") query = db.query(User).filter(User.Username == "bla")
assert util.hash_query(query) == "4dca710f33b1344c27ec6a3c266970f4fa6a8a00" assert util.hash_query(query) == "b51f2bfda67051f381a5c05b2946a1aa4d91e56d"
# With where clause and sorting # With where clause and sorting
query = db.query(User).filter(User.Username == "bla").order_by(User.Username) query = db.query(User).filter(User.Username == "bla").order_by(User.Username)
assert util.hash_query(query) == "ee2c7846fede430776e140f8dfe1d83cd21d2eed" assert util.hash_query(query) == "8d458bfe1edfe8f78929fab590612e9e5d9db3a5"
# With where clause, sorting and specific columns # With where clause, sorting and specific columns
query = ( query = (
@ -166,4 +166,4 @@ def test_hash_query():
.order_by(User.Username) .order_by(User.Username)
.with_entities(User.Username) .with_entities(User.Username)
) )
assert util.hash_query(query) == "c1db751be61443d266cf643005eee7a884dac103" assert util.hash_query(query) == "006811a386789f25d40a37496f6ac6651413c245"