حذف یک ردیف در پایگاه دادهٔ مهندسی، فقط کم‌شدن تعداد رکوردها نیست. اگر دستگاهی چند نمونهٔ ولتاژ داشته باشد، با حذف دستگاه چه اتفاقی برای نمونه‌ها می‌افتد؟ پاسخ باید در مدل رابطه‌ای و آزمون آن روشن باشد. این درس سه قرارداد حذف در SQLite را با دادهٔ ساختگی مقایسه می‌کند؛ تفاوت اصلی، رفتار وابستگی و زمان آشکارشدن خطاست. همهٔ اجراها در حافظه‌اند و هیچ پایگاه دادهٔ موجود یا فایل اندازه‌گیری تغییر نمی‌کند.

۱. یک مدل کوچک با واحد و وابستگی روشن

کلید والد و وابستگی رکوردهای فرزند
تصویر مفهومی: کلید والد و وابستگی رکوردهای فرزند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

جدول device دستگاه‌های یک، دو و سه را دارد. جدول sample سه نمونه نگه می‌دارد: شناسه‌های یازده و دوازده به دستگاه یک وابسته‌اند و مقدارشان ۱۲۰۰ و ۱۳۰۰ میلی‌ولت است؛ نمونهٔ بیست‌ویک به دستگاه دو تعلق دارد و ۲۱۰۰ میلی‌ولت دارد. دستگاه سه فعلاً نمونه‌ای ندارد. این اعداد ثابت، مرجع دستی‌اند؛ دادهٔ واقعی حسگر یا نتیجهٔ کالیبراسیون نیستند.

ستون device_id فرزند به شناسهٔ والد اشاره می‌کند. NOT NULL در جدول اصلی، نبودن شناسهٔ دستگاه را جداگانه ممنوع می‌کند. کلید خارجی صرفاً سازگاری این ارجاع را کنترل می‌کند؛ به‌تنهایی درستی ولتاژ، هویت دستگاه یا مجوز حذف را ثابت نمی‌کند. انتخاب کنش حذف نیز تصمیم نگهداری داده است؛ حذف خودکار سابقهٔ اندازه‌گیری را نباید بدون نیازمندی مشخص انتخاب کرد.

مطابق مستندات رسمی کلید خارجی SQLite، فعال‌سازی را برای هر اتصال صریح انجام می‌دهیم. پیش از BEGIN، دستور PRAGMA foreign_keys=ON اجرا و مقدار آن خوانده می‌شود. به پیش‌فرض نسخه یا تنظیمات ساخت کتابخانه تکیه نمی‌کنیم. آزمون جدا نشان می‌دهد که تلاش برای خاموش‌کردن همین تنظیم درون تراکنش فعال، در این قرارداد اثری ندارد؛ این کار راه تغییر سیاست وسط تراکنش نیست.

۲. سه کنش حذف و دو زمان متفاوتِ بررسی

حذف وابسته با قرارداد CASCADE
تصویر مفهومی: حذف وابسته با قرارداد CASCADE؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

با CASCADE، حذف والد باعث حذف فرزندان ارجاع‌دهنده به همان والد می‌شود. در مثال ما، حذف دستگاه یک نمونه‌های یازده و دوازده را نیز حذف می‌کند؛ دستگاه‌های دو و سه و نمونهٔ بیست‌ویک باقی می‌مانند. این کنش معادل پاک‌کردن همهٔ جدول نمونه نیست. اگر حذف داخل تراکنش باشد، ROLLBACK صریح در آزمون ما والد و فرزندان را به وضعیت آغازین برمی‌گرداند.

RESTRICT اجازهٔ حذف والدِ دارای فرزند را نمی‌دهد و نکتهٔ مهم زمان آن است: حتی وقتی قید با DEFERRABLE INITIALLY DEFERRED تعریف شده باشد، محدودیتِ RESTRICT هنگام تغییر والد اعمال می‌شود. نباید موفقیت DELETE را تا COMMIT فرض کنیم. در اجرای ما، همان DELETE خطا می‌دهد؛ داده‌ها باقی می‌مانند و تراکنش همچنان فعال است. بعد می‌توان فرزندان را صریح حذف کرد و سپس والد را حذف و ثبت کرد.

