| | 257 | === Flask Implementation === |
| | 258 | {{{ |
| | 259 | from sqlalchemy.exc import SQLAlchemyError |
| | 260 | from datetime import date |
| | 261 | |
| | 262 | def accept_rsvp_and_assign_seat(guest_id, event_id, table_number, role='Guest'): |
| | 263 | try: |
| | 264 | db.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 | |
| | 310 | except (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 === |
| | 321 | This 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 | |
| | 378 | === Flask Implementation === |
| | 379 | {{{ |
| | 380 | from sqlalchemy.exc import SQLAlchemyError |
| | 381 | |
| | 382 | def cancel_wedding(wedding_id): |
| | 383 | try: |
| | 384 | db.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 | |
| | 454 | except (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 === |
| | 465 | This 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 | |
| 344 | | === Why FOR UPDATE matters === |
| 345 | | Prevents TOCTOU race conditions by locking the wedding row during validation. |
| | 500 | === Flask Implementation === |
| | 501 | {{{ |
| | 502 | from sqlalchemy.exc import SQLAlchemyError |
| | 503 | |
| | 504 | def validate_wedding_budget(wedding_id): |
| | 505 | try: |
| | 506 | db.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 | |
| | 573 | except (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 === |
| | 584 | The 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. |
| | 631 | |
| | 632 | === Flask Implementation === |
| | 633 | {{{ |
| | 634 | from sqlalchemy.exc import SQLAlchemyError |
| | 635 | |
| | 636 | def partial_booking_example(wedding_id, venue_cfg, photo_cfg, band_cfg): |
| | 637 | try: |
| | 638 | db.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 | |
| | 678 | except SQLAlchemyError as e: |
| | 679 | db.session.rollback() |
| | 680 | |
| | 681 | return { |
| | 682 | 'status': 'error', |
| | 683 | 'reason': str(e) |
| | 684 | }, 500 |
| | 685 | |
| | 686 | }}} |
| | 687 | |
| | 688 | === Explanation === |
| | 689 | begin_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. |