Skip to content

Commit 2c5f356

Browse files
committed
EXPERIMENT: Add a postgresql debver type
Alternative idea: Add a specific (autogenerated) version_sortkey field.
1 parent 68fa54d commit 2c5f356

File tree

7 files changed

+251
-7
lines changed

7 files changed

+251
-7
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Generated by Django 4.2.24 on 2025-09-10 07:38
2+
3+
from django.db import migrations
4+
import pulp_deb.fields
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("deb", "0031_add_domains"),
11+
]
12+
13+
operations = [
14+
migrations.RunSQL(
15+
sql="""
16+
CREATE COLLATION debver (provider='ICU', deterministic=false, locale='und', rules=$$
17+
[reorder digit latn symbol punct others][numericOrdering on]
18+
&[first variable]<'~'<'\\u0000'
19+
$$);
20+
21+
CREATE TYPE debver AS
22+
(
23+
sort_key TEXT COLLATE debver,
24+
value TEXT
25+
);
26+
27+
CREATE FUNCTION debver(value bpchar) RETURNS debver
28+
AS $$
29+
DECLARE
30+
pos integer;
31+
rest bpchar;
32+
epoch bpchar;
33+
version bpchar;
34+
revision bpchar;
35+
BEGIN
36+
pos := position(':' IN value);
37+
IF pos > 0
38+
THEN
39+
epoch := left(value, pos - 1);
40+
rest := right(value, -pos);
41+
ELSE
42+
epoch := '0';
43+
rest := value;
44+
END IF;
45+
pos := position('-' IN reverse(rest));
46+
IF pos > 0
47+
THEN
48+
version := left(rest, -pos);
49+
revision := right(rest, pos -1);
50+
ELSE
51+
version := rest;
52+
revision := '';
53+
END IF;
54+
return (epoch || '\ufffd' || version || '\u0000\ufffd' || revision || '\u0000', value)::debver;
55+
END;
56+
$$
57+
LANGUAGE plpgsql
58+
IMMUTABLE
59+
RETURNS NULL ON NULL INPUT;
60+
61+
CREATE FUNCTION text(version debver) RETURNS text
62+
AS $$
63+
BEGIN
64+
RETURN version.value;
65+
END;
66+
$$
67+
LANGUAGE plpgsql
68+
IMMUTABLE
69+
RETURNS NULL ON NULL INPUT;
70+
71+
CREATE CAST (text AS debver) WITH FUNCTION debver(bpchar) AS IMPLICIT;
72+
CREATE CAST (varchar AS debver) WITH FUNCTION debver(bpchar) AS IMPLICIT;
73+
CREATE CAST (bpchar AS debver) WITH FUNCTION debver(bpchar) AS IMPLICIT;
74+
CREATE CAST (debver AS text) WITH FUNCTION text(debver) AS IMPLICIT;
75+
CREATE CAST (debver AS varchar) WITH FUNCTION text(debver) AS IMPLICIT;
76+
CREATE CAST (debver AS bpchar) WITH FUNCTION text(debver) AS IMPLICIT;
77+
78+
CREATE FUNCTION debver_eq(a debver, b debver) RETURNS boolean
79+
LANGUAGE sql
80+
IMMUTABLE
81+
RETURNS NULL ON NULL INPUT
82+
RETURN a.sort_key = b.sort_key;
83+
84+
CREATE FUNCTION debver_neq(a debver, b debver) RETURNS boolean
85+
LANGUAGE sql
86+
IMMUTABLE
87+
RETURNS NULL ON NULL INPUT
88+
RETURN a.sort_key <> b.sort_key;
89+
90+
CREATE FUNCTION debver_lt(a debver, b debver) RETURNS boolean
91+
LANGUAGE sql
92+
IMMUTABLE
93+
RETURNS NULL ON NULL INPUT
94+
RETURN a.sort_key < b.sort_key;
95+
96+
CREATE FUNCTION debver_lte(a debver, b debver) RETURNS boolean
97+
LANGUAGE sql
98+
IMMUTABLE
99+
RETURNS NULL ON NULL INPUT
100+
RETURN a.sort_key <= b.sort_key;
101+
102+
CREATE FUNCTION debver_gte(a debver, b debver) RETURNS boolean
103+
LANGUAGE sql
104+
IMMUTABLE
105+
RETURNS NULL ON NULL INPUT
106+
RETURN a.sort_key >= b.sort_key;
107+
108+
CREATE FUNCTION debver_gt(a debver, b debver) RETURNS boolean
109+
LANGUAGE sql
110+
IMMUTABLE
111+
RETURNS NULL ON NULL INPUT
112+
RETURN a.sort_key > b.sort_key;
113+
114+
CREATE OPERATOR = (
115+
LEFTARG = debver,
116+
RIGHTARG = debver,
117+
FUNCTION = debver_eq
118+
);
119+
120+
CREATE OPERATOR <> (
121+
LEFTARG = debver,
122+
RIGHTARG = debver,
123+
FUNCTION = debver_neq
124+
);
125+
126+
CREATE OPERATOR < (
127+
LEFTARG = debver,
128+
RIGHTARG = debver,
129+
FUNCTION = debver_lt
130+
);
131+
132+
CREATE OPERATOR <= (
133+
LEFTARG = debver,
134+
RIGHTARG = debver,
135+
FUNCTION = debver_lte
136+
);
137+
138+
CREATE OPERATOR >= (
139+
LEFTARG = debver,
140+
RIGHTARG = debver,
141+
FUNCTION = debver_gte
142+
);
143+
144+
CREATE OPERATOR > (
145+
LEFTARG = debver,
146+
RIGHTARG = debver,
147+
FUNCTION = debver_gt
148+
);
149+
""",
150+
reverse_sql="""
151+
DROP TYPE IF EXISTS debver CASCADE;
152+
DROP COLLATION IF EXISTS debver;
153+
""",
154+
),
155+
migrations.AlterField(
156+
model_name="installerpackage",
157+
name="version",
158+
field=pulp_deb.fields.DebVersionField(),
159+
),
160+
migrations.AlterField(
161+
model_name="package",
162+
name="version",
163+
field=pulp_deb.fields.DebVersionField(),
164+
),
165+
]

