2
0
Fork 0

fix: use true UUID type for Upgrades table primary key

hopefully can use this everywhere soon but let's start slow and test
This commit is contained in:
Lance Edgar 2024-11-30 19:59:59 -06:00
parent 8b6e32145c
commit 028c64fc12
9 changed files with 274 additions and 6 deletions

View file

@ -30,7 +30,7 @@ import sys
import warnings
from wuttjamaican.util import (load_entry_points, load_object,
make_title, make_uuid,
make_title, make_uuid, make_true_uuid,
progress_loop, resource_path)
@ -491,14 +491,44 @@ class AppHandler:
"""
return make_title(text)
def make_true_uuid(self):
"""
Generate a new v7 UUID value.
By default this simply calls
:func:`wuttjamaican.util.make_true_uuid()`.
:returns: :class:`python:uuid.UUID` instance
.. warning::
For now, callers should use this method when they want a
proper UUID instance, whereas :meth:`make_uuid()` will
always return a string.
However once all dependent logic has been refactored to
support proper UUID data type, then ``make_uuid()`` will
return those and this method will eventually be removed.
"""
return make_true_uuid()
def make_uuid(self):
"""
Generate a new UUID value.
Generate a new v7 UUID value.
By default this simply calls
:func:`wuttjamaican.util.make_uuid()`.
:returns: UUID value as 32-character string.
.. warning::
For now, this method always returns a string.
However once all dependent logic has been refactored to
support proper UUID data type, then this method will return
those and the :meth:`make_true_uuid()` method will
eventually be removed.
"""
return make_uuid()

View file

@ -40,5 +40,5 @@ def make_uuid(
"""
config = ctx.parent.wutta_config
app = config.get_app()
uuid = app.make_uuid()
uuid = app.make_true_uuid()
sys.stdout.write(f"{uuid}\n")

View file

@ -9,6 +9,7 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import wuttjamaican.db.util
${imports if imports else ""}
# revision identifiers, used by Alembic.

View file

@ -0,0 +1,66 @@
"""convert uuid types
Revision ID: 6be0ed225f4d
Revises: 6bf900765500
Create Date: 2024-11-30 17:03:08.930050
"""
import uuid as _uuid
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import wuttjamaican.db.util
# revision identifiers, used by Alembic.
revision: str = '6be0ed225f4d'
down_revision: Union[str, None] = '6bf900765500'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# upgrade (convert uuid)
op.add_column('upgrade', sa.Column('uuid_true', wuttjamaican.db.util.UUID(), nullable=True))
upgrade = sa.sql.table('upgrade',
sa.sql.column('uuid'),
sa.sql.column('uuid_true'))
engine = op.get_bind()
cursor = engine.execute(upgrade.select())
for row in cursor.fetchall():
if row['uuid']:
uuid_true = _uuid.UUID(row['uuid'])
engine.execute(upgrade.update()\
.where(upgrade.c.uuid == row['uuid'])\
.values({'uuid_true': uuid_true}))
op.drop_constraint('pk_upgrade', 'upgrade', type_='primary')
op.drop_column('upgrade', 'uuid')
op.alter_column('upgrade', 'uuid_true', new_column_name='uuid')
op.create_primary_key('pk_upgrade', 'upgrade', ['uuid'])
def downgrade() -> None:
# upgrade (convert uuid)
op.add_column('upgrade', sa.Column('uuid_str', sa.VARCHAR(length=32), nullable=True))
upgrade = sa.sql.table('upgrade',
sa.sql.column('uuid'),
sa.sql.column('uuid_str'))
engine = op.get_bind()
cursor = engine.execute(upgrade.select())
for row in cursor.fetchall():
if row['uuid']:
uuid_str = row['uuid'].hex
engine.execute(upgrade.update()\
.where(upgrade.c.uuid == row['uuid'])\
.values({'uuid_str': uuid_str}))
op.drop_constraint('pk_upgrade', 'upgrade', type_='primary')
op.drop_column('upgrade', 'uuid')
op.alter_column('upgrade', 'uuid_str', new_column_name='uuid')
op.create_primary_key('pk_upgrade', 'upgrade', ['uuid'])

View file

@ -31,6 +31,8 @@ from sqlalchemy import orm
from .base import Base, uuid_column, uuid_fk_column
from wuttjamaican.enum import UpgradeStatus
from wuttjamaican.db.util import UUID
from wuttjamaican.util import make_true_uuid
class Upgrade(Base):
@ -39,7 +41,7 @@ class Upgrade(Base):
"""
__tablename__ = 'upgrade'
uuid = uuid_column()
uuid = uuid_column(UUID(), default=make_true_uuid)
created = sa.Column(sa.DateTime(timezone=True), nullable=False,
default=datetime.datetime.now, doc="""

View file