NO ACTION به معنی خاموش‌بودن کلید خارجی نیست. زمان بررسی قید تعیین‌کننده است. با قید معوق، حذف والد می‌تواند فعلاً در تراکنش خودمان موفق شود، درحالی‌که فرزندان موقتاً بدون والدند؛ اما COMMIT تا رفع ناسازگاری شکست می‌خورد. در مقایسهٔ قید فوری، همین حذف در پایان دستور رد می‌شود. این تفاوت‌ها در بخش کنش‌ها و قیود معوقِ همان منبع رسمی توضیح داده شده‌اند.

۳. کد کامل؛ کنترل تراکنش بدون ابهام پایتون

رد فوری در RESTRICT حتی با قید معوق
تصویر مفهومی: رد فوری در RESTRICT حتی با قید معوق؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

هر سناریو اتصال مستقلِ :memory: می‌سازد. در قرارداد sqlite3 پایتون، با autocommit=True روش‌های commit و rollback اتصال اثری ندارند؛ بنابراین این مثال عمداً از دستورهای SQL صریح BEGIN، COMMIT و ROLLBACK استفاده می‌کند. in_transaction وضعیت فعالِ تراکنشِ سطح پایین را نشان می‌دهد. این دو مفهوم را نباید فقط از شباهت نام‌ها یکی گرفت.

"""Original in-memory SQLite deletion-policy/deferred-constraint lesson."""
import json
import platform
import sqlite3


PARENTS = [(1, 'S1'), (2, 'S2'), (3, 'S3')]
SAMPLES = [(11, 1, 1200), (12, 1, 1300), (21, 2, 2100)]


def make_fixture(action, deferred=True):
    if type(action) is not str or action not in ('NO ACTION', 'RESTRICT', 'CASCADE'):
        raise ValueError('only authored fixed deletion actions allowed')
    if type(deferred) is not bool:
        raise TypeError('deferred must be exact bool')
    con = sqlite3.connect(':memory:', autocommit=True)
    con.execute('PRAGMA foreign_keys=ON')
    if con.execute('PRAGMA foreign_keys').fetchone() != (1,):
        con.close()
        raise RuntimeError('foreign-key enforcement unavailable')
    con.execute('CREATE TABLE device(id INTEGER PRIMARY KEY, name TEXT NOT NULL)')
    timing = 'DEFERRABLE INITIALLY DEFERRED' if deferred else 'NOT DEFERRABLE INITIALLY IMMEDIATE'
    # SQL keywords come only from the fixed whitelist; data always uses placeholders.
    con.execute('CREATE TABLE sample(id INTEGER PRIMARY KEY, device_id INTEGER NOT NULL, voltage_mv INTEGER NOT NULL,'
                ' FOREIGN KEY(device_id) REFERENCES device(id) ON DELETE ' + action + ' ' + timing + ')')
    con.execute('CREATE INDEX sample_device ON sample(device_id)')
    con.executemany('INSERT INTO device VALUES(?,?)', PARENTS)
    con.executemany('INSERT INTO sample VALUES(?,?,?)', SAMPLES)
    return con


def snapshot(con):
    return {'devices': con.execute('SELECT id,name FROM device ORDER BY id').fetchall(),
            'samples': con.execute('SELECT id,device_id,voltage_mv FROM sample ORDER BY id').fetchall()}


def _raises(expected, function, *args):
    try:
        function(*args)
    except expected:
        return True
    return False