pulp_deb/app/models/content/content.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from pulpcore.plugin.models import Content
1717
from pulpcore.plugin.util import get_domain_pk
1818

19+
from pulp_deb.fields import DebVersionField
20+
1921
BOOL_CHOICES = [(True, "yes"), (False, "no")]
2022

2123

@@ -33,7 +35,7 @@ class BasePackage(Content):
3335

3436
package = models.TextField() # package name
3537
source = models.TextField(null=True) # source package name
36-
version = models.TextField()
38+
version = DebVersionField()
3739
architecture = models.TextField() # all, i386, ...
3840
section = models.TextField(null=True) # admin, comm, database, ...
3941
priority = models.TextField(null=True) # required, standard, optional, extra

pulp_deb/app/viewsets/content.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ class Meta:
205205
fields = {
206206
"package": NAME_FILTER_OPTIONS,
207207
"source": ["exact"],
208-
"version": ["exact"],
208+
"version": ["exact", "ne", "lt", "lte", "gt", "gte"],
209209
"architecture": ["exact"],
210210
"section": ["exact"],
211211
"priority": ["exact"],

pulp_deb/fields.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.db import models
2+
3+
4+
class _DebVer(models.Value):
5+
def __init__(self, value):
6+
self.value = value
7+
8+
def as_sql(self, compiler, connection):
9+
return "debver(%s)", [self.value]
10+
11+
12+
class DebVersionField(models.CharField):
13+
description = "Debian Version"
14+
15+
def db_type(self, connection):
16+
return "debver"
17+
18+
def get_prep_value(self, value):
19+
if value is not None:
20+
return _DebVer(value)
21+
return value
22+
23+
def select_format(self, compiler, sql, params):
24+
return f"({sql}).value", params

pulp_deb/tests/conftest.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
22
from pathlib import Path
33

4+
from pulpcore.tests.functional.utils import BindingsNamespace
5+
46
from pulp_deb.tests.functional.utils import gen_deb_remote, gen_distribution, gen_repo
57
from pulp_deb.tests.functional.constants import DEB_FIXTURE_STANDARD_REPOSITORY_NAME
68

@@ -16,6 +18,21 @@
1618
)
1719

1820

