Source code for ferrosoft.apps.ferrobase.services.idgenerator
from abc import ABC, abstractmethod
from psycopg import sql
from ferrosoft.apps.ferrobase.models import IdSequenceConfig, IDSequence
from ferrosoft.apps.ferrobase.tenant.utils import tenant_db_connection
[docs]
class IdGenerator(ABC):
"""Abstract sequence-based identifier generator.
Concrete subclasses bind a sequence to a backing store and produce
formatted IDs combining the configured prefix and zero-padded sequence
number.
"""
[docs]
@classmethod
@abstractmethod
def create(cls, name: IDSequence) -> "IdGenerator":
"""Create or load the generator for the sequence identified by ``name``."""
raise NotImplementedError
[docs]
@abstractmethod
def next_id(self) -> str:
"""Return the next formatted identifier from the underlying sequence."""
raise NotImplementedError
[docs]
@classmethod
def get_instance(cls, name: IDSequence):
"""Return the default :class:`IdGeneratorPostgres` for ``name``."""
return IdGeneratorPostgres.create(name)
[docs]
class IdGeneratorPostgres(IdGenerator):
"""PostgreSQL-backed ID generator using ``CREATE SEQUENCE``."""
def __init__(self, config: IdSequenceConfig):
"""Bind the generator to the persisted :class:`IdSequenceConfig`."""
self.config = config
@classmethod
def _sequence_name(cls, name: str) -> str:
"""Return the canonical sequence name ``ferrobase_idseq_<name_lower>``."""
return "ferrobase_idseq_%s" % name.lower()
[docs]
@classmethod
def create(cls, name: IDSequence) -> "IdGeneratorPostgres":
"""Persist a configuration if missing and ensure the SQL sequence exists.
The :class:`IdSequenceConfig` row is created in the master database
with a default prefix equal to ``name.label`` and seven digits of
padding. The corresponding PostgreSQL sequence is created in the
tenant database via ``CREATE SEQUENCE IF NOT EXISTS``.
"""
sequence_name = cls._sequence_name(name.name)
config = IdSequenceConfig.objects.filter(name=name.value).first()
if not config:
config = IdSequenceConfig.objects.create(
name=name, prefix=name.label, padding_amount=7
)
with tenant_db_connection().cursor() as cursor:
query = sql.SQL(
"""CREATE SEQUENCE IF NOT EXISTS {}
START 1
INCREMENT 1
MINVALUE 1
CACHE 1;
"""
).format(sql.Identifier(sequence_name))
cursor.execute(query)
return cls(config)
[docs]
def next_id(self) -> str:
"""Allocate the next sequence value and format it as ``prefix + padded_number``."""
sequence_name = self._sequence_name(IDSequence(self.config.name).name)
with tenant_db_connection().cursor() as cursor:
cursor.execute("SELECT nextval(%s)", [sequence_name])
next_val = cursor.fetchone()[0]
return "%s%s" % (
self.config.prefix,
str(next_val).zfill(self.config.padding_amount),
)