sqlalchemy: use DATETIME(fsp=6) rather than DECIMAL

This migrates the old DECIMAL based format to the new DATETIME format available
in recent versions of MySQL.

Change-Id: I5dc7a7c2586feec72a1a2b13865e353a844a1785
This commit is contained in:
Julien Danjou 2016-09-19 17:34:51 +02:00
parent e8eafdbba7
commit d1391f7fa0
5 changed files with 97 additions and 133 deletions

View File

@ -29,10 +29,25 @@ depends_on = None
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import types
import aodh.storage.sqlalchemy.models import aodh.storage.sqlalchemy.models
class PreciseTimestamp(types.TypeDecorator):
"""Represents a timestamp precise to the microsecond."""
impl = sa.DateTime
def load_dialect_impl(self, dialect):
if dialect.name == 'mysql':
return dialect.type_descriptor(
types.DECIMAL(precision=20,
scale=6,
asdecimal=True))
return dialect.type_descriptor(self.impl)
def upgrade(): def upgrade():
op.create_table( op.create_table(
'alarm_history', 'alarm_history',
@ -44,7 +59,7 @@ def upgrade():
sa.Column('type', sa.String(length=20), nullable=True), sa.Column('type', sa.String(length=20), nullable=True),
sa.Column('detail', sa.Text(), nullable=True), sa.Column('detail', sa.Text(), nullable=True),
sa.Column('timestamp', sa.Column('timestamp',
aodh.storage.sqlalchemy.models.PreciseTimestamp(), PreciseTimestamp(),
nullable=True), nullable=True),
sa.PrimaryKeyConstraint('event_id') sa.PrimaryKeyConstraint('event_id')
) )
@ -60,13 +75,13 @@ def upgrade():
sa.Column('severity', sa.String(length=50), nullable=True), sa.Column('severity', sa.String(length=50), nullable=True),
sa.Column('description', sa.Text(), nullable=True), sa.Column('description', sa.Text(), nullable=True),
sa.Column('timestamp', sa.Column('timestamp',
aodh.storage.sqlalchemy.models.PreciseTimestamp(), PreciseTimestamp(),
nullable=True), nullable=True),
sa.Column('user_id', sa.String(length=128), nullable=True), sa.Column('user_id', sa.String(length=128), nullable=True),
sa.Column('project_id', sa.String(length=128), nullable=True), sa.Column('project_id', sa.String(length=128), nullable=True),
sa.Column('state', sa.String(length=255), nullable=True), sa.Column('state', sa.String(length=255), nullable=True),
sa.Column('state_timestamp', sa.Column('state_timestamp',
aodh.storage.sqlalchemy.models.PreciseTimestamp(), PreciseTimestamp(),
nullable=True), nullable=True),
sa.Column('ok_actions', sa.Column('ok_actions',
aodh.storage.sqlalchemy.models.JSONEncodedDict(), aodh.storage.sqlalchemy.models.JSONEncodedDict(),

View File

@ -0,0 +1,68 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2016 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""precisetimestamp_to_datetime
Revision ID: 367aadf5485f
Revises: f8c31b1ffe11
Create Date: 2016-09-19 16:43:34.379029
"""
# revision identifiers, used by Alembic.
revision = '367aadf5485f'
down_revision = 'f8c31b1ffe11'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
from sqlalchemy import func
from aodh.storage.sqlalchemy import models
def upgrade():
bind = op.get_bind()
if bind and bind.engine.name == "mysql":
# NOTE(jd) So that crappy engine that is MySQL does not have "ALTER
# TABLE … USING …". We need to copy everything and convert…
for table_name, column_name in (("alarm", "timestamp"),
("alarm", "state_timestamp"),
("alarm_change", "timestamp")):
existing_type = sa.types.DECIMAL(
precision=20, scale=6, asdecimal=True)
existing_col = sa.Column(
column_name,
existing_type,
nullable=True)
temp_col = sa.Column(
column_name + "_ts",
models.TimestampUTC(),
nullable=True)
op.add_column(table_name, temp_col)
t = sa.sql.table(table_name, existing_col, temp_col)
op.execute(t.update().values(
**{column_name + "_ts": func.from_unixtime(existing_col)}))
op.drop_column(table_name, column_name)
op.alter_column(table_name,
column_name + "_ts",
nullable=True,
type_=models.TimestampUTC(),
existing_nullable=True,
existing_type=existing_type,
new_column_name=column_name)

View File

@ -13,16 +13,12 @@
""" """
SQLAlchemy models for aodh data. SQLAlchemy models for aodh data.
""" """
import calendar
import datetime
import decimal
import json import json
from oslo_utils import timeutils from oslo_utils import timeutils
from oslo_utils import units
import six import six
from sqlalchemy import Column, String, Index, Boolean, Text, DateTime from sqlalchemy import Column, String, Index, Boolean, Text, DateTime
from sqlalchemy.dialects.mysql import DECIMAL from sqlalchemy.dialects import mysql
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.types import TypeDecorator from sqlalchemy.types import TypeDecorator
@ -45,48 +41,15 @@ class JSONEncodedDict(TypeDecorator):
return value return value
class PreciseTimestamp(TypeDecorator): class TimestampUTC(TypeDecorator):
"""Represents a timestamp precise to the microsecond.""" """Represents a timestamp precise to the microsecond."""
impl = DateTime impl = DateTime
def load_dialect_impl(self, dialect): def load_dialect_impl(self, dialect):
if dialect.name == 'mysql': if dialect.name == 'mysql':
return dialect.type_descriptor(DECIMAL(precision=20, return dialect.type_descriptor(mysql.DATETIME(fsp=6))
scale=6, return self.impl
asdecimal=True))
return dialect.type_descriptor(self.impl)
@staticmethod
def process_bind_param(value, dialect):
if value is None:
return value
elif dialect.name == 'mysql':
decimal.getcontext().prec = 30
return (
decimal.Decimal(
str(calendar.timegm(value.utctimetuple()))) +
(decimal.Decimal(str(value.microsecond)) /
decimal.Decimal("1000000.0"))
)
return value
def compare_against_backend(self, dialect, conn_type):
if dialect.name == 'mysql':
return issubclass(type(conn_type), DECIMAL)
return issubclass(type(conn_type), DateTime)
@staticmethod
def process_result_value(value, dialect):
if value is None:
return value
elif dialect.name == 'mysql':
integer = int(value)
micro = (value
- decimal.Decimal(integer)) * decimal.Decimal(units.M)
daittyme = datetime.datetime.utcfromtimestamp(integer)
return daittyme.replace(microsecond=int(round(micro)))
return value
class AodhBase(object): class AodhBase(object):
@ -125,13 +88,13 @@ class Alarm(Base):
type = Column(String(50)) type = Column(String(50))
severity = Column(String(50)) severity = Column(String(50))
description = Column(Text) description = Column(Text)
timestamp = Column(PreciseTimestamp, default=lambda: timeutils.utcnow()) timestamp = Column(TimestampUTC, default=lambda: timeutils.utcnow())
user_id = Column(String(128)) user_id = Column(String(128))
project_id = Column(String(128)) project_id = Column(String(128))
state = Column(String(255)) state = Column(String(255))
state_timestamp = Column(PreciseTimestamp, state_timestamp = Column(TimestampUTC,
default=lambda: timeutils.utcnow()) default=lambda: timeutils.utcnow())
ok_actions = Column(JSONEncodedDict) ok_actions = Column(JSONEncodedDict)
@ -156,5 +119,5 @@ class AlarmChange(Base):
user_id = Column(String(128)) user_id = Column(String(128))
type = Column(String(20)) type = Column(String(20))
detail = Column(Text) detail = Column(Text)
timestamp = Column(PreciseTimestamp, default=lambda: timeutils.utcnow()) timestamp = Column(TimestampUTC, default=lambda: timeutils.utcnow())
severity = Column(String(50)) severity = Column(String(50))

View File

@ -1,86 +0,0 @@
#
# Copyright 2013 Rackspace Hosting
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import mock
from oslotest import base
import sqlalchemy
from sqlalchemy.dialects.mysql import DECIMAL
from sqlalchemy.types import NUMERIC
from aodh.storage.sqlalchemy import models
class PreciseTimestampTest(base.BaseTestCase):
@staticmethod
def fake_dialect(name):
def _type_descriptor_mock(desc):
if type(desc) == DECIMAL:
return NUMERIC(precision=desc.precision, scale=desc.scale)
else:
return desc
dialect = mock.MagicMock()
dialect.name = name
dialect.type_descriptor = _type_descriptor_mock
return dialect
def setUp(self):
super(PreciseTimestampTest, self).setUp()
self._mysql_dialect = self.fake_dialect('mysql')
self._postgres_dialect = self.fake_dialect('postgres')
self._type = models.PreciseTimestamp()
self._date = datetime.datetime(2012, 7, 2, 10, 44)
def test_load_dialect_impl_mysql(self):
result = self._type.load_dialect_impl(self._mysql_dialect)
self.assertEqual(NUMERIC, type(result))
self.assertEqual(20, result.precision)
self.assertEqual(6, result.scale)
self.assertTrue(result.asdecimal)
def test_load_dialect_impl_postgres(self):
result = self._type.load_dialect_impl(self._postgres_dialect)
self.assertEqual(sqlalchemy.DateTime, type(result))
def test_process_bind_param_store_datetime_postgres(self):
result = self._type.process_bind_param(self._date,
self._postgres_dialect)
self.assertEqual(self._date, result)
def test_process_bind_param_store_none_mysql(self):
result = self._type.process_bind_param(None, self._mysql_dialect)
self.assertIsNone(result)
def test_process_bind_param_store_none_postgres(self):
result = self._type.process_bind_param(None,
self._postgres_dialect)
self.assertIsNone(result)
def test_process_result_value_datetime_postgres(self):
result = self._type.process_result_value(self._date,
self._postgres_dialect)
self.assertEqual(self._date, result)
def test_process_result_value_none_mysql(self):
result = self._type.process_result_value(None,
self._mysql_dialect)
self.assertIsNone(result)
def test_process_result_value_none_postgres(self):
result = self._type.process_result_value(None,
self._postgres_dialect)
self.assertIsNone(result)

View File

@ -0,0 +1,4 @@
---
other:
- Aodh now leverages microseconds timestamps available since MySQL 5.6.4,
meaning it is now the minimum required version of MySQL.