21+
@pytest.fixture(scope="session")
22+
def deb_bindings(_api_client_set, bindings_cfg):
23+
"""
24+
A namespace providing preconfigured pulpcore api clients.
25+
26+
e.g. `pulpcore_bindings.WorkersApi.list()`.
27+
"""
28+
from pulpcore.client import pulp_deb as bindings_module
29+
30+
api_client = bindings_module.ApiClient(bindings_cfg)
31+
_api_client_set.add(api_client)
32+
yield BindingsNamespace(bindings_module, api_client)
33+
_api_client_set.remove(api_client)
34+
35+
1936
@pytest.fixture(scope="session")
2037
def apt_client(_api_client_set, bindings_cfg):
2138
"""Fixture for APT client."""
@@ -124,7 +141,7 @@ def _deb_remote_factory(url, **kwargs):
124141
return _deb_remote_factory
125142

126143

127-
@pytest.fixture
144+
@pytest.fixture(scope="class")
128145
def deb_sync_repository(apt_repository_api, monitor_task):
129146
"""Fixture that synchronizes a given repository with a given remote
130147
and returns the monitored task.
@@ -148,15 +165,15 @@ def _deb_sync_repository(remote, repo, mirror=False, optimize=True):
148165
return _deb_sync_repository
149166

150167

151-
@pytest.fixture
168+
@pytest.fixture(scope="class")
152169
def deb_fixture_server(gen_fixture_server):
153170
"""A fixture that spins up a local web server to serve test data."""
154171
p = Path(__file__).parent.absolute()
155172
fixture_path = p.joinpath("functional/data/")
156173
yield gen_fixture_server(fixture_path, None)
157174

158175

159-
@pytest.fixture
176+
@pytest.fixture(scope="class")
160177
def deb_get_fixture_server_url(deb_fixture_server):
161178
"""A fixture that provides the url of the local web server."""
162179

@@ -171,7 +188,7 @@ def _deb_get_fixture_server_url(repo_name=DEB_FIXTURE_STANDARD_REPOSITORY_NAME):
171188
return _deb_get_fixture_server_url
172189

173190

174-
@pytest.fixture
191+
@pytest.fixture(scope="class")
175192
def deb_init_and_sync(
176193
apt_repository_api,
177194
deb_get_fixture_server_url,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
3+
4+
@pytest.mark.parallel
5+
class TestPackageVersionFilter:
6+
@pytest.fixture(scope="class")
7+
def repository(self, deb_init_and_sync):
8+
_repository, _ = deb_init_and_sync()
9+
return _repository
10+
11+
@pytest.mark.parametrize(
12+
"filter,count",
13+
[
14+
pytest.param({"version": "1.0"}, 4, id="exact"),
15+
pytest.param({"version__ne": "1.0"}, 0, id="ne"),
16+
pytest.param({"version__gt": "1.0~"}, 4, id="gt with tilde"),
17+
pytest.param({"version__gt": "1.0"}, 0, id="gt"),
18+
pytest.param({"version__gt": "1.0+"}, 0, id="gt with plus"),
19+
pytest.param({"version__gte": "1.0~"}, 4, id="gte with tilde"),
20+
pytest.param({"version__gte": "1.0"}, 4, id="gte"),
21+
pytest.param({"version__gte": "1.0+"}, 0, id="gte with plus"),
22+
pytest.param({"version__lt": "1.0~"}, 0, id="lt with tilde"),
23+
pytest.param({"version__lt": "1.0"}, 0, id="lt"),
24+
pytest.param({"version__lt": "1.0+"}, 4, id="lt with plus"),
25+
pytest.param({"version__lte": "1.0~"}, 0, id="lte with tilde"),
26+
pytest.param({"version__lte": "1.0"}, 4, id="lte"),
27+
pytest.param({"version__lte": "1.0+"}, 4, id="lte with plus"),
28+
],
29+
)
30+
def test_returns_a_certain_count_of_entries(self, deb_bindings, repository, filter, count):
31+
"""Verify that Packages can be filtered by versions."""
32+
# Query content units with filters
33+
result = deb_bindings.ContentPackagesApi.list(
34+
repository_version=repository.latest_version_href, **filter
35+
)
36+
assert result.count == count

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ requires-python = ">=3.11"
2828
dependencies = [
2929
# All things django and asyncio are deliberately left to pulpcore
3030
# Example transitive requirements: asgiref, asyncio, aiohttp
31-
"pulpcore>=3.75.0,<3.100",
31+
"pulpcore>=3.85.0,<3.100",
3232
"python-debian>=0.1.44,<0.2.0",
3333
"python-gnupg>=0.5,<0.6",
3434
"jsonschema>=4.6,<5.0",

0 commit comments

Comments
 (0)