Alexander Hess
e4e543bd40
- add new ORM models `ReplaySimulation` and `ReplayedOrder` to store the data generated by the routing simulations - add migrations script to create the corresponding database tables + create "replay_simulations" and "replayed_orders" tables + add missing check constraints to "orders" table + add unique constraint to "orders" table to enable compound key + drop unnecessary check constraints from the "orders" table - add tests for the new ORM models + add `simulation`, `replayed_order`, `make_replay_order()`, and `pre_order` fixtures + add `ReplayedOrderFactor` faker class - fix some typos
413 lines
14 KiB
Python
413 lines
14 KiB
Python
"""Add simulation data.
|
|
|
|
Revision: # 81aa304d7a6e at 2021-09-14 13:47:03
|
|
Revises: # b4dd0b8903a5
|
|
"""
|
|
|
|
import os
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
from urban_meal_delivery import configuration
|
|
|
|
|
|
revision = '81aa304d7a6e'
|
|
down_revision = 'b4dd0b8903a5'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
config = configuration.make_config('testing' if os.getenv('TESTING') else 'production')
|
|
|
|
|
|
def upgrade():
|
|
"""Upgrade to revision 81aa304d7a6e."""
|
|
# Drop unnecessary check constraint.
|
|
op.execute( # `.delivery_at` is not set for `.cancelled` orders anyways.
|
|
f"""
|
|
ALTER TABLE {config.CLEAN_SCHEMA}.orders
|
|
DROP CONSTRAINT ck_orders_on_ordered_timestamps_21;
|
|
""",
|
|
) # noqa:WPS355
|
|
|
|
# Add forgotten check constraints to the `orders` table.
|
|
op.execute(
|
|
f"""
|
|
ALTER TABLE {config.CLEAN_SCHEMA}.orders
|
|
ADD CONSTRAINT check_orders_on_ordered_timestamps_placed_at_before_pickup_at
|
|
CHECK (placed_at < pickup_at);
|
|
""",
|
|
) # noqa:WPS355
|
|
op.execute(
|
|
f"""
|
|
ALTER TABLE {config.CLEAN_SCHEMA}.orders
|
|
ADD CONSTRAINT check_orders_on_scheduled_orders_must_be_at_quarters_of_an_hour
|
|
CHECK (
|
|
(
|
|
EXTRACT(MINUTES FROM scheduled_delivery_at)::INTEGER
|
|
% 15 = 0
|
|
)
|
|
AND
|
|
(
|
|
EXTRACT(SECONDS FROM scheduled_delivery_at)::INTEGER
|
|
= 0
|
|
)
|
|
);
|
|
""",
|
|
) # noqa:WPS355
|
|
|
|
# This `UniqueConstraint` is needed by the `replayed_orders` table below.
|
|
op.create_unique_constraint(
|
|
'uq_orders_on_id_ad_hoc',
|
|
'orders',
|
|
['id', 'ad_hoc'],
|
|
schema=config.CLEAN_SCHEMA,
|
|
)
|
|
|
|
op.create_table(
|
|
'replay_simulations',
|
|
sa.Column('id', sa.Integer, autoincrement=True),
|
|
sa.Column('city_id', sa.SmallInteger, nullable=False),
|
|
sa.Column('strategy', sa.Unicode(length=100), nullable=False),
|
|
sa.Column('run', sa.SmallInteger, nullable=False),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_replay_simulations')),
|
|
sa.ForeignKeyConstraint(
|
|
['city_id'],
|
|
[f'{config.CLEAN_SCHEMA}.cities.id'],
|
|
name=op.f('fk_replay_simulations_to_cities_via_city_id'),
|
|
onupdate='RESTRICT',
|
|
ondelete='RESTRICT',
|
|
),
|
|
sa.UniqueConstraint(
|
|
'city_id',
|
|
'strategy',
|
|
'run',
|
|
name=op.f('uq_replay_simulations_on_city_id_strategy_run'),
|
|
),
|
|
sa.CheckConstraint('run >= 0', name=op.f('run_is_a_count')),
|
|
schema=config.CLEAN_SCHEMA,
|
|
)
|
|
|
|
op.create_table(
|
|
'replayed_orders',
|
|
sa.Column('simulation_id', sa.Integer, primary_key=True),
|
|
sa.Column('actual_order_id', sa.Integer, primary_key=True),
|
|
sa.Column('ad_hoc', sa.Boolean, nullable=False),
|
|
sa.Column('placed_at', sa.DateTime, nullable=False),
|
|
sa.Column('scheduled_delivery_at', sa.DateTime),
|
|
sa.Column('cancelled_at', sa.DateTime),
|
|
sa.Column('estimated_prep_duration', sa.SmallInteger),
|
|
sa.Column('restaurant_notified_at', sa.DateTime),
|
|
sa.Column('restaurant_confirmed_at', sa.DateTime),
|
|
sa.Column('restaurant_ready_at', sa.DateTime),
|
|
sa.Column('dispatch_at', sa.DateTime),
|
|
sa.Column('first_estimated_delivery_at', sa.DateTime),
|
|
sa.Column('courier_id', sa.Integer),
|
|
sa.Column('courier_notified_at', sa.DateTime),
|
|
sa.Column('courier_accepted_at', sa.DateTime),
|
|
sa.Column('utilization', sa.SmallInteger),
|
|
sa.Column('reached_pickup_at', sa.DateTime),
|
|
sa.Column('pickup_at', sa.DateTime),
|
|
sa.Column('left_pickup_at', sa.DateTime),
|
|
sa.Column('reached_delivery_at', sa.DateTime),
|
|
sa.Column('delivery_at', sa.DateTime),
|
|
sa.PrimaryKeyConstraint(
|
|
'simulation_id', 'actual_order_id', name=op.f('pk_replayed_orders'),
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
['simulation_id'],
|
|
[f'{config.CLEAN_SCHEMA}.replay_simulations.id'],
|
|
name=op.f('fk_replayed_orders_to_replay_simulations_via_simulation_id'),
|
|
onupdate='RESTRICT',
|
|
ondelete='RESTRICT',
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
# Needs the `UniqueConstraint` from above.
|
|
['actual_order_id', 'ad_hoc'],
|
|
[
|
|
f'{config.CLEAN_SCHEMA}.orders.id',
|
|
f'{config.CLEAN_SCHEMA}.orders.ad_hoc',
|
|
],
|
|
name=op.f('fk_replayed_orders_to_orders_via_actual_order_id_ad_hoc'),
|
|
onupdate='RESTRICT',
|
|
ondelete='RESTRICT',
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
['courier_id'],
|
|
[f'{config.CLEAN_SCHEMA}.couriers.id'],
|
|
name=op.f('fk_replayed_orders_to_couriers_via_courier_id'),
|
|
onupdate='RESTRICT',
|
|
ondelete='RESTRICT',
|
|
),
|
|
sa.CheckConstraint(
|
|
"""
|
|
(
|
|
ad_hoc IS TRUE
|
|
AND
|
|
scheduled_delivery_at IS NULL
|
|
)
|
|
OR
|
|
(
|
|
ad_hoc IS FALSE
|
|
AND
|
|
scheduled_delivery_at IS NOT NULL
|
|
)
|
|
""",
|
|
name=op.f('either_ad_hoc_or_scheduled_order'),
|
|
),
|
|
sa.CheckConstraint(
|
|
"""
|
|
NOT (
|
|
ad_hoc IS TRUE
|
|
AND (
|
|
EXTRACT(HOUR FROM placed_at) < 11
|
|
OR
|
|
EXTRACT(HOUR FROM placed_at) > 22
|
|
)
|
|
)
|
|
""",
|
|
name=op.f('ad_hoc_orders_within_business_hours'),
|
|
),
|
|
sa.CheckConstraint(
|
|
"""
|
|
NOT (
|
|
ad_hoc IS FALSE
|
|
AND (
|
|
(
|
|
EXTRACT(HOUR FROM scheduled_delivery_at) <= 11
|
|
AND
|
|
NOT (
|
|
EXTRACT(HOUR FROM scheduled_delivery_at) = 11
|
|
AND
|
|
EXTRACT(MINUTE FROM scheduled_delivery_at) = 45
|
|
)
|
|
)
|
|
OR
|
|
EXTRACT(HOUR FROM scheduled_delivery_at) > 22
|
|
)
|
|
)
|
|
""",
|
|
name=op.f('scheduled_orders_within_business_hours'),
|
|
),
|
|
sa.CheckConstraint(
|
|
"""
|
|
(
|
|
EXTRACT(MINUTES FROM scheduled_delivery_at)::INTEGER
|
|
% 15 = 0
|
|
)
|
|
AND
|
|
(
|
|
EXTRACT(SECONDS FROM scheduled_delivery_at)::INTEGER
|
|
= 0
|
|
)
|
|
""",
|
|
name=op.f('scheduled_orders_must_be_at_quarters_of_an_hour'),
|
|
),
|
|
sa.CheckConstraint(
|
|
"""
|
|
NOT (
|
|
EXTRACT(EPOCH FROM scheduled_delivery_at - placed_at) < 1800
|
|
)
|
|
""",
|
|
name=op.f('scheduled_orders_not_within_30_minutes'),
|
|
),
|
|
sa.CheckConstraint(
|
|
"""
|
|
cancelled_at IS NOT NULL
|
|
OR
|
|
(
|
|
restaurant_notified_at IS NOT NULL
|
|
AND
|
|
restaurant_confirmed_at IS NOT NULL
|
|
AND
|
|
restaurant_ready_at IS NOT NULL
|
|
AND
|
|
dispatch_at IS NOT NULL
|
|
AND
|
|
courier_id IS NOT NULL
|
|
AND
|
|
courier_notified_at IS NOT NULL
|
|
AND
|
|
courier_accepted_at IS NOT NULL
|
|
AND
|
|
reached_pickup_at IS NOT NULL
|
|
AND
|
|
pickup_at IS NOT NULL
|
|
AND
|
|
left_pickup_at IS NOT NULL
|
|
AND
|
|
reached_delivery_at IS NOT NULL
|
|
AND
|
|
delivery_at IS NOT NULL
|
|
)
|
|
""",
|
|
name=op.f('orders_must_be_either_cancelled_or_fully_simulated'),
|
|
),
|
|
sa.CheckConstraint(
|
|
"""
|
|
NOT ( -- Only occurred in 528 of 660,608 orders in the actual data.
|
|
cancelled_at IS NOT NULL
|
|
AND
|
|
pickup_at IS NOT NULL
|
|
)
|
|
AND
|
|
NOT ( -- Only occurred in 176 of 660,608 orders in the actual data.
|
|
cancelled_at IS NOT NULL
|
|
AND
|
|
left_pickup_at IS NOT NULL
|
|
)
|
|
AND
|
|
NOT ( -- Never occurred in the actual data.
|
|
cancelled_at IS NOT NULL
|
|
AND
|
|
reached_delivery_at IS NOT NULL
|
|
)
|
|
AND
|
|
NOT ( -- Never occurred in the actual data.
|
|
cancelled_at IS NOT NULL
|
|
AND
|
|
delivery_at IS NOT NULL
|
|
)
|
|
""",
|
|
name=op.f('cancellations_may_only_occur_before_pickup'),
|
|
),
|
|
sa.CheckConstraint(
|
|
'0 <= estimated_prep_duration AND estimated_prep_duration <= 3600',
|
|
name=op.f('estimated_prep_duration_between_0_and_3600'),
|
|
),
|
|
sa.CheckConstraint(
|
|
'estimated_prep_duration % 60 = 0',
|
|
name=op.f('estimated_prep_duration_must_be_whole_minutes'),
|
|
),
|
|
sa.CheckConstraint(
|
|
'0 <= utilization AND utilization <= 100',
|
|
name=op.f('utilization_between_0_and_100'),
|
|
),
|
|
sa.CheckConstraint(
|
|
"""
|
|
NOT (
|
|
EXTRACT(HOUR FROM restaurant_notified_at) < 11
|
|
OR
|
|
EXTRACT(HOUR FROM dispatch_at) < 11
|
|
)
|
|
""",
|
|
name=op.f('orders_dispatched_in_business_hours'),
|
|
),
|
|
*(
|
|
sa.CheckConstraint(
|
|
constraint, name='ordered_timestamps_{index}'.format(index=index),
|
|
)
|
|
for index, constraint in enumerate(
|
|
(
|
|
'placed_at < scheduled_delivery_at',
|
|
'placed_at < cancelled_at',
|
|
'placed_at < restaurant_notified_at',
|
|
'placed_at < restaurant_confirmed_at',
|
|
'placed_at < restaurant_ready_at',
|
|
'placed_at < dispatch_at',
|
|
'placed_at < first_estimated_delivery_at',
|
|
'placed_at < courier_notified_at',
|
|
'placed_at < courier_accepted_at',
|
|
'placed_at < reached_pickup_at',
|
|
'placed_at < pickup_at',
|
|
'placed_at < left_pickup_at',
|
|
'placed_at < reached_delivery_at',
|
|
'placed_at < delivery_at',
|
|
'cancelled_at > restaurant_notified_at',
|
|
'cancelled_at > restaurant_confirmed_at',
|
|
'cancelled_at > restaurant_ready_at',
|
|
'cancelled_at > dispatch_at',
|
|
'cancelled_at > courier_notified_at',
|
|
'cancelled_at > courier_accepted_at',
|
|
'cancelled_at > reached_pickup_at',
|
|
'restaurant_notified_at < restaurant_confirmed_at',
|
|
'restaurant_notified_at < restaurant_ready_at',
|
|
'restaurant_notified_at < pickup_at',
|
|
'restaurant_confirmed_at < restaurant_ready_at',
|
|
'restaurant_confirmed_at < pickup_at',
|
|
'restaurant_ready_at < pickup_at',
|
|
'dispatch_at < first_estimated_delivery_at',
|
|
'dispatch_at < courier_notified_at',
|
|
'dispatch_at < courier_accepted_at',
|
|
'dispatch_at < reached_pickup_at',
|
|
'dispatch_at < pickup_at',
|
|
'dispatch_at < left_pickup_at',
|
|
'dispatch_at < reached_delivery_at',
|
|
'dispatch_at < delivery_at',
|
|
'courier_notified_at < courier_accepted_at',
|
|
'courier_notified_at < reached_pickup_at',
|
|
'courier_notified_at < pickup_at',
|
|
'courier_notified_at < left_pickup_at',
|
|
'courier_notified_at < reached_delivery_at',
|
|
'courier_notified_at < delivery_at',
|
|
'courier_accepted_at < reached_pickup_at',
|
|
'courier_accepted_at < pickup_at',
|
|
'courier_accepted_at < left_pickup_at',
|
|
'courier_accepted_at < reached_delivery_at',
|
|
'courier_accepted_at < delivery_at',
|
|
'reached_pickup_at < pickup_at',
|
|
'reached_pickup_at < left_pickup_at',
|
|
'reached_pickup_at < reached_delivery_at',
|
|
'reached_pickup_at < delivery_at',
|
|
'pickup_at < left_pickup_at',
|
|
'pickup_at < reached_delivery_at',
|
|
'pickup_at < delivery_at',
|
|
'left_pickup_at < reached_delivery_at',
|
|
'left_pickup_at < delivery_at',
|
|
'reached_delivery_at < delivery_at',
|
|
),
|
|
)
|
|
),
|
|
schema=config.CLEAN_SCHEMA,
|
|
)
|
|
|
|
op.create_index(
|
|
op.f('ix_replay_simulations_on_city_id'),
|
|
'replay_simulations',
|
|
['city_id'],
|
|
unique=False,
|
|
schema=config.CLEAN_SCHEMA,
|
|
)
|
|
op.create_index(
|
|
op.f('ix_replay_simulations_on_strategy'),
|
|
'replay_simulations',
|
|
['strategy'],
|
|
unique=False,
|
|
schema=config.CLEAN_SCHEMA,
|
|
)
|
|
op.create_index(
|
|
op.f('ix_replayed_orders_on_courier_id'),
|
|
'replayed_orders',
|
|
['courier_id'],
|
|
unique=False,
|
|
schema=config.CLEAN_SCHEMA,
|
|
)
|
|
|
|
|
|
def downgrade():
|
|
"""Downgrade to revision b4dd0b8903a5."""
|
|
op.drop_table('replayed_orders', schema=config.CLEAN_SCHEMA)
|
|
op.drop_table('replay_simulations', schema=config.CLEAN_SCHEMA)
|
|
op.drop_constraint(
|
|
'uq_orders_on_id_ad_hoc', 'orders', type_=None, schema=config.CLEAN_SCHEMA,
|
|
)
|
|
op.execute(
|
|
f"""
|
|
ALTER TABLE {config.CLEAN_SCHEMA}.orders
|
|
DROP CONSTRAINT check_orders_on_scheduled_orders_must_be_at_quarters_of_an_hour;
|
|
""",
|
|
) # noqa:WPS355
|
|
op.execute(
|
|
f"""
|
|
ALTER TABLE {config.CLEAN_SCHEMA}.orders
|
|
DROP CONSTRAINT check_orders_on_ordered_timestamps_placed_at_before_pickup_at;
|
|
""",
|
|
) # noqa:WPS355
|
|
op.execute(
|
|
f"""
|
|
ALTER TABLE {config.CLEAN_SCHEMA}.orders
|
|
ADD CONSTRAINT ck_orders_on_ordered_timestamps_21
|
|
CHECK (cancelled_at > delivery_at);
|
|
""",
|
|
) # noqa:WPS355
|