Changes between Version 5 and Version 6 of P8


Ignore:
Timestamp:
05/18/26 21:17:23 (8 days ago)
Author:
211171
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • P8

    v5 v6  
    255255The attendance table has UNIQUE (guest_id, event_id). A guest may need to be reassigned to a different table later. Using ON CONFLICT DO UPDATE handles both the initial seat assignment and any re-assignment atomically, avoiding race conditions.
    256256
     257=== Flask Implementation ===
     258{{{
     259from sqlalchemy.exc import SQLAlchemyError
     260from datetime import date
     261
     262def accept_rsvp_and_assign_seat(guest_id, event_id, table_number, role='Guest'):
     263try:
     264db.session.begin()
     265
     266    # STEP 1: Find RSVP record
     267    rsvp = db.session.query(EventRSVP).filter_by(
     268        guest_id=guest_id,
     269        event_id=event_id
     270    ).first()
     271
     272    # RSVP must exist
     273    if not rsvp:
     274        raise ValueError('RSVP record not found')
     275
     276    # STEP 2: Update RSVP status
     277    rsvp.status = 'accepted'
     278    rsvp.response_date = date.today()
     279
     280    # STEP 3: Check if attendance already exists
     281    attendance = db.session.query(Attendance).filter_by(
     282        guest_id=guest_id,
     283        event_id=event_id
     284    ).first()
     285
     286    # STEP 4: UPSERT logic
     287    if attendance:
     288        attendance.status = 'attending'
     289        attendance.table_number = table_number
     290        attendance.role = role
     291    else:
     292        attendance = Attendance(
     293            status='attending',
     294            table_number=table_number,
     295            role=role,
     296            guest_id=guest_id,
     297            event_id=event_id
     298        )
     299        db.session.add(attendance)
     300
     301    db.session.commit()
     302
     303    return {
     304        'status': 'success',
     305        'guest_id': guest_id,
     306        'event_id': event_id,
     307        'table_number': table_number
     308    }
     309
     310except (ValueError, SQLAlchemyError) as e:
     311    db.session.rollback()
     312
     313    return {
     314        'status': 'error',
     315        'reason': str(e)
     316    }, 409
     317
     318}}}
     319
     320=== Explanation ===
     321This transaction guarantees that RSVP acceptance and attendance assignment happen together atomically. If attendance creation fails after the RSVP update, the rollback restores the RSVP to its previous state, preventing inconsistencies.
     322
    257323== Transaction 4 – Wedding Cancellation with Status Cascade ==
    258324=== Business Scenario ===
     
    310376A concurrent RSVP or attendance insert could occur between steps, causing inconsistent data tied to a cancelled wedding.
    311377
     378=== Flask Implementation ===
     379{{{
     380from sqlalchemy.exc import SQLAlchemyError
     381
     382def cancel_wedding(wedding_id):
     383try:
     384db.session.begin()
     385
     386    # STEP 1: Find wedding
     387    wedding = db.session.query(Wedding).filter_by(
     388        wedding_id=wedding_id
     389    ).first()
     390
     391    if not wedding:
     392        raise ValueError('Wedding not found')
     393
     394    # STEP 2: Update wedding status
     395    wedding.notes = 'Wedding cancelled'
     396
     397    # STEP 3: Cancel venue bookings
     398    db.session.query(VenueBooking).filter_by(
     399        wedding_id=wedding_id
     400    ).update({
     401        'status': 'cancelled'
     402    })
     403
     404    # STEP 4: Cancel photographer bookings
     405    db.session.query(PhotographerBooking).filter_by(
     406        wedding_id=wedding_id
     407    ).update({
     408        'status': 'cancelled'
     409    })
     410
     411    # STEP 5: Cancel band bookings
     412    db.session.query(BandBooking).filter_by(
     413        wedding_id=wedding_id
     414    ).update({
     415        'status': 'cancelled'
     416    })
     417
     418    # STEP 6: Cancel registrar bookings
     419    db.session.query(RegistrarBooking).filter_by(
     420        wedding_id=wedding_id
     421    ).update({
     422        'status': 'cancelled'
     423    })
     424
     425    # STEP 7: Cancel events
     426    db.session.query(Event).filter_by(
     427        wedding_id=wedding_id
     428    ).update({
     429        'status': 'cancelled'
     430    })
     431
     432    # STEP 8: Find all wedding events
     433    events = db.session.query(Event.event_id).filter_by(
     434        wedding_id=wedding_id
     435    ).all()
     436
     437    event_ids = [e.event_id for e in events]
     438
     439    # STEP 9: Mark attendance absent
     440    if event_ids:
     441        db.session.query(Attendance).filter(
     442            Attendance.event_id.in_(event_ids)
     443        ).update({
     444            'status': 'absent'
     445        }, synchronize_session=False)
     446
     447    db.session.commit()
     448
     449    return {
     450        'status': 'success',
     451        'wedding_id': wedding_id
     452    }
     453
     454except (ValueError, SQLAlchemyError) as e:
     455    db.session.rollback()
     456
     457    return {
     458        'status': 'error',
     459        'reason': str(e)
     460    }, 500
     461
     462}}}
     463
     464=== Explanation ===
     465This transaction performs a full cancellation cascade across all dependent tables. Every update occurs inside one transaction so the database can never contain partially cancelled wedding data.
     466
     467
    312468== Transaction 5 – Budget Validation with Row Locking ==
    313469=== Business Scenario ===
     
    342498}}}
    343499
    344 === Why FOR UPDATE matters ===
    345 Prevents TOCTOU race conditions by locking the wedding row during validation.
     500=== Flask Implementation ===
     501{{{
     502from sqlalchemy.exc import SQLAlchemyError
     503
     504def validate_wedding_budget(wedding_id):
     505try:
     506db.session.begin()
     507
     508    # STEP 1: Lock wedding row
     509    wedding = db.session.query(Wedding)\
     510        .filter_by(wedding_id=wedding_id)\
     511        .with_for_update()\
     512        .first()
     513
     514    if not wedding:
     515        raise ValueError('Wedding not found')
     516
     517    budget = wedding.budget
     518
     519    # STEP 2: Calculate venue costs
     520    venue_total = db.session.query(
     521        db.func.coalesce(db.func.sum(VenueBooking.price), 0)
     522    ).filter_by(
     523        wedding_id=wedding_id
     524    ).scalar()
     525
     526    # STEP 3: Calculate photographer costs
     527    photographer_total = db.session.query(
     528        db.func.coalesce(db.func.sum(PhotographerBooking.price), 0)
     529    ).filter_by(
     530        wedding_id=wedding_id
     531    ).scalar()
     532
     533    # STEP 4: Calculate band costs
     534    band_total = db.session.query(
     535        db.func.coalesce(db.func.sum(BandBooking.price), 0)
     536    ).filter_by(
     537        wedding_id=wedding_id
     538    ).scalar()
     539
     540    # STEP 5: Calculate registrar costs
     541    registrar_total = db.session.query(
     542        db.func.coalesce(db.func.sum(RegistrarBooking.price), 0)
     543    ).filter_by(
     544        wedding_id=wedding_id
     545    ).scalar()
     546
     547    # STEP 6: Compute total cost
     548    total_cost = (
     549        venue_total +
     550        photographer_total +
     551        band_total +
     552        registrar_total
     553    )
     554
     555    # STEP 7: Validate budget
     556    if total_cost > budget:
     557        raise ValueError(
     558            f'Budget exceeded. Total: {total_cost}, Budget: {budget}'
     559        )
     560
     561    # STEP 8: Finalise wedding
     562    wedding.notes = 'Budget validated – wedding finalised'
     563
     564    db.session.commit()
     565
     566    return {
     567        'status': 'success',
     568        'wedding_id': wedding_id,
     569        'budget': float(budget),
     570        'total_cost': float(total_cost)
     571    }
     572
     573except (ValueError, SQLAlchemyError) as e:
     574    db.session.rollback()
     575
     576    return {
     577        'status': 'error',
     578        'reason': str(e)
     579    }, 409
     580
     581}}}
     582
     583=== Explanation ===
     584The with_for_update() row lock prevents concurrent transactions from modifying the wedding budget or related financial data during validation. This avoids TOCTOU (Time Of Check To Time Of Use) race conditions.
    346585
    347586== Savepoints – Partial Rollback ==
     
    390629=== Why needed ===
    391630Pooling reuses DB connections instead of creating new ones per request.
     631
     632=== Flask Implementation ===
     633{{{
     634from sqlalchemy.exc import SQLAlchemyError
     635
     636def partial_booking_example(wedding_id, venue_cfg, photo_cfg, band_cfg):
     637try:
     638db.session.begin()
     639
     640    # STEP 1: Insert venue booking
     641    venue = VenueBooking(
     642        **venue_cfg,
     643        wedding_id=wedding_id
     644    )
     645    db.session.add(venue)
     646
     647    # STEP 2: Insert photographer booking
     648    photographer = PhotographerBooking(
     649        **photo_cfg,
     650        wedding_id=wedding_id
     651    )
     652    db.session.add(photographer)
     653
     654    # STEP 3: Create savepoint
     655    savepoint = db.session.begin_nested()
     656
     657    try:
     658        # STEP 4: Attempt risky band booking
     659        band = BandBooking(
     660            **band_cfg,
     661            wedding_id=wedding_id
     662        )
     663
     664        db.session.add(band)
     665        db.session.flush()
     666
     667    except SQLAlchemyError:
     668        # STEP 5: Roll back only to savepoint
     669        savepoint.rollback()
     670
     671    # STEP 6: Continue transaction
     672    db.session.commit()
     673
     674    return {
     675        'status': 'partial_success'
     676    }
     677
     678except SQLAlchemyError as e:
     679    db.session.rollback()
     680
     681    return {
     682        'status': 'error',
     683        'reason': str(e)
     684    }, 500
     685
     686}}}
     687
     688=== Explanation ===
     689begin_nested() creates a database savepoint. If the band booking fails, only the operations after the savepoint are rolled back while earlier successful inserts remain intact.
    392690
    393691=== Affected endpoints ===