Move Shipping Addresses to Their Own Table
Shipping addresses move out of orders and into their own addresses table, which unblocks split shipments and saved addresses next quarter. Migration 0087 creates the table and copies every existing address, the repository now reads and writes the new table, and 0088 drops the old ship_* columns. orders has about 140 million rows. I ran both migrations against last week's staging snapshot and they finished in about 6 minutes. Planning to ship it all in Thursday's release.
Amigrations/0087_create_addresses.sql1 of 4
Files Changed
migrations/
orders/
tests/
migrations/0087_create_addresses.sql
SQL · Click a line number to comment · Tap a line to comment@@ -0,0 +1,23 @@
Added: BEGIN;
Added:
Added: CREATE TABLE addresses (
Added: id bigserial PRIMARY KEY,
Added: order_id bigint NOT NULL REFERENCES orders (id),
Added: kind text NOT NULL,
Added: name text NOT NULL,
Added: line1 text NOT NULL,
Added: line2 text,
Added: city text NOT NULL,
Added: region text,
Added: postal_code text NOT NULL,
Added: country char(2) NOT NULL,
Added: created_at timestamptz NOT NULL DEFAULT now()
Added: );
Added:
Added: CREATE UNIQUE INDEX addresses_order_id_kind_key ON addresses (order_id, kind);
Added:
Added: INSERT INTO addresses (order_id, kind, name, line1, line2, city, region, postal_code, country)
Added: SELECT id, 'shipping', ship_name, ship_line1, ship_line2, ship_city, ship_region, ship_postal_code, ship_country
Added: FROM orders;
Added:
Added: COMMIT;
migrations/0088_drop_order_ship_columns.sql
SQL · Click a line number to comment · Tap a line to comment@@ -0,0 +1,8 @@
Added: ALTER TABLE orders
Added: DROP COLUMN ship_name,
Added: DROP COLUMN ship_line1,
Added: DROP COLUMN ship_line2,
Added: DROP COLUMN ship_city,
Added: DROP COLUMN ship_region,
Added: DROP COLUMN ship_postal_code,
Added: DROP COLUMN ship_country;
orders/repository.py
Python · Click a line number to comment · Tap a line to comment@@ -1,9 +1,9 @@
import logging
from orders.db import Database
Removed: from orders.models import Order
Added: from orders.models import Address, Order
log = logging.getLogger(__name__)
class OrderRepository:
@@ -13,22 +13,32 @@ class OrderRepository:
def get(self, order_id: int) -> Order | None:
row = self.db.fetch_one(
Removed: "SELECT id, customer_id, status, total_cents, ship_name, ship_line1, ship_line2,"
Removed: " ship_city, ship_region, ship_postal_code, ship_country"
Removed: " FROM orders WHERE id = %s",
Added: "SELECT o.id, o.customer_id, o.status, o.total_cents,"
Added: " a.name, a.line1, a.line2, a.city, a.region, a.postal_code, a.country"
Added: " FROM orders o JOIN addresses a ON a.order_id = o.id AND a.kind = 'shipping'"
Added: " WHERE o.id = %s",
(order_id,),
)
return Order.from_row(row) if row else None
def create(self, order: Order) -> int:
order_id = self.db.fetch_value(
Removed: "INSERT INTO orders (customer_id, status, total_cents, ship_name, ship_line1, ship_line2,"
Removed: " ship_city, ship_region, ship_postal_code, ship_country)"
Removed: " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id",
Removed: (order.customer_id, order.status, order.total_cents, *order.shipping.as_tuple()),
Added: "INSERT INTO orders (customer_id, status, total_cents) VALUES (%s, %s, %s) RETURNING id",
Added: (order.customer_id, order.status, order.total_cents),
)
Added: self._insert_shipping_address(order_id, order.shipping)
return order_id
Added: def _insert_shipping_address(self, order_id: int, address: Address) -> None:
Added: try:
Added: self.db.execute(
Added: "INSERT INTO addresses (order_id, kind, name, line1, line2, city, region, postal_code, country)"
Added: " VALUES (%s, 'shipping', %s, %s, %s, %s, %s, %s, %s)",
Added: (order_id, *address.as_tuple()),
Added: )
Added: except Exception:
Added: log.exception("could not write shipping address for order %s", order_id)
Added:
def cancel(self, order_id: int) -> None:
with self.db.transaction() as tx:
tx.execute("UPDATE orders SET status = 'cancelled' WHERE id = %s", (order_id,))
tx.execute("INSERT INTO order_events (order_id, kind) VALUES (%s, 'cancelled')", (order_id,))
tests/test_repository.py
Python · Click a line number to comment · Tap a line to comment@@ -0,0 +1,22 @@
Added: from orders.models import Address, Order
Added: from orders.repository import OrderRepository
Added:
Added:
Added: def make_order():
Added: shipping = Address("Ada Park", "12 Elm St", None, "Portland", "OR", "97201", "US")
Added: return Order(id=None, customer_id=7, status="placed", total_cents=4200, shipping=shipping)
Added:
Added:
Added: def test_create_then_get_round_trips_the_address(db):
Added: repo = OrderRepository(db)
Added: order_id = repo.create(make_order())
Added: order = repo.get(order_id)
Added: assert order.shipping.city == "Portland"
Added: assert order.shipping.postal_code == "97201"
Added:
Added:
Added: def test_create_writes_one_shipping_address(db):
Added: repo = OrderRepository(db)
Added: order_id = repo.create(make_order())
Added: count = db.fetch_value("SELECT count(*) FROM addresses WHERE order_id = %s", (order_id,))
Added: assert count == 1