@ -24,7 +24,10 @@
Database Utilities
"""
import uuid as _uuid
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from wuttjamaican.util import make_uuid
@ -57,6 +60,51 @@ class ModelBase:
return getattr(self, key)
class UUID(sa.types.TypeDecorator):
"""
Platform-independent UUID type.
Uses PostgreSQL's UUID type, otherwise uses CHAR(32), storing as
stringified hex values.
This type definition is based on example from the `SQLAlchemy
documentation
<https://docs.sqlalchemy.org/en/14/core/custom_types.html#backend-agnostic-guid-type>`_.
"""
impl = sa.CHAR
cache_ok = True
""" """ # nb. suppress sphinx autodoc for cache_ok
def load_dialect_impl(self, dialect):
""" """
if dialect.name == "postgresql":
return dialect.type_descriptor(PGUUID())
else:
return dialect.type_descriptor(sa.CHAR(32))
def process_bind_param(self, value, dialect):
""" """
if value is None:
return value
elif dialect.name == "postgresql":
return str(value)
else:
if not isinstance(value, _uuid.UUID):
return "%.32x" % _uuid.UUID(value).int
else:
# hexstring
return "%.32x" % value.int
def process_result_value(self, value, dialect):
""" """
if value is None:
return value
else:
if not isinstance(value, _uuid.UUID):
value = _uuid.UUID(value)
return value
def uuid_column(*args, **kwargs):
"""
Returns a UUID column for use as a table's primary key.

View file

@ -171,13 +171,43 @@ def make_title(text):
return ' '.join([x.capitalize() for x in words])
def make_true_uuid():
"""
Generate a new v7 UUID value.
:returns: :class:`python:uuid.UUID` instance
.. warning::
For now, callers should use this function when they want a
proper UUID instance, whereas :func:`make_uuid()` will always
return a string.
However once all dependent logic has been refactored to support
proper UUID data type, then ``make_uuid()`` will return those
and this function will eventually be removed.
"""
return uuid7()
# TODO: deprecate this logic, and reclaim this name
# but using the above logic
def make_uuid():
"""
Generate a universally-unique identifier.
Generate a new v7 UUID value.
:returns: A 32-character hex string.
.. warning::
For now, this function always returns a string.
However once all dependent logic has been refactored to support
proper UUID data type, then this function will return those and
the :func:`make_true_uuid()` function will eventually be
removed.
"""
return uuid7().hex
return make_true_uuid().hex
def parse_bool(value):

View file

@ -1,12 +1,16 @@
# -*- coding: utf-8; -*-
import uuid as _uuid
from unittest import TestCase
from unittest.mock import MagicMock
try:
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from wuttjamaican.db import util as mod
from wuttjamaican.db.model.base import Setting
from wuttjamaican.util import make_true_uuid
except ImportError:
pass
else:
@ -22,6 +26,88 @@ else:
self.assertEqual(setting['name'], 'foo')
class TestUUID(TestCase):
def test_load_dialect_impl(self):
typ = mod.UUID()
dialect = MagicMock()
# TODO: this doesn't really test anything, but gives us
# coverage at least..
# postgres
dialect.name = 'postgresql'
dialect.type_descriptor.return_value = 42
result = typ.load_dialect_impl(dialect)
self.assertTrue(dialect.type_descriptor.called)
self.assertEqual(result, 42)
# other
dialect.name = 'mysql'
dialect.type_descriptor.return_value = 43
dialect.type_descriptor.reset_mock()
result = typ.load_dialect_impl(dialect)
self.assertTrue(dialect.type_descriptor.called)
self.assertEqual(result, 43)
def test_process_bind_param_postgres(self):
typ = mod.UUID()
dialect = MagicMock()
dialect.name = 'postgresql'
# null
result = typ.process_bind_param(None, dialect)
self.assertIsNone(result)
# string
uuid_str = make_true_uuid().hex
result = typ.process_bind_param(uuid_str, dialect)
self.assertEqual(result, uuid_str)
# uuid
uuid_true = make_true_uuid()
result = typ.process_bind_param(uuid_true, dialect)
self.assertEqual(result, str(uuid_true))
def test_process_bind_param_other(self):
typ = mod.UUID()
dialect = MagicMock()
dialect.name = 'mysql'
# null
result = typ.process_bind_param(None, dialect)
self.assertIsNone(result)
# string
uuid_str = make_true_uuid().hex
result = typ.process_bind_param(uuid_str, dialect)
self.assertEqual(result, uuid_str)
# uuid
uuid_true = make_true_uuid()
result = typ.process_bind_param(uuid_true, dialect)
self.assertEqual(result, uuid_true.hex)
def test_process_result_value(self):
typ = mod.UUID()
dialect = MagicMock()
# null
result = typ.process_result_value(None, dialect)
self.assertIsNone(result)
# string
uuid_str = make_true_uuid().hex
result = typ.process_result_value(uuid_str, dialect)
self.assertIsInstance(result, _uuid.UUID)
self.assertEqual(result.hex, uuid_str)
# uuid
uuid_true = make_true_uuid()
result = typ.process_result_value(uuid_true, dialect)
self.assertIs(result, uuid_true)
class TestUUIDColumn(TestCase):
def test_basic(self):

View file

@ -5,6 +5,7 @@ import shutil
import sys
import tempfile
import warnings
import uuid as _uuid
from unittest import TestCase
from unittest.mock import patch, MagicMock
@ -385,6 +386,10 @@ app_title = WuttaTest
uuid = self.app.make_uuid()
self.assertEqual(len(uuid), 32)
def test_make_true_uuid(self):
uuid = self.app.make_true_uuid()
self.assertIsInstance(uuid, _uuid.UUID)
def test_progress_loop(self):
def act(obj, i):