def run_example():
    connections = []

    def fresh(action='NO ACTION', deferred=True):
        con = make_fixture(action, deferred)
        connections.append(con)
        return con

    initial = {'devices': PARENTS, 'samples': SAMPLES}
    try:
        cascade = fresh('CASCADE')
        cascade.execute('BEGIN')
        cascade.execute('DELETE FROM device WHERE id=?', (1,))
        cascade_inside = snapshot(cascade)
        cascade.execute('COMMIT')
        cascade_final = snapshot(cascade)
        cascade_rollback = fresh('CASCADE')
        cascade_rollback.execute('BEGIN')
        cascade_rollback.execute('DELETE FROM device WHERE id=?', (1,))
        cascade_rollback.execute('ROLLBACK')
        restricted = fresh('RESTRICT')
        restricted.execute('BEGIN')
        restrict_rejected = _raises(sqlite3.IntegrityError, restricted.execute, 'DELETE FROM device WHERE id=?', (1,))
        restricted_snapshot = snapshot(restricted)
        restrict_still_active = restricted.in_transaction
        restricted.execute('DELETE FROM sample WHERE device_id=?', (1,))
        restricted.execute('DELETE FROM device WHERE id=?', (1,))
        restricted.execute('COMMIT')
        no_action = fresh('NO ACTION')
        no_action.execute('BEGIN')
        no_action.execute('DELETE FROM device WHERE id=?', (1,))
        deferred_inside = snapshot(no_action)
        violations = sorted(no_action.execute('PRAGMA foreign_key_check').fetchall())
        commit_failed = _raises(sqlite3.IntegrityError, no_action.execute, 'COMMIT')
        deferred_still_active = no_action.in_transaction
        no_action.execute('INSERT INTO device VALUES(?,?)', (1, 'S1'))
        no_action.execute('COMMIT')
        repaired = snapshot(no_action)
        rolled_back = fresh('NO ACTION')
        rolled_back.execute('BEGIN')
        rolled_back.execute('DELETE FROM device WHERE id=?', (1,))
        rollback_commit_failed = _raises(sqlite3.IntegrityError, rolled_back.execute, 'COMMIT')
        rolled_back.execute('ROLLBACK')
        orphan = fresh('NO ACTION')
        orphan.execute('BEGIN')
        orphan.execute('INSERT INTO sample VALUES(?,?,?)', (91, 9, 900))
        orphan_inside = snapshot(orphan)
        orphan_commit_failed = _raises(sqlite3.IntegrityError, orphan.execute, 'COMMIT')
        orphan_active = orphan.in_transaction
        orphan.execute('INSERT INTO device VALUES(?,?)', (9, 'S9'))
        orphan.execute('COMMIT')
        immediate = fresh('NO ACTION', False)
        immediate.execute('BEGIN')
        immediate_rejected = _raises(sqlite3.IntegrityError, immediate.execute, 'DELETE FROM device WHERE id=?', (1,))
        nulls = fresh('NO ACTION')
        nulls.execute('CREATE TABLE optional_sample(id INTEGER PRIMARY KEY, device_id INTEGER REFERENCES device(id))')
        nulls.execute('INSERT INTO optional_sample VALUES(?,?)', (99, None))
        null_child_rejected = _raises(sqlite3.IntegrityError, nulls.execute, 'INSERT INTO sample VALUES(?,?,?)', (99, None, 0))
        toggled = fresh('NO ACTION')
        toggled.execute('BEGIN')
        toggled.execute('PRAGMA foreign_keys=OFF')
        enabled_inside = toggled.execute('PRAGMA foreign_keys').fetchone()
        toggled.execute('ROLLBACK')
        enabled_after = toggled.execute('PRAGMA foreign_keys').fetchone()
        oracle_cases = []
        for action in ('CASCADE', 'RESTRICT', 'NO ACTION'):
            for device_id in (1, 2, 3, 99):
                con = fresh(action)
                con.execute('BEGIN')
                has_children = any(row[1] == device_id for row in SAMPLES)
                if action == 'RESTRICT' and has_children:
                    rejected = _raises(sqlite3.IntegrityError, con.execute, 'DELETE FROM device WHERE id=?', (device_id,))
                    con.execute('ROLLBACK')
                    oracle_cases.append(rejected and snapshot(con) == initial)
                elif action == 'NO ACTION' and has_children:
                    con.execute('DELETE FROM device WHERE id=?', (device_id,))
                    rejected = _raises(sqlite3.IntegrityError, con.execute, 'COMMIT')
                    remained_active = con.in_transaction
                    con.execute('ROLLBACK')
                    oracle_cases.append(rejected and remained_active and snapshot(con) == initial)
                else:
                    con.execute('DELETE FROM device WHERE id=?', (device_id,))
                    con.execute('COMMIT')
                    expected = {'devices': [row for row in PARENTS if row[0] != device_id],
                                'samples': [row for row in SAMPLES if row[1] != device_id]}
                    oracle_cases.append(snapshot(con) == expected)
        expected_after_delete = {'devices': [(2, 'S2'), (3, 'S3')], 'samples': [(21, 2, 2100)]}
        checks = {
            'foreign_keys_enabled_for_every_connection_before_transactions': all(con.execute('PRAGMA foreign_keys').fetchone() == (1,) for con in connections),
            'hand_initial_device_and_millivolt_sample_fixture': snapshot(fresh()) == initial,
            'cascade_removes_only_referencing_children_with_parent': cascade_inside == expected_after_delete and cascade_final == expected_after_delete,
            'cascade_is_rolled_back_with_explicit_transaction': snapshot(cascade_rollback) == initial and not cascade_rollback.in_transaction,
            'restrict_rejects_delete_immediately_even_declared_deferred': restrict_rejected,
            'restrict_failed_statement_keeps_data_and_transaction_active': restrict_still_active and restricted_snapshot == initial,
            'restrict_allows_explicit_child_delete_then_parent_commit': snapshot(restricted) == expected_after_delete and not restricted.in_transaction,
            'deferred_no_action_allows_temporary_orphans_inside_same_transaction': deferred_inside == {'devices': [(2, 'S2'), (3, 'S3')], 'samples': SAMPLES},
            'foreign_key_check_lists_hand_orphan_rowids': violations == [('sample', 11, 'device', 0), ('sample', 12, 'device', 0)],
            'deferred_commit_fails_and_transaction_remains_active': commit_failed and deferred_still_active,
            'repair_parent_after_failed_commit_then_commit_succeeds': repaired == initial and not no_action.in_transaction,
            'rollback_after_failed_commit_restores_initial_rows': rollback_commit_failed and snapshot(rolled_back) == initial and not rolled_back.in_transaction,
            'orphan_insert_deferred_until_commit_not_final_approval': orphan_inside['samples'][-1] == (91, 9, 900) and orphan_commit_failed and orphan_active,
            'insert_missing_parent_repairs_deferred_child_then_commit': snapshot(orphan) == {'devices': PARENTS + [(9, 'S9')], 'samples': SAMPLES + [(91, 9, 900)]}
                and orphan.execute('PRAGMA foreign_key_check').fetchall() == [],
            'immediate_no_action_rejects_at_statement_end': immediate_rejected and snapshot(immediate) == initial,
            'nullable_relationship_distinct_from_not_null_child_contract': nulls.execute('SELECT device_id FROM optional_sample').fetchall() == [(None,)]
                and null_child_rejected and snapshot(nulls) == initial,
            'foreign_key_toggle_inside_transaction_is_no_op': enabled_inside == enabled_after == (1,),
            'independent_parent_child_scan_oracle_twelve_policy_cases': len(oracle_cases) == 12 and all(oracle_cases),
        }
        assert len(checks) == 18 and all(type(v) is bool and v for v in checks.values())
        return {'runtime': {'python': platform.python_version(), 'sqlite_runtime': sqlite3.sqlite_version,
                            'storage': 'independent :memory: connections only, no existing file opened'},
                'contract': {'tables': 'device parent, sample child; integer fixture values in millivolts',
                             'actions': ['CASCADE', 'RESTRICT', 'NO ACTION'],
                             'timing': 'DEFERRABLE INITIALLY DEFERRED except one explicit immediate comparison',
                             'control': 'autocommit=True Python connection; explicit SQL BEGIN/COMMIT/ROLLBACK'},
                'metrics': {'initial': initial, 'cascade_after_commit': cascade_final,
                            'restrict_rejected_delete': restrict_rejected, 'restrict_failed_statement_transaction_active': restrict_still_active,
                            'no_action_inside_before_commit': deferred_inside, 'foreign_key_check_before_repair': violations,
                            'no_action_commit_failed': commit_failed, 'no_action_failed_commit_transaction_active': deferred_still_active,
                            'repaired_and_committed': repaired, 'independent_policy_reference_cases': len(oracle_cases)},
                'checks': checks,
                'limitations': ['Synthetic single-connection-per-case in-memory tests; no persistent durability, backup, concurrency, capacity or hardware authenticity claim.',
                                'Foreign keys enforce relational consistency, not authorization or permission to delete historical engineering data.',
                                'No UPDATE cascade, SET NULL/DEFAULT actions, savepoints, composite keys, migrations or hostile SQL input supported.',
                                'Direct SQL fixture values are predefined integers, not a new Python record-type/physical calibration validator.',
                                'Temporary inconsistent rows are only observed inside their own uncommitted transaction; they are not successfully committed or globally published.']}
    finally:
        for con in connections:
            if con.in_transaction:
                con.execute('ROLLBACK')
            con.close()


