Skip to content

Commit 3d8d6fb

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

File tree

7 files changed

+247
-7
lines changed

7 files changed

+247
-7
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
rest bpchar;
31+
epoch bpchar;
32+
version bpchar;
33+
revision bpchar;
34+
BEGIN
35+
IF position(':' IN value) > 0
36+
THEN
37+
epoch := split_part(value, ':', 1);
38+
rest := split_part(value, ':', 2);
39+
ELSE
40+
epoch := '0';
41+
rest := value;
42+
END IF;
43+
IF position('-' IN rest) > 0
44+
THEN
45+
version := split_part(rest, '-', -2);
46+
revision := split_part(rest, '-', -1);
47+
ELSE
48+
version := rest;
49+
revision := '';
50+
END IF;
51+
return (epoch || '\\ufffd' || version || '\\u0000\\ufffd' || revision || '\\u0000', value)::debver;
52+
END;
53+
$$
54+
LANGUAGE plpgsql
55+
IMMUTABLE
56+
RETURNS NULL ON NULL INPUT;
57+
58+
CREATE FUNCTION text(version debver) RETURNS text
59+
AS $$
60+
BEGIN
61+
RETURN version.value;
62+
END;
63+
$$
64+
LANGUAGE plpgsql
65+
IMMUTABLE
66+
RETURNS NULL ON NULL INPUT;
67+
68+
CREATE CAST (text AS debver) WITH FUNCTION debver(bpchar) AS IMPLICIT;
69+
CREATE CAST (varchar AS debver) WITH FUNCTION debver(bpchar) AS IMPLICIT;
70+
CREATE CAST (bpchar AS debver) WITH FUNCTION debver(bpchar) AS IMPLICIT;
71+
CREATE CAST (debver AS text) WITH FUNCTION text(debver) AS IMPLICIT;
72+
CREATE CAST (debver AS varchar) WITH FUNCTION text(debver) AS IMPLICIT;
73+
CREATE CAST (debver AS bpchar) WITH FUNCTION text(debver) AS IMPLICIT;
74+
75+
CREATE FUNCTION debver_eq(a debver, b debver) RETURNS boolean
76+
LANGUAGE sql
77+
IMMUTABLE
78+
RETURNS NULL ON NULL INPUT
79+
RETURN a.sort_key = b.sort_key;
80+
81+
CREATE FUNCTION debver_neq(a debver, b debver) RETURNS boolean
82+
LANGUAGE sql
83+
IMMUTABLE
84+
RETURNS NULL ON NULL INPUT
85+
RETURN a.sort_key <> b.sort_key;
86+
87+
CREATE FUNCTION debver_lt(a debver, b debver) RETURNS boolean
88+
LANGUAGE sql
89+
IMMUTABLE
90+
RETURNS NULL ON NULL INPUT
91+
RETURN a.sort_key < b.sort_key;
92+
93+
CREATE FUNCTION debver_lte(a debver, b debver) RETURNS boolean
94+
LANGUAGE sql
95+
IMMUTABLE
96+
RETURNS NULL ON NULL INPUT
97+
RETURN a.sort_key <= b.sort_key;
98+
99+
CREATE FUNCTION debver_gte(a debver, b debver) RETURNS boolean
100+
LANGUAGE sql
101+
IMMUTABLE
102+
RETURNS NULL ON NULL INPUT
103+
RETURN a.sort_key >= b.sort_key;
104+
105+
CREATE FUNCTION debver_gt(a debver, b debver) RETURNS boolean
106+
LANGUAGE sql
107+
IMMUTABLE
108+
RETURNS NULL ON NULL INPUT
109+
RETURN a.sort_key > b.sort_key;
110+
111+
CREATE OPERATOR = (
112+
LEFTARG = debver,
113+
RIGHTARG = debver,
114+
FUNCTION = debver_eq
115+
);
116+
117+
CREATE OPERATOR <> (
118+
LEFTARG = debver,
119+
RIGHTARG = debver,
120+
FUNCTION = debver_neq
121+
);
122+
123+
CREATE OPERATOR < (
124+
LEFTARG = debver,
125+
RIGHTARG = debver,
126+
FUNCTION = debver_lt
127+
);
128+
129+
CREATE OPERATOR <= (
130+
LEFTARG = debver,
131+
RIGHTARG = debver,
132+
FUNCTION = debver_lte
133+
);
134+
135+
CREATE OPERATOR >= (
136+
LEFTARG = debver,
137+
RIGHTARG = debver,
138+
FUNCTION = debver_gte
139+
);
140+
141+
CREATE OPERATOR > (
142+
LEFTARG = debver,
143+
RIGHTARG = debver,
144+
FUNCTION = debver_gt
145+
);
146+
""",
147+
reverse_sql="""
148+
DROP TYPE IF EXISTS debver CASCADE;
149+
DROP COLLATION IF EXISTS debver;
150+
""",
151+
),
152+
migrations.AlterField(
153+
model_name="installerpackage",
154+
name="version",
155+
field=pulp_deb.fields.DebVersionField(),
156+
),
157+
migrations.AlterField(
158+
model_name="package",
159+
name="version",
160+
field=pulp_deb.fields.DebVersionField(),
161+
),
162+
]

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", "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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import pytest
2+
3+
4+
class TestPackageVersionFilter:
5+
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__gt": "1.0~"}, 4, id="gt with tilde"),
16+
pytest.param({"version__gt": "1.0"}, 0, id="gt"),
17+
pytest.param({"version__gt": "1.0+"}, 0, id="gt with plus"),
18+
pytest.param({"version__gte": "1.0~"}, 4, id="gte with tilde"),
19+
pytest.param({"version__gte": "1.0"}, 4, id="gte"),
20+
pytest.param({"version__gte": "1.0+"}, 0, id="gte with plus"),
21+
pytest.param({"version__lt": "1.0~"}, 0, id="lt with tilde"),
22+
pytest.param({"version__lt": "1.0"}, 0, id="lt"),
23+
pytest.param({"version__lt": "1.0+"}, 4, id="lt with plus"),
24+
pytest.param({"version__lte": "1.0~"}, 0, id="lte with tilde"),
25+
pytest.param({"version__lte": "1.0"}, 4, id="lte"),
26+
pytest.param({"version__lte": "1.0+"}, 4, id="lte with plus"),
27+
],
28+
)
29+
def test_returns_a_certain_count_of_entries(self, deb_bindings, repository, filter, count):
30+
"""Verify that Packages can be filtered by versions."""
31+
# Query content units with filters
32+
result = deb_bindings.ContentPackagesApi.list(
33+
repository_version=repository.latest_version_href, **filter
34+
)
35+
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)