2017-08-16 23:48:18 +03:00
|
|
|
from __future__ import unicode_literals
|
2016-07-04 17:01:51 +03:00
|
|
|
import unittest
|
|
|
|
|
2020-06-06 11:07:25 +03:00
|
|
|
from infi.clickhouse_orm.database import Database, ServerError
|
2020-06-06 20:56:32 +03:00
|
|
|
from infi.clickhouse_orm.models import Model, BufferModel, Constraint, Index
|
2016-07-04 17:01:51 +03:00
|
|
|
from infi.clickhouse_orm.fields import *
|
|
|
|
from infi.clickhouse_orm.engines import *
|
|
|
|
from infi.clickhouse_orm.migrations import MigrationHistory
|
|
|
|
|
2019-06-23 11:53:58 +03:00
|
|
|
from enum import Enum
|
2016-07-04 17:01:51 +03:00
|
|
|
# Add tests to path so that migrations will be importable
|
|
|
|
import sys, os
|
|
|
|
sys.path.append(os.path.dirname(__file__))
|
|
|
|
|
2016-08-31 15:26:28 +03:00
|
|
|
|
2016-07-04 17:01:51 +03:00
|
|
|
import logging
|
|
|
|
logging.basicConfig(level=logging.DEBUG, format='%(message)s')
|
|
|
|
logging.getLogger("requests").setLevel(logging.WARNING)
|
|
|
|
|
|
|
|
|
|
|
|
class MigrationsTestCase(unittest.TestCase):
|
|
|
|
|
|
|
|
def setUp(self):
|
2019-06-13 08:12:56 +03:00
|
|
|
self.database = Database('test-db', log_statements=True)
|
2016-07-04 17:01:51 +03:00
|
|
|
self.database.drop_table(MigrationHistory)
|
|
|
|
|
2016-08-31 15:26:28 +03:00
|
|
|
def tearDown(self):
|
|
|
|
self.database.drop_database()
|
|
|
|
|
2020-06-06 20:56:32 +03:00
|
|
|
def table_exists(self, model_class):
|
2016-07-11 16:17:49 +03:00
|
|
|
query = "EXISTS TABLE $db.`%s`" % model_class.table_name()
|
2016-07-04 17:01:51 +03:00
|
|
|
return next(self.database.select(query)).result == 1
|
|
|
|
|
2020-06-06 20:56:32 +03:00
|
|
|
def get_table_fields(self, model_class):
|
2016-07-04 17:01:51 +03:00
|
|
|
query = "DESC `%s`.`%s`" % (self.database.db_name, model_class.table_name())
|
|
|
|
return [(row.name, row.type) for row in self.database.select(query)]
|
|
|
|
|
2020-06-06 20:56:32 +03:00
|
|
|
def get_table_def(self, model_class):
|
|
|
|
return self.database.raw('SHOW CREATE TABLE $db.`%s`' % self.table_name)
|
|
|
|
|
2016-07-04 17:01:51 +03:00
|
|
|
def test_migrations(self):
|
2016-08-31 15:26:28 +03:00
|
|
|
# Creation and deletion of table
|
2016-07-04 17:01:51 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 1)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(Model1))
|
2016-07-04 17:01:51 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 2)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertFalse(self.table_exists(Model1))
|
2016-07-04 17:01:51 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 3)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(Model1))
|
2016-08-31 15:26:28 +03:00
|
|
|
# Adding, removing and altering simple fields
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertEqual(self.get_table_fields(Model1), [('date', 'Date'), ('f1', 'Int32'), ('f2', 'String')])
|
2016-07-04 17:01:51 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 4)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertEqual(self.get_table_fields(Model2), [('date', 'Date'), ('f1', 'Int32'), ('f3', 'Float32'), ('f2', 'String'), ('f4', 'String'), ('f5', 'Array(UInt64)')])
|
2016-07-04 17:01:51 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 5)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertEqual(self.get_table_fields(Model3), [('date', 'Date'), ('f1', 'Int64'), ('f3', 'Float64'), ('f4', 'String')])
|
2016-08-31 15:26:28 +03:00
|
|
|
# Altering enum fields
|
|
|
|
self.database.migrate('tests.sample_migrations', 6)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(EnumModel1))
|
|
|
|
self.assertEqual(self.get_table_fields(EnumModel1),
|
2016-08-31 15:26:28 +03:00
|
|
|
[('date', 'Date'), ('f1', "Enum8('dog' = 1, 'cat' = 2, 'cow' = 3)")])
|
|
|
|
self.database.migrate('tests.sample_migrations', 7)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(EnumModel1))
|
|
|
|
self.assertEqual(self.get_table_fields(EnumModel2),
|
2016-08-31 15:26:28 +03:00
|
|
|
[('date', 'Date'), ('f1', "Enum16('dog' = 1, 'cat' = 2, 'horse' = 3, 'pig' = 4)")])
|
2017-09-10 15:46:55 +03:00
|
|
|
# Materialized fields and alias fields
|
2017-01-26 13:42:33 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 8)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(MaterializedModel))
|
|
|
|
self.assertEqual(self.get_table_fields(MaterializedModel),
|
2017-01-26 13:42:33 +03:00
|
|
|
[('date_time', "DateTime"), ('date', 'Date')])
|
|
|
|
self.database.migrate('tests.sample_migrations', 9)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(AliasModel))
|
|
|
|
self.assertEqual(self.get_table_fields(AliasModel),
|
2017-01-26 13:42:33 +03:00
|
|
|
[('date', 'Date'), ('date_alias', "Date")])
|
2017-09-10 15:46:55 +03:00
|
|
|
# Buffer models creation and alteration
|
2017-09-20 08:08:07 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 10)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(Model4))
|
|
|
|
self.assertTrue(self.table_exists(Model4Buffer))
|
|
|
|
self.assertEqual(self.get_table_fields(Model4), [('date', 'Date'), ('f1', 'Int32'), ('f2', 'String')])
|
|
|
|
self.assertEqual(self.get_table_fields(Model4Buffer), [('date', 'Date'), ('f1', 'Int32'), ('f2', 'String')])
|
2017-09-10 15:46:55 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 11)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertEqual(self.get_table_fields(Model4), [('date', 'Date'), ('f3', 'DateTime'), ('f2', 'String')])
|
|
|
|
self.assertEqual(self.get_table_fields(Model4Buffer), [('date', 'Date'), ('f3', 'DateTime'), ('f2', 'String')])
|
2017-01-26 13:42:33 +03:00
|
|
|
|
2017-10-02 13:49:53 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 12)
|
2017-09-20 08:08:07 +03:00
|
|
|
self.assertEqual(self.database.count(Model3), 3)
|
|
|
|
data = [item.f1 for item in self.database.select('SELECT f1 FROM $table ORDER BY f1', model_class=Model3)]
|
|
|
|
self.assertListEqual(data, [1, 2, 3])
|
|
|
|
|
2017-10-02 13:49:53 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 13)
|
2017-09-20 08:08:07 +03:00
|
|
|
self.assertEqual(self.database.count(Model3), 4)
|
|
|
|
data = [item.f1 for item in self.database.select('SELECT f1 FROM $table ORDER BY f1', model_class=Model3)]
|
|
|
|
self.assertListEqual(data, [1, 2, 3, 4])
|
|
|
|
|
2017-10-30 16:49:25 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 14)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(MaterializedModel1))
|
|
|
|
self.assertEqual(self.get_table_fields(MaterializedModel1),
|
2019-01-28 11:08:07 +03:00
|
|
|
[('date_time', 'DateTime'), ('int_field', 'Int8'), ('date', 'Date'), ('int_field_plus_one', 'Int8')])
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(AliasModel1))
|
|
|
|
self.assertEqual(self.get_table_fields(AliasModel1),
|
2019-01-28 11:08:07 +03:00
|
|
|
[('date', 'Date'), ('int_field', 'Int8'), ('date_alias', 'Date'), ('int_field_plus_one', 'Int8')])
|
2020-06-06 11:07:25 +03:00
|
|
|
# Codecs and low cardinality
|
2019-06-24 12:31:19 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 15)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(Model4_compressed))
|
2019-06-24 14:20:18 +03:00
|
|
|
if self.database.has_low_cardinality_support:
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertEqual(self.get_table_fields(Model2LowCardinality),
|
2019-06-24 14:20:18 +03:00
|
|
|
[('date', 'Date'), ('f1', 'LowCardinality(Int32)'), ('f3', 'LowCardinality(Float32)'),
|
|
|
|
('f2', 'LowCardinality(String)'), ('f4', 'LowCardinality(Nullable(String))'), ('f5', 'Array(LowCardinality(UInt64))')])
|
|
|
|
else:
|
|
|
|
logging.warning('No support for low cardinality')
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertEqual(self.get_table_fields(Model2),
|
2019-06-24 14:20:18 +03:00
|
|
|
[('date', 'Date'), ('f1', 'Int32'), ('f3', 'Float32'), ('f2', 'String'), ('f4', 'Nullable(String)'),
|
|
|
|
('f5', 'Array(UInt64)')])
|
2017-10-10 10:23:31 +03:00
|
|
|
|
2020-06-06 11:07:25 +03:00
|
|
|
if self.database.server_version >= (19, 14, 3, 3):
|
2020-06-06 20:56:32 +03:00
|
|
|
# Creating constraints
|
2020-06-06 11:07:25 +03:00
|
|
|
self.database.migrate('tests.sample_migrations', 16)
|
2020-06-06 20:56:32 +03:00
|
|
|
self.assertTrue(self.table_exists(ModelWithConstraints))
|
2020-06-06 11:07:25 +03:00
|
|
|
self.database.insert([ModelWithConstraints(f1=101, f2='a')])
|
|
|
|
with self.assertRaises(ServerError):
|
|
|
|
self.database.insert([ModelWithConstraints(f1=99, f2='a')])
|
|
|
|
with self.assertRaises(ServerError):
|
|
|
|
self.database.insert([ModelWithConstraints(f1=101, f2='x')])
|
|
|
|
# Modifying constraints
|
|
|
|
self.database.migrate('tests.sample_migrations', 17)
|
|
|
|
self.database.insert([ModelWithConstraints(f1=99, f2='a')])
|
|
|
|
with self.assertRaises(ServerError):
|
|
|
|
self.database.insert([ModelWithConstraints(f1=101, f2='a')])
|
|
|
|
with self.assertRaises(ServerError):
|
|
|
|
self.database.insert([ModelWithConstraints(f1=99, f2='x')])
|
2016-07-04 17:01:51 +03:00
|
|
|
|
2020-06-06 21:21:18 +03:00
|
|
|
if self.database.server_version >= (20, 1, 2, 4):
|
2020-06-06 20:56:32 +03:00
|
|
|
# Creating indexes
|
|
|
|
self.database.migrate('tests.sample_migrations', 18)
|
|
|
|
self.assertTrue(self.table_exists(ModelWithIndex))
|
|
|
|
self.assertIn('INDEX `index`', self.get_table_def())
|
|
|
|
self.assertIn('INDEX another_index', self.get_table_def())
|
|
|
|
# Modifying indexes
|
|
|
|
self.database.migrate('tests.sample_migrations', 19)
|
|
|
|
self.assertNotIn('INDEX `index`', self.get_table_def())
|
|
|
|
self.assertIn('INDEX index2', self.get_table_def())
|
|
|
|
self.assertIn('INDEX another_index', self.get_table_def())
|
|
|
|
|
|
|
|
|
2016-07-04 17:01:51 +03:00
|
|
|
# Several different models with the same table name, to simulate a table that changes over time
|
|
|
|
|
|
|
|
class Model1(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Int32Field()
|
|
|
|
f2 = StringField()
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'mig'
|
|
|
|
|
|
|
|
|
|
|
|
class Model2(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Int32Field()
|
|
|
|
f3 = Float32Field()
|
|
|
|
f2 = StringField()
|
|
|
|
f4 = StringField()
|
2018-08-13 08:35:26 +03:00
|
|
|
f5 = ArrayField(UInt64Field()) # addition of an array field
|
2016-07-04 17:01:51 +03:00
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'mig'
|
|
|
|
|
|
|
|
|
|
|
|
class Model3(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Int64Field() # changed from Int32
|
|
|
|
f3 = Float64Field() # changed from Float32
|
|
|
|
f4 = StringField()
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'mig'
|
|
|
|
|
2016-08-31 15:26:28 +03:00
|
|
|
|
|
|
|
class EnumModel1(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Enum8Field(Enum('SomeEnum1', 'dog cat cow'))
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'enum_mig'
|
|
|
|
|
|
|
|
|
|
|
|
class EnumModel2(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Enum16Field(Enum('SomeEnum2', 'dog cat horse pig')) # changed type and values
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'enum_mig'
|
2017-01-26 13:42:33 +03:00
|
|
|
|
|
|
|
|
|
|
|
class MaterializedModel(Model):
|
|
|
|
date_time = DateTimeField()
|
2017-01-27 08:42:37 +03:00
|
|
|
date = DateField(materialized='toDate(date_time)')
|
2017-01-26 13:42:33 +03:00
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'materalized_date'
|
|
|
|
|
|
|
|
|
2017-10-10 10:23:31 +03:00
|
|
|
class MaterializedModel1(Model):
|
|
|
|
date_time = DateTimeField()
|
|
|
|
date = DateField(materialized='toDate(date_time)')
|
|
|
|
int_field = Int8Field()
|
2019-01-28 11:08:07 +03:00
|
|
|
int_field_plus_one = Int8Field(materialized='int_field + 1')
|
2017-10-10 10:23:31 +03:00
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'materalized_date'
|
|
|
|
|
|
|
|
|
2017-01-26 13:42:33 +03:00
|
|
|
class AliasModel(Model):
|
|
|
|
date = DateField()
|
2017-01-27 08:42:37 +03:00
|
|
|
date_alias = DateField(alias='date')
|
2017-01-26 13:42:33 +03:00
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
2017-08-16 23:48:18 +03:00
|
|
|
return 'alias_date'
|
2017-09-10 15:46:55 +03:00
|
|
|
|
|
|
|
|
2017-10-10 10:23:31 +03:00
|
|
|
class AliasModel1(Model):
|
|
|
|
date = DateField()
|
|
|
|
date_alias = DateField(alias='date')
|
|
|
|
int_field = Int8Field()
|
2019-01-28 11:08:07 +03:00
|
|
|
int_field_plus_one = Int8Field(alias='int_field + 1')
|
2017-10-10 10:23:31 +03:00
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'alias_date'
|
|
|
|
|
|
|
|
|
2017-09-10 15:46:55 +03:00
|
|
|
class Model4(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Int32Field()
|
|
|
|
f2 = StringField()
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'model4'
|
|
|
|
|
|
|
|
|
|
|
|
class Model4Buffer(BufferModel, Model4):
|
|
|
|
|
|
|
|
engine = Buffer(Model4)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'model4buffer'
|
|
|
|
|
|
|
|
|
|
|
|
class Model4_changed(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f3 = DateTimeField()
|
|
|
|
f2 = StringField()
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'model4'
|
|
|
|
|
|
|
|
|
|
|
|
class Model4Buffer_changed(BufferModel, Model4_changed):
|
|
|
|
|
|
|
|
engine = Buffer(Model4_changed)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'model4buffer'
|
2019-06-20 11:21:43 +03:00
|
|
|
|
|
|
|
|
|
|
|
class Model4_compressed(Model):
|
|
|
|
|
2019-06-24 12:31:19 +03:00
|
|
|
date = DateField()
|
2019-06-20 11:21:43 +03:00
|
|
|
f3 = DateTimeField(codec='Delta,ZSTD(10)')
|
|
|
|
f2 = StringField(codec='LZ4HC')
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
2019-06-24 14:20:18 +03:00
|
|
|
return 'model4'
|
|
|
|
|
|
|
|
|
|
|
|
class Model2LowCardinality(Model):
|
|
|
|
date = DateField()
|
|
|
|
f1 = LowCardinalityField(Int32Field())
|
|
|
|
f3 = LowCardinalityField(Float32Field())
|
|
|
|
f2 = LowCardinalityField(StringField())
|
|
|
|
f4 = LowCardinalityField(NullableField(StringField()))
|
|
|
|
f5 = ArrayField(LowCardinalityField(UInt64Field()))
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'mig'
|
2020-06-06 11:07:25 +03:00
|
|
|
|
|
|
|
|
|
|
|
class ModelWithConstraints(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Int32Field()
|
|
|
|
f2 = StringField()
|
|
|
|
|
|
|
|
constraint = Constraint(f2.isIn(['a', 'b', 'c'])) # check reserved keyword as constraint name
|
|
|
|
f1_constraint = Constraint(f1 > 100)
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'modelwithconstraints'
|
|
|
|
|
|
|
|
|
|
|
|
class ModelWithConstraints2(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Int32Field()
|
|
|
|
f2 = StringField()
|
|
|
|
|
|
|
|
constraint = Constraint(f2.isIn(['a', 'b', 'c']))
|
|
|
|
f1_constraint_new = Constraint(f1 < 100)
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'modelwithconstraints'
|
|
|
|
|
2020-06-06 20:56:32 +03:00
|
|
|
|
|
|
|
class ModelWithIndex(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Int32Field()
|
|
|
|
f2 = StringField()
|
|
|
|
|
|
|
|
index = Index(f1, type=Index.minmax(), granularity=1)
|
|
|
|
another_index = Index(f2, type=Index.set(0), granularity=1)
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'modelwithindex'
|
|
|
|
|
|
|
|
|
|
|
|
class ModelWithIndex2(Model):
|
|
|
|
|
|
|
|
date = DateField()
|
|
|
|
f1 = Int32Field()
|
|
|
|
f2 = StringField()
|
|
|
|
|
|
|
|
index2 = Index(f1, type=Index.bloom_filter(), granularity=2)
|
|
|
|
another_index = Index(f2, type=Index.set(0), granularity=1)
|
|
|
|
|
|
|
|
engine = MergeTree('date', ('date',))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def table_name(cls):
|
|
|
|
return 'modelwithindex'
|
|
|
|
|