if __name__ == '__main__':
    print(json.dumps(run_example(), ensure_ascii=False, indent=2))

نام کنش فقط از سه مقدار ثابتِ مجاز انتخاب می‌شود؛ چون کلمهٔ کلیدی SQL را نمی‌توان مثل مقدار داده جایگزین کرد، رشتهٔ دلخواه وارد آن نمی‌کنیم. مقدار شناسه‌ها و اندازه‌گیری‌ها با جای‌نگهدار متصل می‌شوند. همهٔ مقادیرِ نمونه از پیش عدد صحیح‌اند؛ این برنامه قرارداد تازه‌ای برای اعتبارسنجی ورودی پایتون، NaN یا نوع فیزیکی رکورد نیست. نمایهٔ ستون فرزند هم وجود دارد، ولی اندازه‌گیری سرعت انجام نشده است.

در پایان هر آزمایش، اگر تراکنشی باز باشد، کد آن را صریح بازمی‌گرداند و اتصال را می‌بندد. این رفتار، مدیریتِ همان اتصال‌های آموزشی است. قواعد کلی BEGIN، COMMIT و ROLLBACK نباید به ادعای نادرستِ «هر خطای SQL تمام تراکنش را خودکار برمی‌گرداند» تبدیل شوند؛ وضعیت را در نقاط مورد نظر می‌خوانیم و تصمیم بعدی را صریح می‌گیریم.

