Skip to content

Commit 1a91e72

Browse files
committed
feat: adding alpha numeric sequence
1 parent a450473 commit 1a91e72

9 files changed

+154
-13
lines changed

.gitattributes

-1
This file was deleted.

LICENSE.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2013-present Jared Forsyth / Fabio Caccamo
3+
Copyright (c) 2024-present Loic Quertenmont
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# sequencefield
1818
simple model field taking it's value from a postgres sequence. This is an easy replacement for django autofield offering the following advantages:
1919
- Sequence could be shared among multiple models (db tables), so you can now have unique id among multiple django models
20+
- Possibility to generate alphanumeric id of the form "{PREFIX}_{ID}"
2021
- Unique Id could also be combined with data from other field to build complex id. One example is given that combine unique id with date information to offer an efficient table indexing/clustering for faster data retrieval when filtering on date. (Particularly useful with BRIN index)
2122

2223

@@ -51,6 +52,7 @@ class IntSequenceModel(models.Model):
5152
IntSequenceConstraint(
5253
name="%(app_label)s_%(class)s_custseq",
5354
sequence="int_custseq", #name of sequence to use
55+
drop=False, #avoid deleting the sequence if shared among multiple tables
5456
fields=["id"], #name of the field that should be populated by this sequence
5557
start=100, #first value of the sequence
5658
maxvalue=200 #max value allowed for the sequence, will raise error if we go above, use None for the maximum allowed value of the db
@@ -59,6 +61,26 @@ class IntSequenceModel(models.Model):
5961

6062
```
6163

64+
### Simple AlphaNumeric Example
65+
Just add an AlphaNumericSequenceField field(s) to your models like this.
66+
You can provide a "format" argument to define how to convert the number to char. The syntax is the one used in postgres to_char function ([see here](https://www.postgresql.org/docs/current/functions-formatting.html))
67+
68+
class AlphaNumericSequenceModel(models.Model):
69+
seqid = AlphaNumericSequenceField(
70+
primary_key=False, prefix="INV", format="FM000000"
71+
)
72+
73+
class Meta:
74+
constraints = [
75+
IntSequenceConstraint(
76+
name="%(app_label)s_%(class)s_custseq",
77+
sequence="alphanum_custseq", , #name of sequence to use
78+
drop=False, #avoid deleting the sequence if shared among multiple tables
79+
fields=["seqid"], #name of the field that should be populated by this sequence
80+
start=1,
81+
)
82+
]
83+
6284
### Advance Example
6385
Just add a sequence field(s) to your models like this:
6486

@@ -80,12 +102,19 @@ class BigIntSequenceModel(models.Model):
80102
BigIntSequenceConstraint(
81103
name="%(app_label)s_%(class)s_custseq",
82104
sequence="gdw_post_custseq", #name of the quence
105+
drop=False, #avoid deleting the sequence if shared among multiple tables
83106
fields=["seqid"], #field to be populated from this sequence
84107
start=1, #first value of the sequence
85108
)
86109
]
87110
```
88111

112+
---
113+
114+
## Remarks
115+
116+
Until we find a good solution to properly handle supression of a sequence shared among multiple tables,
117+
It's better to pass the flag drop=False in the SequenceConstraint. Otherwise a sequence that is still being used by another table might be deleted.
89118

90119
---
91120

sequencefield/constraints.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
class SequenceConstraint(BaseConstraint):
77
maxvalue: int | None = None
88

9-
def __init__(self, name=None, sequence="", maxvalue=None, fields=(), start=1):
9+
def __init__(
10+
self, name=None, sequence="", drop=True, maxvalue=None, fields=(), start=1
11+
):
1012
if not sequence:
1113
raise TypeError(
1214
f"{self.__class__.__name__}.__init__() missing 1 required keyword-only "
1315
f"argument: 'sequence'"
1416
)
1517

1618
self.fields = tuple(fields)
19+
self.drop = drop
1720
self.start = start
1821
self.sequence = sequence
1922
self.maxvalue = maxvalue if maxvalue else self.maxvalue
@@ -25,6 +28,7 @@ def __eq__(self, other):
2528
return (
2629
self.name == other.name
2730
and self.fields == other.fields
31+
and self.drop == other.drop
2832
and self.sequence == other.sequence
2933
and self.maxvalue == other.maxvalue
3034
)
@@ -33,6 +37,7 @@ def __eq__(self, other):
3337
def deconstruct(self):
3438
path, args, kwargs = super().deconstruct()
3539
kwargs["sequence"] = self.sequence
40+
kwargs["drop"] = self.drop
3641
kwargs["fields"] = self.fields
3742
kwargs["start"] = self.start
3843
kwargs["maxvalue"] = self.maxvalue
@@ -68,10 +73,13 @@ def remove_sql(self, model, schema_editor):
6873
assert schema_editor
6974
assert model
7075

71-
return schema_editor.execute(
72-
sql=f"""DROP SEQUENCE IF EXISTS {self.sequence}""",
73-
params=(),
74-
)
76+
if self.drop:
77+
return schema_editor.execute(
78+
sql=f"""DROP SEQUENCE IF EXISTS {self.sequence}""",
79+
params=(),
80+
)
81+
82+
return
7583

7684
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
7785
print("CALL VALIDATE")

sequencefield/fields.py

+40-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@
33

44
from django.core import checks
55
from django.db import models
6-
from django.db.models import BigIntegerField, Field, IntegerField, SmallIntegerField
6+
from django.db.models import (
7+
BigIntegerField,
8+
CharField,
9+
Field,
10+
IntegerField,
11+
SmallIntegerField,
12+
Value,
13+
)
714
from django.db.models.expressions import Expression
15+
from django.db.models.functions import Cast, Concat
816
from django.utils.functional import cached_property
917

1018
from .constraints import SequenceConstraint
11-
from .functions import LeftShift, NextSeqVal
19+
from .functions import LeftShift, NextSeqVal, ToChar
1220

1321

1422
class SequenceFieldMixin(Field):
@@ -73,6 +81,36 @@ class BigIntegerSequenceField(SequenceFieldMixin, BigIntegerField):
7381
pass
7482

7583

84+
class AlphaNumericSequenceField(SequenceFieldMixin, CharField):
85+
def __init__(self, *args, prefix, separator="_", format: str = "", **kwargs):
86+
super().__init__(*args, **kwargs)
87+
self.prefix = prefix
88+
self.separator = separator
89+
self.format = format
90+
91+
# needed to store the additional parameter
92+
def deconstruct(self):
93+
name, path, args, kwargs = super().deconstruct()
94+
if self.prefix:
95+
kwargs["prefix"] = self.prefix
96+
if self.separator:
97+
kwargs["separator"] = self.separator
98+
if self.format:
99+
kwargs["format"] = self.format
100+
return name, path, args, kwargs
101+
102+
def get_db_expression(self, model_instance) -> Expression:
103+
seqid = super().get_db_expression(model_instance)
104+
if self.format:
105+
seqid = ToChar(seqid, format=self.format)
106+
else:
107+
seqid = Cast(seqid, output_field=CharField())
108+
109+
return Concat(
110+
Value(self.prefix + self.separator), seqid, output_field=CharField()
111+
)
112+
113+
76114
class BigIntegerWithDateSequenceField(SequenceFieldMixin, BigIntegerField):
77115
def __init__(self, *args, datetime_field, **kwargs):
78116
super().__init__(*args, **kwargs)

sequencefield/functions.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from django.db.models import DateField
1+
from django.db.models import CharField, DateField
22
from django.db.models.expressions import Func
33

44

@@ -11,6 +11,17 @@ def __init__(self, sequence="", output_field=None):
1111
super().__init__(output_field=output_field, template=template)
1212

1313

14+
class ToChar(Func):
15+
template = ""
16+
arity = 1
17+
18+
def __init__(self, *expressions, format="", output_field=None):
19+
template = f"to_char(%(expressions)s, '{format}')"
20+
super().__init__(
21+
*expressions, output_field=output_field or CharField(), template=template
22+
)
23+
24+
1425
class LeftShift(Func):
1526
template = ""
1627
arity = 1

sequencefield/metadata.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
__email__ = "[email protected]"
1111
__license__ = "MIT"
1212
__title__ = "django-sequencefield"
13-
__version__ = "1.0.6"
13+
__version__ = "1.0.7"

tests/models.py

+37
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from sequencefield.constraints import BigIntSequenceConstraint, IntSequenceConstraint
44
from sequencefield.fields import (
5+
AlphaNumericSequenceField,
56
BigIntegerWithDateSequenceField,
67
IntegerSequenceField,
78
)
@@ -42,6 +43,42 @@ def __str__(self) -> str:
4243
return f"seqid:{self.seqid}"
4344

4445

46+
class AlphaNumericSequenceModelA(models.Model):
47+
seqid = AlphaNumericSequenceField(primary_key=True, prefix="INV") # type: ignore
48+
49+
class Meta:
50+
constraints = [
51+
IntSequenceConstraint(
52+
name="%(app_label)s_%(class)s_custseq",
53+
sequence="alphanumA_custseq",
54+
fields=["seqid"],
55+
start=1,
56+
)
57+
]
58+
59+
def __str__(self) -> str:
60+
return f"seqid:{self.seqid}"
61+
62+
63+
class AlphaNumericSequenceModelB(models.Model):
64+
seqid = AlphaNumericSequenceField(
65+
primary_key=False, prefix="INV", format="FM000000"
66+
) # type: ignore
67+
68+
class Meta:
69+
constraints = [
70+
IntSequenceConstraint(
71+
name="%(app_label)s_%(class)s_custseq",
72+
sequence="alphanumB_custseq",
73+
fields=["seqid"],
74+
start=1,
75+
)
76+
]
77+
78+
def __str__(self) -> str:
79+
return f"seqid:{self.seqid}"
80+
81+
4582
class BigIntSequenceModel(models.Model):
4683
id = models.BigIntegerField(primary_key=True, auto_created=False)
4784
seqid = BigIntegerWithDateSequenceField(datetime_field="created") # type: ignore

tests/test_fields.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55

66
from sequencefield.functions import DateFromId, RightShift
77
from tests.models import (
8+
AlphaNumericSequenceModelA,
9+
AlphaNumericSequenceModelB,
810
BigIntSequenceModel,
911
IntSequenceModelA,
1012
IntSequenceModelB,
1113
)
1214

1315

14-
class ColorFieldTestCase(TestCase):
16+
class SequenceFieldTestCase(TestCase):
1517
def setUp(self):
1618
pass
1719

@@ -36,6 +38,22 @@ def test_model_id(self):
3638
a2.save()
3739
self.assertEqual(a2.seqid, 102)
3840

41+
def test_model_alphanumeric_wfmt(self):
42+
"""
43+
Checking that alphanumeric model works fine
44+
"""
45+
b = AlphaNumericSequenceModelB()
46+
b.save()
47+
self.assertEqual(b.seqid, "INV_000001")
48+
49+
def test_model_alphanumeric_wofmt(self):
50+
"""
51+
Checking that alphanumeric model works fine
52+
"""
53+
a = AlphaNumericSequenceModelA()
54+
a.save()
55+
self.assertEqual(a.seqid, "INV_1")
56+
3957
def test_model_idWithDate(self):
4058
"""
4159
Checking that id we can generate an id containing a data value inside
@@ -49,9 +67,10 @@ def test_model_idWithDate(self):
4967
data = BigIntSequenceModel.objects.values(
5068
epochdays=RightShift("seqid", bits=48)
5169
).first()
52-
print("epochdays", data)
70+
assert data
5371
self.assertEqual(data["epochdays"], math.floor(dt.timestamp() / 86400))
5472

5573
# verify that we can extract the date from the id itself
5674
data = BigIntSequenceModel.objects.values(date=DateFromId("seqid")).first()
75+
assert data
5776
self.assertEqual(data["date"], dt.date())

0 commit comments

Comments
 (0)