From 2def3e958a62d251f1853a33ec4887d02cef6166 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Mon, 15 Jan 2018 14:31:00 +0000 Subject: [PATCH 01/12] Add an updated field to labels --- routemaster/db/model.py | 1 + ...e7d5ad06c0d1_add_updated_field_to_label.py | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 routemaster/migrations/versions/e7d5ad06c0d1_add_updated_field_to_label.py diff --git a/routemaster/db/model.py b/routemaster/db/model.py index 2cee4070..d6b28672 100644 --- a/routemaster/db/model.py +++ b/routemaster/db/model.py @@ -26,6 +26,7 @@ Column('metadata', JSONB), Column('metadata_triggers_processed', Boolean, default=True), Column('deleted', Boolean, default=False), + Column('updated', DateTime, nullable=False), ) diff --git a/routemaster/migrations/versions/e7d5ad06c0d1_add_updated_field_to_label.py b/routemaster/migrations/versions/e7d5ad06c0d1_add_updated_field_to_label.py new file mode 100644 index 00000000..07162f1d --- /dev/null +++ b/routemaster/migrations/versions/e7d5ad06c0d1_add_updated_field_to_label.py @@ -0,0 +1,31 @@ +""" +add updated field to label + +Revision ID: e7d5ad06c0d1 +Revises: e1fec9622785 +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'e7d5ad06c0d1' +down_revision = 'e1fec9622785' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + 'labels', + sa.Column( + 'updated', + sa.DateTime(), + nullable=False, + server_default=sa.func.now(), + ), + ) + + +def downgrade(): + op.drop_column('labels', 'updated') From 84a974968935e650e7bd2c9ec4974c286ba4fe25 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Mon, 15 Jan 2018 14:33:59 +0000 Subject: [PATCH 02/12] Record label updated times --- routemaster/state_machine/api.py | 5 ++++- routemaster/state_machine/gates.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/routemaster/state_machine/api.py b/routemaster/state_machine/api.py index 2b3f4d15..fd562554 100644 --- a/routemaster/state_machine/api.py +++ b/routemaster/state_machine/api.py @@ -3,7 +3,7 @@ import logging from typing import Any, Callable, Iterable -from sqlalchemy import and_, not_ +from sqlalchemy import and_, func, not_ from sqlalchemy.exc import IntegrityError from sqlalchemy.sql import select @@ -88,6 +88,7 @@ def create_label(app: App, label: LabelRef, metadata: Metadata) -> Metadata: name=label.name, state_machine=state_machine.name, metadata=metadata, + updated=func.now(), )) except IntegrityError: raise LabelAlreadyExists(label) @@ -135,6 +136,7 @@ def update_metadata_for_label( )).values( metadata=new_metadata, metadata_triggers_processed=not needs_gate_evaluation, + updated=func.now(), )) # Outside transaction @@ -218,6 +220,7 @@ def delete_label(app: App, label: LabelRef) -> None: )).values( metadata={}, deleted=True, + updated=func.now(), )) # Add a history entry for the deletion diff --git a/routemaster/state_machine/gates.py b/routemaster/state_machine/gates.py index 416c0f5e..40e1c5ee 100644 --- a/routemaster/state_machine/gates.py +++ b/routemaster/state_machine/gates.py @@ -1,5 +1,5 @@ """Processing for gate states.""" -from sqlalchemy import and_ +from sqlalchemy import and_, func from routemaster.db import labels, history from routemaster.app import App @@ -63,6 +63,7 @@ def process_gate( labels.c.state_machine == label.state_machine, )).values( metadata_triggers_processed=True, + updated=func.now(), )) return True From c84dcf3c6513a9631e32791ee74c4cc8ce376d53 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Mon, 15 Jan 2018 15:31:02 +0000 Subject: [PATCH 03/12] Make a lot of stuff not-nullable --- routemaster/db/model.py | 21 +++-- ...eb9_make_fields_non_nullable_by_default.py | 90 +++++++++++++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 routemaster/migrations/versions/814a6b555eb9_make_fields_non_nullable_by_default.py diff --git a/routemaster/db/model.py b/routemaster/db/model.py index d6b28672..099b56ef 100644 --- a/routemaster/db/model.py +++ b/routemaster/db/model.py @@ -1,9 +1,10 @@ """Database model definition.""" import datetime +import functools +from sqlalchemy import Column as NullableColumn from sqlalchemy import ( Table, - Column, String, Boolean, Integer, @@ -16,6 +17,8 @@ metadata = MetaData() +Column = functools.partial(NullableColumn, nullable=False) + """The representation of the state of a label.""" labels = Table( @@ -26,7 +29,7 @@ Column('metadata', JSONB), Column('metadata_triggers_processed', Boolean, default=True), Column('deleted', Boolean, default=False), - Column('updated', DateTime, nullable=False), + Column('updated', DateTime), ) @@ -50,8 +53,8 @@ Column('forced', Boolean, default=False), # Null indicates starting a state machine - Column('old_state', String, nullable=True), - Column('new_state', String), + NullableColumn('old_state', String), + NullableColumn('new_state', String), # Can we get foreign key constraints on these as well? # Currently: no, because those columns are not unique themselves, however @@ -108,11 +111,11 @@ edges = Table( 'edges', metadata, - Column('state_machine', String, primary_key=True, nullable=False), - Column('from_state', String, primary_key=True, nullable=False), - Column('to_state', String, primary_key=True, nullable=False), - Column('deprecated', Boolean, default=False, nullable=False), - Column('updated', DateTime, nullable=False), + Column('state_machine', String, primary_key=True), + Column('from_state', String, primary_key=True), + Column('to_state', String, primary_key=True), + Column('deprecated', Boolean, default=False), + Column('updated', DateTime), ForeignKeyConstraint( columns=('state_machine', 'from_state'), refcolumns=(states.c.state_machine, states.c.name), diff --git a/routemaster/migrations/versions/814a6b555eb9_make_fields_non_nullable_by_default.py b/routemaster/migrations/versions/814a6b555eb9_make_fields_non_nullable_by_default.py new file mode 100644 index 00000000..70eac017 --- /dev/null +++ b/routemaster/migrations/versions/814a6b555eb9_make_fields_non_nullable_by_default.py @@ -0,0 +1,90 @@ +""" +make fields non-nullable by default + +Revision ID: 814a6b555eb9 +Revises: e7d5ad06c0d1 +""" +import sqlalchemy as sa + +from alembic import op + +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '814a6b555eb9' +down_revision = 'e7d5ad06c0d1' +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column('history', 'label_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('history', 'label_state_machine', + existing_type=sa.VARCHAR(), + nullable=False) + + def set_not_null(table, column, existing_type, server_default): + op.alter_column(table, column, + existing_type=existing_type, + server_default=server_default) + op.execute( + f"UPDATE {table} SET {column} = {server_default} " + f"WHERE {column} IS NULL", + ) + op.alter_column(table, column, + existing_type=existing_type, + nullable=False) + + set_not_null('history', 'created', + existing_type=postgresql.TIMESTAMP(), + server_default=sa.func.now()) + set_not_null('history', 'forced', + existing_type=sa.BOOLEAN(), + server_default=sa.false()) + set_not_null('labels', 'metadata', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + server_default=sa.text("'{}'::json")) + set_not_null('labels', 'metadata_triggers_processed', + existing_type=sa.BOOLEAN(), + server_default=sa.true()) + set_not_null('state_machines', 'updated', + existing_type=postgresql.TIMESTAMP(), + server_default=sa.func.now()) + set_not_null('states', 'deprecated', + existing_type=sa.BOOLEAN(), + server_default=sa.false()) + set_not_null('states', 'updated', + existing_type=postgresql.TIMESTAMP(), + server_default=sa.func.now()) + + +def downgrade(): + op.alter_column('states', 'updated', + existing_type=postgresql.TIMESTAMP(), + nullable=True) + op.alter_column('states', 'deprecated', + existing_type=sa.BOOLEAN(), + nullable=True) + op.alter_column('state_machines', 'updated', + existing_type=postgresql.TIMESTAMP(), + nullable=True) + op.alter_column('labels', 'metadata_triggers_processed', + existing_type=sa.BOOLEAN(), + nullable=True) + op.alter_column('labels', 'metadata', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + nullable=True) + op.alter_column('history', 'label_state_machine', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('history', 'label_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('history', 'forced', + existing_type=sa.BOOLEAN(), + nullable=True) + op.alter_column('history', 'created', + existing_type=postgresql.TIMESTAMP(), + nullable=True) From 4c086816e79331a85d6ab21233b809012606e8cc Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Tue, 16 Jan 2018 12:31:39 +0000 Subject: [PATCH 04/12] Maintain the updated field with a trigger --- routemaster/db/model.py | 33 ++++++++++++++++- routemaster/state_machine/api.py | 3 -- routemaster/state_machine/gates.py | 1 - .../state_machine/tests/test_state_machine.py | 36 +++++++++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/routemaster/db/model.py b/routemaster/db/model.py index d6b28672..dced3441 100644 --- a/routemaster/db/model.py +++ b/routemaster/db/model.py @@ -2,6 +2,9 @@ import datetime from sqlalchemy import ( + func, + event, + DDL, Table, Column, String, @@ -10,12 +13,32 @@ DateTime, MetaData, ForeignKey, + FetchedValue, ForeignKeyConstraint, ) from sqlalchemy.dialects.postgresql import JSONB metadata = MetaData() +sync_label_updated_column = DDL( + ''' + CREATE OR REPLACE FUNCTION sync_label_updated_column_fn() + RETURNS TRIGGER AS + $$ + BEGIN + NEW.updated = now(); + RETURN NEW; + END; + $$ + LANGUAGE PLPGSQL; + + CREATE TRIGGER sync_label_updated_column + BEFORE UPDATE ON labels + FOR EACH ROW + EXECUTE PROCEDURE sync_label_updated_column_fn(); + ''', +) + """The representation of the state of a label.""" labels = Table( @@ -26,7 +49,15 @@ Column('metadata', JSONB), Column('metadata_triggers_processed', Boolean, default=True), Column('deleted', Boolean, default=False), - Column('updated', DateTime, nullable=False), + Column( + 'updated', + DateTime, + server_default=func.now(), + server_onupdate=FetchedValue(), + ), + listeners=[ + ('after_create', sync_label_updated_column) + ] ) diff --git a/routemaster/state_machine/api.py b/routemaster/state_machine/api.py index fd562554..bd7033d7 100644 --- a/routemaster/state_machine/api.py +++ b/routemaster/state_machine/api.py @@ -88,7 +88,6 @@ def create_label(app: App, label: LabelRef, metadata: Metadata) -> Metadata: name=label.name, state_machine=state_machine.name, metadata=metadata, - updated=func.now(), )) except IntegrityError: raise LabelAlreadyExists(label) @@ -136,7 +135,6 @@ def update_metadata_for_label( )).values( metadata=new_metadata, metadata_triggers_processed=not needs_gate_evaluation, - updated=func.now(), )) # Outside transaction @@ -220,7 +218,6 @@ def delete_label(app: App, label: LabelRef) -> None: )).values( metadata={}, deleted=True, - updated=func.now(), )) # Add a history entry for the deletion diff --git a/routemaster/state_machine/gates.py b/routemaster/state_machine/gates.py index 40e1c5ee..875b6e70 100644 --- a/routemaster/state_machine/gates.py +++ b/routemaster/state_machine/gates.py @@ -63,7 +63,6 @@ def process_gate( labels.c.state_machine == label.state_machine, )).values( metadata_triggers_processed=True, - updated=func.now(), )) return True diff --git a/routemaster/state_machine/tests/test_state_machine.py b/routemaster/state_machine/tests/test_state_machine.py index a2671ca1..bbe34915 100644 --- a/routemaster/state_machine/tests/test_state_machine.py +++ b/routemaster/state_machine/tests/test_state_machine.py @@ -236,3 +236,39 @@ def test_metadata_update_gate_evaluations_dont_race_processing_subsequent_metada assert_history([ (None, 'gate_1'), ]) + + +def test_maintains_updated_field_on_label(app_config, mock_test_feed): + label = LabelRef('foo', 'test_machine') + + with mock_test_feed(): + state_machine.create_label( + app_config, + label, + {}, + ) + + with app_config.db.begin() as conn: + first_updated = conn.scalar( + select([labels.c.updated]).where(and_( + labels.c.name == label.name, + labels.c.state_machine == label.state_machine, + )), + ) + + with mock_test_feed(): + state_machine.update_metadata_for_label( + app_config, + label, + {'foo': 'bar'}, + ) + + with app_config.db.begin() as conn: + second_updated = conn.scalar( + select([labels.c.updated]).where(and_( + labels.c.name == label.name, + labels.c.state_machine == label.state_machine, + )), + ) + + assert second_updated > first_updated From 9def8b7572303ecc8b35c992726cb6941b7b6b07 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Tue, 16 Jan 2018 12:34:06 +0000 Subject: [PATCH 05/12] Linters --- routemaster/db/model.py | 5 ++--- routemaster/state_machine/api.py | 2 +- routemaster/state_machine/gates.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/routemaster/db/model.py b/routemaster/db/model.py index eace3faf..3bae43e9 100644 --- a/routemaster/db/model.py +++ b/routemaster/db/model.py @@ -4,8 +4,6 @@ from sqlalchemy import Column as NullableColumn from sqlalchemy import ( - func, - event, DDL, Table, String, @@ -16,6 +14,7 @@ ForeignKey, FetchedValue, ForeignKeyConstraint, + func, ) from sqlalchemy.dialects.postgresql import JSONB @@ -59,7 +58,7 @@ server_onupdate=FetchedValue(), ), listeners=[ - ('after_create', sync_label_updated_column) + ('after_create', sync_label_updated_column), ], ) diff --git a/routemaster/state_machine/api.py b/routemaster/state_machine/api.py index bd7033d7..2b3f4d15 100644 --- a/routemaster/state_machine/api.py +++ b/routemaster/state_machine/api.py @@ -3,7 +3,7 @@ import logging from typing import Any, Callable, Iterable -from sqlalchemy import and_, func, not_ +from sqlalchemy import and_, not_ from sqlalchemy.exc import IntegrityError from sqlalchemy.sql import select diff --git a/routemaster/state_machine/gates.py b/routemaster/state_machine/gates.py index 875b6e70..416c0f5e 100644 --- a/routemaster/state_machine/gates.py +++ b/routemaster/state_machine/gates.py @@ -1,5 +1,5 @@ """Processing for gate states.""" -from sqlalchemy import and_, func +from sqlalchemy import and_ from routemaster.db import labels, history from routemaster.app import App From 2bb9cb92f2108c00c1d5dba0514ba0fe4acb9c87 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Tue, 16 Jan 2018 12:38:59 +0000 Subject: [PATCH 06/12] Document what NULL is here --- routemaster/db/model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routemaster/db/model.py b/routemaster/db/model.py index 23b87d92..c934e574 100644 --- a/routemaster/db/model.py +++ b/routemaster/db/model.py @@ -84,6 +84,8 @@ # Null indicates starting a state machine NullableColumn('old_state', String), + + # Null indicates being deleted from a state machine NullableColumn('new_state', String), ) From 13c4c342b322244a546174959a14c65a2301c573 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Tue, 16 Jan 2018 12:43:30 +0000 Subject: [PATCH 07/12] Migration to create/drop trigger --- ...c6_create_trigger_to_sync_updated_field.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 routemaster/migrations/versions/3eb4f3b419c6_create_trigger_to_sync_updated_field.py diff --git a/routemaster/migrations/versions/3eb4f3b419c6_create_trigger_to_sync_updated_field.py b/routemaster/migrations/versions/3eb4f3b419c6_create_trigger_to_sync_updated_field.py new file mode 100644 index 00000000..97b0155f --- /dev/null +++ b/routemaster/migrations/versions/3eb4f3b419c6_create_trigger_to_sync_updated_field.py @@ -0,0 +1,41 @@ +""" +create trigger to sync updated field + +Revision ID: 3eb4f3b419c6 +Revises: 814a6b555eb9 +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = '3eb4f3b419c6' +down_revision = '814a6b555eb9' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + ''' + CREATE OR REPLACE FUNCTION sync_label_updated_column_fn() + RETURNS TRIGGER AS + $$ + BEGIN + NEW.updated = now(); + RETURN NEW; + END; + $$ + LANGUAGE PLPGSQL; + + CREATE TRIGGER sync_label_updated_column + BEFORE UPDATE ON labels + FOR EACH ROW + EXECUTE PROCEDURE sync_label_updated_column_fn(); + ''', + ) + + +def downgrade(): + op.execute('DROP TRIGGER sync_label_updated_column') + op.execute('DROP FUNCTION sync_label_updated_column_fn') From 6e9908890ad0f0475b2c36c34449ede9917f48fc Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Tue, 16 Jan 2018 13:04:30 +0000 Subject: [PATCH 08/12] Make these UTC --- routemaster/db/model.py | 2 +- .../3eb4f3b419c6_create_trigger_to_sync_updated_field.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routemaster/db/model.py b/routemaster/db/model.py index c934e574..a39b604a 100644 --- a/routemaster/db/model.py +++ b/routemaster/db/model.py @@ -28,7 +28,7 @@ RETURNS TRIGGER AS $$ BEGIN - NEW.updated = now(); + NEW.updated = now() AT TIME ZONE 'UTC'; RETURN NEW; END; $$ diff --git a/routemaster/migrations/versions/3eb4f3b419c6_create_trigger_to_sync_updated_field.py b/routemaster/migrations/versions/3eb4f3b419c6_create_trigger_to_sync_updated_field.py index 97b0155f..c2c9b2d1 100644 --- a/routemaster/migrations/versions/3eb4f3b419c6_create_trigger_to_sync_updated_field.py +++ b/routemaster/migrations/versions/3eb4f3b419c6_create_trigger_to_sync_updated_field.py @@ -22,7 +22,7 @@ def upgrade(): RETURNS TRIGGER AS $$ BEGIN - NEW.updated = now(); + NEW.updated = now() AT TIME ZONE 'UTC'; RETURN NEW; END; $$ From f3a8f350f517b0fa6e825999e5b754711d095060 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Tue, 16 Jan 2018 13:05:29 +0000 Subject: [PATCH 09/12] Make these timezone aware --- routemaster/db/model.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/routemaster/db/model.py b/routemaster/db/model.py index a39b604a..8ae895ee 100644 --- a/routemaster/db/model.py +++ b/routemaster/db/model.py @@ -2,6 +2,8 @@ import datetime import functools +import dateutil.tz + from sqlalchemy import Column as NullableColumn from sqlalchemy import ( DDL, @@ -53,7 +55,7 @@ Column('deleted', Boolean, default=False), Column( 'updated', - DateTime, + DateTime(timezone=True), server_default=func.now(), server_onupdate=FetchedValue(), ), @@ -76,7 +78,11 @@ ['labels.name', 'labels.state_machine'], ), - Column('created', DateTime, default=datetime.datetime.utcnow), + Column( + 'created', + DateTime(timezone=True), + default=lambda: datetime.datetime.now(dateutil.tz.tzutc()), + ), # `forced = True` represents a manual transition that may not be in # accordance with the state machine logic. From b8f50cf67923c474b348da351bf2cdfba2d32f11 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Tue, 16 Jan 2018 13:05:29 +0000 Subject: [PATCH 10/12] Linting --- routemaster/db/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/routemaster/db/model.py b/routemaster/db/model.py index 8ae895ee..8ae80cc7 100644 --- a/routemaster/db/model.py +++ b/routemaster/db/model.py @@ -3,7 +3,6 @@ import functools import dateutil.tz - from sqlalchemy import Column as NullableColumn from sqlalchemy import ( DDL, From 3118376a03c7cea46150e33fc828afd3a80c1c71 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Tue, 16 Jan 2018 13:57:42 +0000 Subject: [PATCH 11/12] Mention that we require Postgres --- README.md | 2 +- docs/getting_started.md | 3 ++- docs/migrations.md | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f8245259..996e2e8b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ State machines as a service. (The _master_ of _routes_ through a state machine.) -Routemaster targets Python 3.6 and above. +Routemaster targets Python 3.6 and above, and requires Postgres. ##### Useful Links diff --git a/docs/getting_started.md b/docs/getting_started.md index 29172f50..7e56a9b4 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -5,7 +5,8 @@ You'll need to create a database for developing against and for running tests against. This can be done by running the `scripts/database/create_databases.sh` script. Full details of how the database, models & migrations are handled can be -found in the [migrations docs](docs/migrations.md). +found in the [migrations docs](docs/migrations.md). Routemaster requires +Postgres. #### Tox diff --git a/docs/migrations.md b/docs/migrations.md index 28f617e7..06402c5d 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -1,6 +1,7 @@ # Migrations setup -Routemaster uses [`alembic`][alembic] for its migrations. +Routemaster uses [`alembic`][alembic] for its migrations, and supports +Postgres for its data storage. ## I need to set up my database up for the first time From e9f015a8794b86d01710f2c7d0e0b489fe3b7b57 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Tue, 16 Jan 2018 14:00:48 +0000 Subject: [PATCH 12/12] Reflow --- ...eb9_make_fields_non_nullable_by_default.py | 180 ++++++++++++------ 1 file changed, 120 insertions(+), 60 deletions(-) diff --git a/routemaster/migrations/versions/814a6b555eb9_make_fields_non_nullable_by_default.py b/routemaster/migrations/versions/814a6b555eb9_make_fields_non_nullable_by_default.py index 70eac017..84ca63c2 100644 --- a/routemaster/migrations/versions/814a6b555eb9_make_fields_non_nullable_by_default.py +++ b/routemaster/migrations/versions/814a6b555eb9_make_fields_non_nullable_by_default.py @@ -18,73 +18,133 @@ def upgrade(): - op.alter_column('history', 'label_name', - existing_type=sa.VARCHAR(), - nullable=False) - op.alter_column('history', 'label_state_machine', - existing_type=sa.VARCHAR(), - nullable=False) + op.alter_column( + 'history', + 'label_name', + existing_type=sa.VARCHAR(), + nullable=False, + ) + op.alter_column( + 'history', + 'label_state_machine', + existing_type=sa.VARCHAR(), + nullable=False, + ) def set_not_null(table, column, existing_type, server_default): - op.alter_column(table, column, - existing_type=existing_type, - server_default=server_default) + op.alter_column( + table, + column, + existing_type=existing_type, + server_default=server_default, + ) op.execute( f"UPDATE {table} SET {column} = {server_default} " f"WHERE {column} IS NULL", ) - op.alter_column(table, column, - existing_type=existing_type, - nullable=False) + op.alter_column( + table, + column, + existing_type=existing_type, + nullable=False, + ) - set_not_null('history', 'created', - existing_type=postgresql.TIMESTAMP(), - server_default=sa.func.now()) - set_not_null('history', 'forced', - existing_type=sa.BOOLEAN(), - server_default=sa.false()) - set_not_null('labels', 'metadata', - existing_type=postgresql.JSONB(astext_type=sa.Text()), - server_default=sa.text("'{}'::json")) - set_not_null('labels', 'metadata_triggers_processed', - existing_type=sa.BOOLEAN(), - server_default=sa.true()) - set_not_null('state_machines', 'updated', - existing_type=postgresql.TIMESTAMP(), - server_default=sa.func.now()) - set_not_null('states', 'deprecated', - existing_type=sa.BOOLEAN(), - server_default=sa.false()) - set_not_null('states', 'updated', - existing_type=postgresql.TIMESTAMP(), - server_default=sa.func.now()) + set_not_null( + 'history', + 'created', + existing_type=postgresql.TIMESTAMP(), + server_default=sa.func.now(), + ) + set_not_null( + 'history', + 'forced', + existing_type=sa.BOOLEAN(), + server_default=sa.false(), + ) + set_not_null( + 'labels', + 'metadata', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + server_default=sa.text("'{}'::json"), # noqa + ) + set_not_null( + 'labels', + 'metadata_triggers_processed', + existing_type=sa.BOOLEAN(), + server_default=sa.true(), + ) + set_not_null( + 'state_machines', + 'updated', + existing_type=postgresql.TIMESTAMP(), + server_default=sa.func.now(), + ) + set_not_null( + 'states', + 'deprecated', + existing_type=sa.BOOLEAN(), + server_default=sa.false(), + ) + set_not_null( + 'states', + 'updated', + existing_type=postgresql.TIMESTAMP(), + server_default=sa.func.now(), + ) def downgrade(): - op.alter_column('states', 'updated', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - op.alter_column('states', 'deprecated', - existing_type=sa.BOOLEAN(), - nullable=True) - op.alter_column('state_machines', 'updated', - existing_type=postgresql.TIMESTAMP(), - nullable=True) - op.alter_column('labels', 'metadata_triggers_processed', - existing_type=sa.BOOLEAN(), - nullable=True) - op.alter_column('labels', 'metadata', - existing_type=postgresql.JSONB(astext_type=sa.Text()), - nullable=True) - op.alter_column('history', 'label_state_machine', - existing_type=sa.VARCHAR(), - nullable=True) - op.alter_column('history', 'label_name', - existing_type=sa.VARCHAR(), - nullable=True) - op.alter_column('history', 'forced', - existing_type=sa.BOOLEAN(), - nullable=True) - op.alter_column('history', 'created', - existing_type=postgresql.TIMESTAMP(), - nullable=True) + op.alter_column( + 'states', + 'updated', + existing_type=postgresql.TIMESTAMP(), + nullable=True, + ) + op.alter_column( + 'states', + 'deprecated', + existing_type=sa.BOOLEAN(), + nullable=True, + ) + op.alter_column( + 'state_machines', + 'updated', + existing_type=postgresql.TIMESTAMP(), + nullable=True, + ) + op.alter_column( + 'labels', + 'metadata_triggers_processed', + existing_type=sa.BOOLEAN(), + nullable=True, + ) + op.alter_column( + 'labels', + 'metadata', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + ) + op.alter_column( + 'history', + 'label_state_machine', + existing_type=sa.VARCHAR(), + nullable=True, + ) + op.alter_column( + 'history', + 'label_name', + existing_type=sa.VARCHAR(), + nullable=True, + ) + op.alter_column( + 'history', + 'forced', + existing_type=sa.BOOLEAN(), + nullable=True, + ) + op.alter_column( + 'history', + 'created', + existing_type=postgresql.TIMESTAMP(), + nullable=True, + )