۴. مرجع دستی، خطای ثبت و راه ترمیم

ناهماهنگی موقت و شکست ثبت نهایی
تصویر مفهومی: ناهماهنگی موقت و شکست ثبت نهایی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

کد با Python 3.14.5 و SQLite 3.50.4 اجرا شده است. پس از DELETE دستگاه یک با NO ACTION معوق، در تراکنش خودمان دو والد و هر سه نمونه را می‌بینیم. foreign_key_check دو تخلف برای rowidهای یازده و دوازده گزارش می‌کند. چهار ستون خروجی به‌ترتیب جدول فرزند، rowid فرزند، جدول والد و شمارهٔ قید هستند؛ این ابزار تشخیص است و خودش داده را ترمیم نمی‌کند.

{
  "runtime": {
    "python": "3.14.5",
    "sqlite_runtime": "3.50.4",
    "storage": "independent :memory: connections only, no existing file opened"
  },
  "contract": {
    "tables": "device parent, sample child; integer fixture values in millivolts",
    "actions": [
      "CASCADE",
      "RESTRICT",
      "NO ACTION"
    ],
    "timing": "DEFERRABLE INITIALLY DEFERRED except one explicit immediate comparison",
    "control": "autocommit=True Python connection; explicit SQL BEGIN/COMMIT/ROLLBACK"
  },
  "metrics": {
    "initial": {
      "devices": [
        [
          1,
          "S1"
        ],
        [
          2,
          "S2"
        ],
        [
          3,
          "S3"
        ]
      ],
      "samples": [
        [
          11,
          1,
          1200
        ],
        [
          12,
          1,
          1300
        ],
        [
          21,
          2,
          2100
        ]
      ]
    },
    "cascade_after_commit": {
      "devices": [
        [
          2,
          "S2"
        ],
        [
          3,
          "S3"
        ]
      ],
      "samples": [
        [
          21,
          2,
          2100
        ]
      ]
    },
    "restrict_rejected_delete": true,
    "restrict_failed_statement_transaction_active": true,
    "no_action_inside_before_commit": {
      "devices": [
        [
          2,
          "S2"
        ],
        [
          3,
          "S3"
        ]
      ],
      "samples": [
        [
          11,
          1,
          1200
        ],
        [
          12,
          1,
          1300
        ],
        [
          21,
          2,
          2100
        ]
      ]
    },
    "foreign_key_check_before_repair": [
      [
        "sample",
        11,
        "device",
        0
      ],
      [
        "sample",
        12,
        "device",
        0
      ]
    ],
    "no_action_commit_failed": true,
    "no_action_failed_commit_transaction_active": true,
    "repaired_and_committed": {
      "devices": [
        [
          1,
          "S1"
        ],
        [
          2,
          "S2"
        ],
        [
          3,
          "S3"
        ]
      ],
      "samples": [
        [
          11,
          1,
          1200
        ],
        [
          12,
          1,
          1300
        ],
        [
          21,
          2,
          2100
        ]
      ]
    },
    "independent_policy_reference_cases": 12
  },
  "checks": {
    "foreign_keys_enabled_for_every_connection_before_transactions": true,
    "hand_initial_device_and_millivolt_sample_fixture": true,
    "cascade_removes_only_referencing_children_with_parent": true,
    "cascade_is_rolled_back_with_explicit_transaction": true,
    "restrict_rejects_delete_immediately_even_declared_deferred": true,
    "restrict_failed_statement_keeps_data_and_transaction_active": true,
    "restrict_allows_explicit_child_delete_then_parent_commit": true,
    "deferred_no_action_allows_temporary_orphans_inside_same_transaction": true,
    "foreign_key_check_lists_hand_orphan_rowids": true,
    "deferred_commit_fails_and_transaction_remains_active": true,
    "repair_parent_after_failed_commit_then_commit_succeeds": true,
    "rollback_after_failed_commit_restores_initial_rows": true,
    "orphan_insert_deferred_until_commit_not_final_approval": true,
    "insert_missing_parent_repairs_deferred_child_then_commit": true,
    "immediate_no_action_rejects_at_statement_end": true,
    "nullable_relationship_distinct_from_not_null_child_contract": true,
    "foreign_key_toggle_inside_transaction_is_no_op": true,
    "independent_parent_child_scan_oracle_twelve_policy_cases": true
  },
  "limitations": [
    "Synthetic single-connection-per-case in-memory tests; no persistent durability, backup, concurrency, capacity or hardware authenticity claim.",
    "Foreign keys enforce relational consistency, not authorization or permission to delete historical engineering data.",
    "No UPDATE cascade, SET NULL/DEFAULT actions, savepoints, composite keys, migrations or hostile SQL input supported.",
    "Direct SQL fixture values are predefined integers, not a new Python record-type/physical calibration validator.",
    "Temporary inconsistent rows are only observed inside their own uncommitted transaction; they are not successfully committed or globally published."
  ]
}

