Skip to content

Commit d66e6a4

Browse files
authored
Merge pull request piccolo-orm#70 from piccolo-orm/migrations_change_column_types
Migrations change column types
2 parents 644d427 + 3fce612 commit d66e6a4

File tree

9 files changed

+194
-25
lines changed

9 files changed

+194
-25
lines changed

piccolo/apps/migrations/auto/diffable_table.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ def __sub__(self, value: DiffableTable) -> TableDelta:
110110
column_name=column._meta.name,
111111
params=deserialise_params(delta),
112112
old_params=old_params,
113+
column_class=column.__class__,
114+
old_column_class=existing_column.__class__,
113115
)
114116
)
115117

piccolo/apps/migrations/auto/migration_manager.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ def alter_column(
252252
column_name: str,
253253
params: t.Dict[str, t.Any],
254254
old_params: t.Dict[str, t.Any],
255+
column_class: t.Optional[t.Type[Column]] = None,
256+
old_column_class: t.Optional[t.Type[Column]] = None,
255257
):
256258
"""
257259
All possible alterations aren't currently supported.
@@ -263,6 +265,8 @@ def alter_column(
263265
column_name=column_name,
264266
params=params,
265267
old_params=old_params,
268+
column_class=column_class,
269+
old_column_class=old_column_class,
266270
)
267271
)
268272

@@ -328,9 +332,53 @@ async def _run_alter_columns(self, backwards=False):
328332
_Table: t.Type[Table] = type(table_class_name, (Table,), {})
329333
_Table._meta.tablename = alter_columns[0].tablename
330334

331-
for column in alter_columns:
332-
params = column.old_params if backwards else column.params
333-
column_name = column.column_name
335+
for alter_column in alter_columns:
336+
337+
params = (
338+
alter_column.old_params
339+
if backwards
340+
else alter_column.params
341+
)
342+
343+
old_params = (
344+
alter_column.params
345+
if backwards
346+
else alter_column.old_params
347+
)
348+
349+
###############################################################
350+
351+
# Change the column type if possible
352+
column_class = (
353+
alter_column.old_column_class
354+
if backwards
355+
else alter_column.column_class
356+
)
357+
old_column_class = (
358+
alter_column.column_class
359+
if backwards
360+
else alter_column.old_column_class
361+
)
362+
363+
if (old_column_class is not None) and (
364+
column_class is not None
365+
):
366+
if old_column_class != column_class:
367+
old_column = old_column_class(**old_params)
368+
old_column._meta._table = _Table
369+
old_column._meta._name = alter_column.column_name
370+
371+
new_column = column_class(**params)
372+
new_column._meta._table = _Table
373+
new_column._meta._name = alter_column.column_name
374+
375+
await _Table.alter().set_column_type(
376+
old_column=old_column, new_column=new_column
377+
)
378+
379+
###############################################################
380+
381+
column_name = alter_column.column_name
334382

335383
null = params.get("null")
336384
if null is not None:
@@ -383,7 +431,7 @@ async def _run_alter_columns(self, backwards=False):
383431
digits = params.get("digits", ...)
384432
if digits is not ...:
385433
await _Table.alter().set_digits(
386-
column=column.column_name, digits=digits,
434+
column=alter_column.column_name, digits=digits,
387435
).run()
388436

389437
async def _run_drop_tables(self, backwards=False):

piccolo/apps/migrations/auto/operations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class AlterColumn:
2626
tablename: str
2727
params: t.Dict[str, t.Any]
2828
old_params: t.Dict[str, t.Any]
29+
column_class: t.Optional[t.Type[Column]] = None
30+
old_column_class: t.Optional[t.Type[Column]] = None
2931

3032

3133
@dataclass

piccolo/apps/migrations/auto/schema_differ.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,17 +308,45 @@ def alter_columns(self) -> AlterStatements:
308308
else:
309309
continue
310310

311-
for i in delta.alter_columns:
312-
new_params = serialise_params(i.params)
311+
for alter_column in delta.alter_columns:
312+
new_params = serialise_params(alter_column.params)
313313
extra_imports.extend(new_params.extra_imports)
314314
extra_definitions.extend(new_params.extra_definitions)
315315

316-
old_params = serialise_params(i.old_params)
316+
old_params = serialise_params(alter_column.old_params)
317317
extra_imports.extend(old_params.extra_imports)
318318
extra_definitions.extend(old_params.extra_definitions)
319319

320+
column_class = (
321+
alter_column.column_class.__name__
322+
if alter_column.column_class
323+
else "None"
324+
)
325+
326+
old_column_class = (
327+
alter_column.old_column_class.__name__
328+
if alter_column.old_column_class
329+
else "None"
330+
)
331+
332+
if alter_column.column_class is not None:
333+
extra_imports.append(
334+
Import(
335+
module=alter_column.column_class.__module__,
336+
target=alter_column.column_class.__name__,
337+
)
338+
)
339+
340+
if alter_column.old_column_class is not None:
341+
extra_imports.append(
342+
Import(
343+
module=alter_column.old_column_class.__module__,
344+
target=alter_column.old_column_class.__name__,
345+
)
346+
)
347+
320348
response.append(
321-
f"manager.alter_column(table_class_name='{table.class_name}', tablename='{table.tablename}', column_name='{i.column_name}', params={new_params.params}, old_params={old_params.params})" # noqa: E501
349+
f"manager.alter_column(table_class_name='{table.class_name}', tablename='{table.tablename}', column_name='{alter_column.column_name}', params={new_params.params}, old_params={old_params.params}, column_class={column_class}, old_column_class={old_column_class})" # noqa: E501
322350
)
323351

324352
return AlterStatements(

piccolo/columns/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .column_types import ( # noqa: F401
2+
BigInt,
23
Boolean,
34
Bytea,
45
Date,
@@ -14,6 +15,7 @@
1415
Real,
1516
Secret,
1617
Serial,
18+
SmallInt,
1719
Text,
1820
Timestamp,
1921
Timestamptz,

piccolo/columns/base.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -462,15 +462,16 @@ def get_sql_value(self, value: t.Any) -> t.Any:
462462

463463
return output
464464

465+
@property
466+
def column_type(self):
467+
return self.__class__.__name__.upper()
468+
465469
@property
466470
def querystring(self) -> QueryString:
467471
"""
468472
Used when creating tables.
469473
"""
470-
column_type = getattr(
471-
self, "column_type", self.__class__.__name__.upper()
472-
)
473-
query = f'"{self._meta.name}" {column_type}'
474+
query = f'"{self._meta.name}" {self.column_type}'
474475
if self._meta.primary:
475476
query += " PRIMARY"
476477
if self._meta.key:

piccolo/query/methods/alter.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,24 @@ def querystring(self) -> QueryString:
9191
return QueryString(f"ALTER COLUMN {self.column_name} DROP DEFAULT")
9292

9393

94+
@dataclass
95+
class SetColumnType(AlterStatement):
96+
__slots__ = ("old_column", "new_column")
97+
98+
old_column: Column
99+
new_column: Column
100+
101+
@property
102+
def querystring(self) -> QueryString:
103+
if self.new_column._meta._table is None:
104+
self.new_column._meta._table = self.old_column._meta.table
105+
106+
column_name = self.old_column._meta.name
107+
return QueryString(
108+
f"ALTER COLUMN {column_name} TYPE {self.new_column.column_type}"
109+
)
110+
111+
94112
@dataclass
95113
class SetDefault(AlterColumnStatement):
96114
__slots__ = ("column", "value")
@@ -107,7 +125,7 @@ def querystring(self) -> QueryString:
107125

108126

109127
@dataclass
110-
class Unique(AlterColumnStatement):
128+
class SetUnique(AlterColumnStatement):
111129
__slots__ = ("boolean",)
112130

113131
boolean: bool
@@ -129,7 +147,7 @@ def querystring(self) -> QueryString:
129147

130148

131149
@dataclass
132-
class Null(AlterColumnStatement):
150+
class SetNull(AlterColumnStatement):
133151
__slots__ = ("boolean",)
134152

135153
boolean: bool
@@ -255,13 +273,14 @@ class Alter(Query):
255273
"_drop_default",
256274
"_drop_table",
257275
"_drop",
258-
"_null",
259276
"_rename_columns",
260277
"_rename_table",
278+
"_set_column_type",
261279
"_set_default",
262280
"_set_digits",
263281
"_set_length",
264-
"_unique",
282+
"_set_null",
283+
"_set_unique",
265284
)
266285

267286
def __init__(self, table: t.Type[Table]):
@@ -272,13 +291,14 @@ def __init__(self, table: t.Type[Table]):
272291
self._drop_default: t.List[DropDefault] = []
273292
self._drop_table: t.Optional[DropTable] = None
274293
self._drop: t.List[DropColumn] = []
275-
self._null: t.List[Null] = []
276294
self._rename_columns: t.List[RenameColumn] = []
277295
self._rename_table: t.List[RenameTable] = []
296+
self._set_column_type: t.List[SetColumnType] = []
278297
self._set_default: t.List[SetDefault] = []
279298
self._set_digits: t.List[SetDigits] = []
280299
self._set_length: t.List[SetLength] = []
281-
self._unique: t.List[Unique] = []
300+
self._set_null: t.List[SetNull] = []
301+
self._set_unique: t.List[SetUnique] = []
282302

283303
def add_column(self, name: str, column: Column) -> Alter:
284304
"""
@@ -333,6 +353,15 @@ def rename_column(
333353
self._rename_columns.append(RenameColumn(column, new_name))
334354
return self
335355

356+
def set_column_type(self, old_column: Column, new_column: Column) -> Alter:
357+
"""
358+
Change the type of a column.
359+
"""
360+
self._set_column_type.append(
361+
SetColumnType(old_column=old_column, new_column=new_column)
362+
)
363+
return self
364+
336365
def set_default(self, column: Column, value: t.Any) -> Alter:
337366
"""
338367
Set the default for a column.
@@ -349,7 +378,7 @@ def set_null(
349378
Band.alter().set_null(Band.name, True)
350379
Band.alter().set_null('name', True)
351380
"""
352-
self._null.append(Null(column, boolean))
381+
self._set_null.append(SetNull(column, boolean))
353382
return self
354383

355384
def set_unique(
@@ -359,7 +388,7 @@ def set_unique(
359388
Band.alter().set_unique(Band.name, True)
360389
Band.alter().set_unique('name', True)
361390
"""
362-
self._unique.append(Unique(column, boolean))
391+
self._set_unique.append(SetUnique(column, boolean))
363392
return self
364393

365394
def set_length(self, column: t.Union[str, Varchar], length: int) -> Alter:
@@ -472,8 +501,9 @@ def querystrings(self) -> t.Sequence[QueryString]:
472501
self._rename_table,
473502
self._drop,
474503
self._drop_default,
475-
self._unique,
476-
self._null,
504+
self._set_column_type,
505+
self._set_unique,
506+
self._set_null,
477507
self._set_length,
478508
self._set_default,
479509
self._set_digits,

tests/apps/migrations/auto/test_schema_differ.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def test_alter_column_precision(self):
187187
self.assertTrue(len(schema_differ.alter_columns.statements) == 1)
188188
self.assertEqual(
189189
schema_differ.alter_columns.statements[0],
190-
"manager.alter_column(table_class_name='Ticket', tablename='ticket', column_name='price', params={'digits': (4, 2)}, old_params={'digits': (5, 2)})", # noqa
190+
"manager.alter_column(table_class_name='Ticket', tablename='ticket', column_name='price', params={'digits': (4, 2)}, old_params={'digits': (5, 2)}, column_class=Numeric, old_column_class=Numeric)", # noqa
191191
)
192192

193193
def test_alter_default(self):

tests/table/test_alter.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
from piccolo.columns.column_types import Varchar
12
from unittest import TestCase
23

3-
from piccolo.columns import Integer, Numeric
4+
from piccolo.columns import BigInt, Integer, Numeric
45
from piccolo.table import Table
56

67
from ..base import DBTestCase, postgres_only
@@ -113,7 +114,6 @@ def test_unique(self):
113114
self.assertTrue(len(response), 2)
114115

115116

116-
# TODO - make it work for SQLite. Should work.
117117
@postgres_only
118118
class TestMultiple(DBTestCase):
119119
"""
@@ -137,6 +137,62 @@ def test_multiple(self):
137137
self.assertTrue("column_b" in column_names)
138138

139139

140+
# TODO - test more conversions.
141+
@postgres_only
142+
class TestSetColumnType(DBTestCase):
143+
def test_integer_to_bigint(self):
144+
"""
145+
Test converting an Integer column to BigInt.
146+
"""
147+
self.insert_row()
148+
149+
alter_query = Band.alter().set_column_type(
150+
old_column=Band.popularity, new_column=BigInt()
151+
)
152+
alter_query.run_sync()
153+
154+
query = """
155+
SELECT data_type FROM information_schema.columns
156+
WHERE table_name = 'band'
157+
AND table_catalog = 'piccolo'
158+
AND column_name = 'popularity'
159+
"""
160+
161+
response = Band.raw(query).run_sync()
162+
self.assertEqual(response[0]["data_type"].upper(), "BIGINT")
163+
164+
popularity = (
165+
Band.select(Band.popularity).first().run_sync()["popularity"]
166+
)
167+
self.assertEqual(popularity, 1000)
168+
169+
def test_integer_to_varchar(self):
170+
"""
171+
Test converting an Integer column to Varchar.
172+
"""
173+
self.insert_row()
174+
175+
alter_query = Band.alter().set_column_type(
176+
old_column=Band.popularity, new_column=Varchar()
177+
)
178+
alter_query.run_sync()
179+
180+
query = """
181+
SELECT data_type FROM information_schema.columns
182+
WHERE table_name = 'band'
183+
AND table_catalog = 'piccolo'
184+
AND column_name = 'popularity'
185+
"""
186+
187+
response = Band.raw(query).run_sync()
188+
self.assertEqual(response[0]["data_type"].upper(), "CHARACTER VARYING")
189+
190+
popularity = (
191+
Band.select(Band.popularity).first().run_sync()["popularity"]
192+
)
193+
self.assertEqual(popularity, "1000")
194+
195+
140196
@postgres_only
141197
class TestSetNull(DBTestCase):
142198
def test_set_null(self):

0 commit comments

Comments
 (0)