|
1
|
#!/usr/bin/env python3
|
|
2
|
# SQL check for the "Assignee's group" grouping patch (Redmine #7655, v2).
|
|
3
|
#
|
|
4
|
# This script writes test.sql, which builds a minimal schema (users,
|
|
5
|
# groups_users, issues) and runs SQL of the same shape as the patch generates
|
|
6
|
# (the joins of IssueQuery#joins_for_assignee_group and
|
|
7
|
# IssueQuery#assignee_group_statement) for grouped counts, grouped totals and
|
|
8
|
# ordering. The SQL strings below are written by hand to mirror the patch;
|
|
9
|
# they are not extracted from Redmine.
|
|
10
|
#
|
|
11
|
# Test data:
|
|
12
|
# groups 10 "B Team" and 11 "A Team"
|
|
13
|
# user 1 in group 10, user 2 in group 11, user 3 in both, user 4 in none
|
|
14
|
# issues 100-103 assigned to users 1-4, issue 104 assigned to group 11,
|
|
15
|
# issue 105 unassigned
|
|
16
|
#
|
|
17
|
# Usage:
|
|
18
|
# python3 assignee_group_sql_check.py
|
|
19
|
# sqlite3 :memory: < test.sql
|
|
20
|
# psql -d <empty database> -f test.sql
|
|
21
|
# mysql <empty database> < test.sql
|
|
22
|
#
|
|
23
|
# Checked on SQLite 3.45, PostgreSQL 16 and MariaDB 10.11 (same groups,
|
|
24
|
# counts, totals and ordering on all three, apart from where NULLs sort).
|
|
25
|
# Not checked on SQL Server.
|
|
26
|
|
|
27
|
# Mirrors joins_for_assignee_group / assignee_group_statement in the patch
|
|
28
|
def subq(ids):
|
|
29
|
cond = f"gu.group_id IN ({','.join(map(str,ids))})" if ids else "1=0"
|
|
30
|
return f"(SELECT gu.user_id, MIN(gu.group_id) AS group_id FROM groups_users gu WHERE {cond} GROUP BY gu.user_id)"
|
|
31
|
STMT="COALESCE(assignee_group_self.id, assignee_group_preferred.group_id, assignee_group_any.group_id)"
|
|
32
|
def joins(pref, vis):
|
|
33
|
return " ".join([
|
|
34
|
"LEFT OUTER JOIN users assignee_group_self ON assignee_group_self.id = issues.assigned_to_id AND assignee_group_self.type = 'Group'",
|
|
35
|
f"LEFT OUTER JOIN {subq(pref)} assignee_group_preferred ON assignee_group_preferred.user_id = issues.assigned_to_id",
|
|
36
|
f"LEFT OUTER JOIN {subq(vis)} assignee_group_any ON assignee_group_any.user_id = issues.assigned_to_id",
|
|
37
|
f"LEFT OUTER JOIN users assignee_group ON assignee_group.id = {STMT}"])
|
|
38
|
schema = """
|
|
39
|
CREATE TABLE users (id INTEGER PRIMARY KEY, type VARCHAR(30), lastname VARCHAR(255));
|
|
40
|
CREATE TABLE groups_users (group_id INTEGER, user_id INTEGER);
|
|
41
|
CREATE TABLE issues (id INTEGER PRIMARY KEY, assigned_to_id INTEGER, estimated_hours FLOAT);
|
|
42
|
INSERT INTO users VALUES (10,'Group','B Team'),(11,'Group','A Team'),(12,'GroupNonMember','Non member'),
|
|
43
|
(1,'User','a'),(2,'User','b'),(3,'User','ab'),(4,'User','none');
|
|
44
|
INSERT INTO groups_users VALUES (10,1),(11,2),(10,3),(11,3);
|
|
45
|
INSERT INTO issues VALUES (100,1,1.5),(101,2,4),(102,3,2),(103,4,NULL),(104,11,NULL),(105,NULL,NULL);
|
|
46
|
"""
|
|
47
|
cases = {"no filter":([], [10,11]), "filter=11":([11],[10,11]), "only 11 visible":([],[11]), "no visible":([],[])}
|
|
48
|
out=[schema]
|
|
49
|
for name,(pref,vis) in cases.items():
|
|
50
|
j=joins(pref,vis)
|
|
51
|
out.append(f"SELECT '{name}: count' AS label;")
|
|
52
|
out.append(f"SELECT COUNT(*) AS count_all, {STMT} AS grp FROM issues {j} GROUP BY {STMT} ORDER BY grp;")
|
|
53
|
out.append(f"SELECT SUM(issues.estimated_hours) AS total, {STMT} AS grp FROM issues {j} GROUP BY {STMT} ORDER BY grp;")
|
|
54
|
out.append(f"SELECT issues.id, assignee_group.lastname FROM issues {j} ORDER BY assignee_group.lastname ASC, issues.id DESC;")
|
|
55
|
open("test.sql","w").write("\n".join(out)+"\n")
|