COMMIT خطا می‌دهد و in_transaction همچنان درست است. یک مسیر آزمون، والد یک را دوباره درج می‌کند و سپس COMMIT موفق می‌شود؛ مسیر مستقل دیگر پس از خطای ثبت، ROLLBACK می‌کند و وضعیت آغازین را می‌گیرد. موفقیت DELETE در مسیر اول به معنی ثبت موفق نبود. ناسازگاری موقت فقط در تراکنشِ ثبت‌نشدهٔ همان اتصال مشاهده شده است؛ دادهٔ ناسازگار را ثبت‌شده یا برای سایر اتصال‌ها منتشرشده معرفی نمی‌کنیم.

مرجع مستقل، فهرست والدها و فرزندان را در پایتون پیمایش می‌کند. سه کنش را برای والد دارای فرزند، والد بدون فرزند و شناسهٔ غایب مقایسه می‌کنیم؛ دوازده حالت، یک گروه از هجده گروه نام‌دار است. علاوه بر آن، درج فرزند با والدِ مفقود، ترمیم والد، اثر NULL، بازگردانی CASCADE و زمان خطای RESTRICT کنترل می‌شوند. اجرای دوبارهٔ برنامه، آزمون تازه حساب نمی‌شود.

در جدول مقایسه‌ایِ nullable، ارجاع NULL پذیرفته می‌شود؛ جدول اصلی همان مقدار را به دلیل NOT NULL رد می‌کند. این دو قاعده مستقل‌اند. برای مرزهای دیگر، آزمون تراکنش دادهٔ حسگر و قیود رکورد حسگر در SQLite را بخوانید. این درس دوام فایل، هم‌زمانی، مهاجرت، کلید مرکب یا کنش‌های UPDATE و SET NULL را آزمایش نکرده است.

منابع در ۲۴ مهر ۱۴۰۵ خوانده شدند. تاریخ معرفی پشتیبانی کلید خارجی، چهاردهم اکتبر ۲۰۰۹ در نسخهٔ 3.6.19، تاریخ انتشار صفحهٔ مستندات نیست. تاریخ اصلی صفحه‌ها مشخص ثبت نشده است. مستندات پایتون نسخهٔ 3.14.8 دارند و از نسخهٔ اجرای کد جدا هستند؛ این آموزش گزارش قابلیت تازه یا آزمایش خرابی برق و دیسک نیست.

۵. چهار پرسش با پاسخ کامل

بازگردانی یا ترمیم پیش از COMMIT
تصویر مفهومی: بازگردانی یا ترمیم پیش از COMMIT؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: حذف دستگاه یک با CASCADE چه می‌گذارد؟ پاسخ: والدهای دو و سه و فقط نمونهٔ بیست‌ویکِ دستگاه دو با ۲۱۰۰ میلی‌ولت باقی می‌مانند؛ دو نمونهٔ متعلق به دستگاه یک حذف می‌شوند.

پرسش دوم: RESTRICT معوق تا COMMIT صبر می‌کند؟ پاسخ: خیر؛ در این سیاست، وجود فرزندان همان تغییر والد را رد می‌کند، حتی اگر تعریف قید معوق باشد. حذف صریح فرزندان و سپس والد مسیر دیگری است.

پرسش سوم: DELETE موفق با NO ACTION معوق کافی است؟ پاسخ: خیر؛ COMMIT ممکن است شکست بخورد و تراکنش باز بماند. در مثال، یا والد را ترمیم و دوباره ثبت می‌کنیم، یا صریحاً ROLLBACK می‌کنیم؛ نتیجهٔ نهایی باید روشن باشد.

پرسش چهارم: کلید خارجی NULL و مجوز حذف را کنترل می‌کند؟ پاسخ: ارجاع nullable می‌تواند NULL باشد؛ برای ممنوعیت آن، NOT NULL جدا لازم است. کلید خارجی نیز مجوز کاربر، سیاست نگهداری تاریخچه و صحت فیزیکی اندازه‌گیری را تعیین نمی‌کند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.