فهرست مقاله
فرض کنید ابزارِ بررسیِ یک آزمایش، برای هر نمونه یک ردیفِ والد و چند ردیفِ وابسته نگه میدارد. میخواهیم برچسبِ نسخهٔ والد را تغییر بدهیم و انتظار داریم وابستهها باقی بمانند. دو دستور میتوانند در پایان دقیقاً همان شناسه و نسخه را در جدولِ والد نشان بدهند، اما جدولِ وابستهٔ یک مسیر خالیتر باشد. دیدنِ ردیفِ نهاییِ والد برای تشخیص این تفاوت کافی نیست. این آموزش، همین اشتباهِ پیادهسازی را با یک پایگاه دادهٔ کوچکِ حافظهای و خروجیِ هر مرحله بررسی میکند.
۱. یک والدِ تازه با همان شناسه، همان سابقه نیست

رابطِ apply_parent_writes سه tuple میگیرد: والدهای اولیه، فرزندان اولیه و نوشتنهای پیاپی. هر والد زوجِ شناسه و revision است؛ revision فقط برچسبِ نسخهٔ ساختگی و بدون واحد است. هر فرزند نیز شناسهٔ خودش و شناسهٔ والدش را دارد. از روی این اعداد، ولتاژ، زمان، کالیبراسیون یا هویتِ سختافزار نتیجه نمیگیریم. فایدهٔ مهندسیِ مثال، آشکارکردنِ قراردادِ تغییرِ فراداده و نگهداریِ وابستگی است.
در شاهدِ دستی، والدهای صفر و یک بهترتیب نسخههای دو و چهار دارند. فرزندان صفر و یک به والد صفر وصلاند و فرزند دو به والد یک. اکنون نسخهٔ والد صفر را به هفت میبریم. مسیر نخست باید نشان بدهد که جایگزینیِ ردیف میتواند فرزندان صفر و یک را حذف کند؛ مسیر دوم، همان تغییرِ نسخه را بدون حذفِ آن دو انجام میدهد. فرزندِ متعلق به والد یک در هر دو مسیر باید باقی بماند.
دامنه عمداً محدود است: حداکثر سه والد، چهار فرزند و چهار نوشتن؛ شناسهٔ والد از صفر تا دو، شناسهٔ فرزند از صفر تا سه و نسخه از صفر تا نه. شناسههای آغازین باید یکتا باشند و هر فرزند، والدِ موجود داشته باشد. همهٔ مقدارها دقیقاً int هستند و bool یا رشتهٔ عددی پذیرفته نیست. این سقفها سیاستِ آموزشیِ تابعاند؛ محدودیتِ عمومیِ SQLite معرفی نمیشوند.
۲. دو دستور با ردیفِ نهاییِ شبیه و اثرِ جانبیِ متفاوت

طبق قرارداد رسمیِ ON CONFLICT، وقتی REPLACE با تعارضِ PRIMARY KEY یا UNIQUE روبهرو میشود، ردیفهای قبلیِ ایجادکنندهٔ تعارض را پیش از درج حذف میکند. بنابراین INSERT OR REPLACE کوتاهنویسیِ یک UPDATE معمولی نیست. در جدولِ ثابتِ ما، فقط شناسهٔ صحیحِ والد کلیدِ یکتا است؛ تعارضِ دوم، کلیدِ تهی و تغییرِ کلید در این آزمایش حضور ندارند.
مسیر دیگر از UPSERT رسمیِ SQLite استفاده میکند: ON CONFLICT(id) DO UPDATE SET revision=excluded.revision. در این عبارت، excluded.revision مقدارِ پیشنهادیِ درج را میرساند. اگر شناسه از قبل وجود داشته باشد، فقط فیلدِ غیرکلیدیِ همان ردیف تغییر میکند؛ اگر وجود نداشته باشد، ردیف درج میشود. UPSERT همهٔ خطاهای قید را درمان نمیکند؛ شکستِ NOT NULL، CHECK و کلید خارجی، تعارضِ یکتاییِ این مسیر محسوب نمیشود.
علتِ حذفِ وابستهها را هم صریح نوشتهایم: ستونِ parent_id به والد ارجاع میدهد و ON DELETE CASCADE دارد. مطابق بخشِ فعالسازی و کنشهای کلید خارجی، هر اتصال ابتدا foreign_keys را فعال و مقدارش را دوباره میخواند. اگر یک برنگردد، آزمایش متوقف میشود. این تنظیم پیش از ایجادِ داده انجام میشود؛ به پیشفرضِ کتابخانه یا تغییرِ تنظیم در میانهٔ تراکنش تکیه نداریم.
مستنداتِ REPLACE دربارهٔ اجرای DELETE trigger با recursive_triggers نیز شرطِ جدا دارد. این مثال هیچ trigger کاربری نمیسازد و از روشن یا خاموش بودنِ آن، رفتارِ CASCADE را حدس نمیزند. کنشِ کلید خارجی و trigger نوشتهشده توسط کاربر را نباید یک قرارداد دانست. همچنین شمارندهٔ تغییرات، rowid و update hook معیارِ تشخیصِ ما نیستند؛ خودِ ردیفها را میخوانیم.
۳. کدِ کامل و مرجعِ مستقلِ فهرستی

تمامِ درخواست، حتی آخرین نوشتن، پیش از بازشدنِ اتصال اعتبارسنجی میشود. tuple و str و int باید نوعِ دقیقِ خواستهشده باشند؛ مولد برای تشخیصِ نامعتبر بودن مصرف نمیشود. نامِ دستور فقط replace یا upsert است و به یکی از دو رشتهٔ SQL ثابت نگاشت میشود. همهٔ مقدارهای داده با جاینگهدار متصل میشوند. تابع راهی برای فرستادنِ SQL دلخواه، فایلِ پایگاه داده یا اتصالِ موجود ندارد.
هر فراخوانی یک اتصالِ خصوصیِ :memory: با autocommit=True میسازد. پس از ساختِ جدولها و درجِ وضعیتِ آغازین، ابتدا یک snapshot و سپس snapshot پس از هر نوشتن ثبت میشود. خروجی، tupleهای تودرتوی تغییرناپذیر است و ردیفها با ORDER BY شناسه مرتب میشوند. در finally اتصال بسته میشود؛ حتی خطای خواندنِ خروجی نباید آن را باز بگذارد. این آزمایش، قراردادِ یک تراکنشِ چنددستوریِ همهیاهیچ را ارائه نمیکند.
"""Bounded parent writes: PK replacement versus non-key UPSERT, private memory."""
import itertools
import json
import platform
import sqlite3
import sys
PARENT_SELECT = "SELECT id,revision FROM parent ORDER BY id"
CHILD_SELECT = "SELECT id,parent_id FROM child ORDER BY id"
REPLACE_SQL = "INSERT OR REPLACE INTO parent(id,revision) VALUES(?,?)"
UPSERT_SQL = ("INSERT INTO parent(id,revision) VALUES(?,?) "
"ON CONFLICT(id) DO UPDATE SET revision=excluded.revision")
def _integer(value, lower, upper):
if type(value) is not int:
raise TypeError("exact int required; no bool or coercion")
if not lower <= value <= upper:
raise ValueError("outside the authored finite integer domain")
def _row(value, length):
if type(value) is not tuple:
raise TypeError("exact tuple row required")
if len(value) != length:
raise ValueError("wrong row length")
def _validate(parents, children, writes):
"""Validate the complete request before any connection or SQL operation."""
for value in (parents, children, writes):
if type(value) is not tuple:
raise TypeError("three exact tuple containers required")
if len(parents) > 3 or len(children) > 4 or len(writes) > 4:
raise ValueError("at most three parents, four children and four writes")
parent_ids = set()
for row in parents:
_row(row, 2)
_integer(row[0], 0, 2)
_integer(row[1], 0, 9)
if row[0] in parent_ids:
raise ValueError("duplicate initial parent id")
parent_ids.add(row[0])
child_ids = set()
for row in children:
_row(row, 2)
_integer(row[0], 0, 3)
_integer(row[1], 0, 2)
if row[0] in child_ids:
raise ValueError("duplicate initial child id")
if row[1] not in parent_ids:
raise ValueError("initial child must reference a present parent")
child_ids.add(row[0])
for row in writes:
_row(row, 3)
if type(row[0]) is not str:
raise TypeError("exact str mode required")
if row[0] not in ("replace", "upsert"):
raise ValueError("only the two authored fixed write modes")
_integer(row[1], 0, 2)
_integer(row[2], 0, 9)
def _snapshot(connection):
return (tuple(connection.execute(PARENT_SELECT).fetchall()),
tuple(connection.execute(CHILD_SELECT).fetchall()))
def apply_parent_writes(parents, children, writes):
"""Return initial and after-each-write immutable relational snapshots.
IDs and revision tags are synthetic integers. No file, existing connection,
arbitrary SQL, key-changing update or second uniqueness target is accepted.
"""
_validate(parents, children, writes)
connection = sqlite3.connect(":memory:", autocommit=True)
try:
connection.execute("PRAGMA foreign_keys=ON")
if connection.execute("PRAGMA foreign_keys").fetchone() != (1,):
raise RuntimeError("foreign-key enforcement unavailable")
connection.execute("CREATE TABLE parent(id INTEGER PRIMARY KEY NOT NULL,"
"revision INTEGER NOT NULL)")
connection.execute("CREATE TABLE child(id INTEGER PRIMARY KEY NOT NULL,"
"parent_id INTEGER NOT NULL REFERENCES parent(id) "
"ON DELETE CASCADE)")
connection.executemany("INSERT INTO parent VALUES(?,?)", parents)
connection.executemany("INSERT INTO child VALUES(?,?)", children)
snapshots = [_snapshot(connection)]
for mode, parent_id, revision in writes:
statement = REPLACE_SQL if mode == "replace" else UPSERT_SQL
connection.execute(statement, (parent_id, revision))
snapshots.append(_snapshot(connection))
return tuple(snapshots)
finally:
connection.close()
def row_reference(parents, children, writes):
"""Valid-input oracle: direct tuple relations, no SQL or production helpers."""
parent_rows = tuple(sorted(parents))
child_rows = tuple(sorted(children))
result = [(parent_rows, child_rows)]
for mode, key, revision in writes:
existed = any(row[0] == key for row in parent_rows)
if mode == "replace" and existed:
child_rows = tuple(row for row in child_rows if row[1] != key)
remaining = [row for row in parent_rows if row[0] != key]
remaining.append((key, revision))
parent_rows = tuple(sorted(remaining))
result.append((parent_rows, child_rows))
return tuple(result)
def _rejects(expected, call):
try:
call()
except expected:
return True
return False
def invalid_input_probes():
class IntSubclass(int):
pass
class TupleSubclass(tuple):
pass
class StrSubclass(str):
pass
baseline = (((0, 2),), ((0, 0),), (("upsert", 0, 3),))
consumed = []
def lazy():
consumed.append(True)
yield ("upsert", 0, 1)
bad_types = []
for position in range(3):
for value in (None, [], TupleSubclass()):
item = list(baseline)
item[position] = value
bad_types.append(tuple(item))
bad_types += [(baseline[0], baseline[1], lazy()),
(([0, 2],), (), ()),
(((0, IntSubclass(2)),), (), ()),
(((True, 2),), (), ()),
(((0, 2),), ([0, 0],), ()),
(((0, 2),), ((0, False),), ()),
(((0, 2),), (), (["upsert", 0, 2],)),
(((0, 2),), (), ((StrSubclass("upsert"), 0, 2),)),
(((0, 2),), (), (("upsert", 0, 2.0),)),
(((0, 2),), (), ((1, 0, 2),)),
(((0, 2),), (), (("upsert", 0, 2), ("replace", 0, True)))]
bad_values = [(((0,),), (), ()), (((-1, 2),), (), ()),
(((3, 2),), (), ()), (((0, -1),), (), ()),
(((0, 10),), (), ()), (((0, 2), (0, 3)), (), ()),
(((0, 2),), ((0, 1),), ()),
(((0, 2),), ((0, 0), (0, 0)), ()),
(((0, 2),), ((4, 0),), ()),
(((0, 2),), ((0, 0, 0),), ()),
(((0, 2),), (), ((),)),
(((0, 2),), (), (("UPDATE", 0, 2),)),
(((0, 2),), (), (("replace", 3, 2),)),
(((0, 2),), (), (("upsert", 0, 10),)),
(((0, 2),), (), (("upsert", 0, 2, 1),)),
(((0, 2),), (), (("upsert", 0, 2),) * 5),
(((0, 2),) * 4, (), ()),
(((0, 2),), ((0, 0),) * 5, ()),
((), ((0, 0),), ())]
real_connect = sqlite3.connect
connection_attempts = []
def forbidden_connect(*args, **kwargs):
connection_attempts.append((args, kwargs))
raise AssertionError("connection before whole request validation")
try:
sqlite3.connect = forbidden_connect
types_ok = all(_rejects(TypeError, lambda item=item: apply_parent_writes(*item))
for item in bad_types)
values_ok = all(_rejects(ValueError, lambda item=item: apply_parent_writes(*item))
for item in bad_values)
finally:
sqlite3.connect = real_connect
return {"types_ok": types_ok, "values_ok": values_ok,
"type_profiles": len(bad_types), "value_profiles": len(bad_values),
"connection_attempts": len(connection_attempts),
"lazy_source_consumed": bool(consumed),
"late_invalid_write_prevents_all_SQL":
types_ok and connection_attempts == [] and consumed == []}
def cleanup_probes():
real_connect = sqlite3.connect
closed = []
connections = []
fail_snapshot = [False]
disabled_fk = [False]
sentinel = RuntimeError("expected injected snapshot read failure")
class OneRow:
def fetchone(self):
return (0,)
class TrackedConnection:
def __init__(self, connection):
self.connection = connection
def execute(self, statement, parameters=()):
if fail_snapshot[0] and statement == CHILD_SELECT:
raise sentinel
if disabled_fk[0] and statement == "PRAGMA foreign_keys":
return OneRow()
return self.connection.execute(statement, parameters)
def executemany(self, statement, parameters):
return self.connection.executemany(statement, parameters)
def close(self):
self.connection.close()
closed.append(True)
def tracked_connect(*args, **kwargs):
assert args == (":memory:",) and kwargs == {"autocommit": True}
wrapped = TrackedConnection(real_connect(*args, **kwargs))
connections.append(wrapped)
return wrapped
try:
sqlite3.connect = tracked_connect
apply_parent_writes((), (), ())
fail_snapshot[0] = True
caught = None
try:
apply_parent_writes(((0, 1),), ((0, 0),), ())
except RuntimeError as error:
caught = error
fail_snapshot[0] = False
disabled_fk[0] = True
fk_refused = _rejects(RuntimeError, lambda: apply_parent_writes((), (), ()))
finally:
sqlite3.connect = real_connect
return {"connections_created": len(connections),
"connections_closed": len(closed),
"expected_injected_error_identity": caught is sentinel,
"disabled_FK_readback_refused": fk_refused,
"all_three_paths_closed": closed == [True, True, True]}
def run_example():
checks = {}
def check(name, condition):
assert type(condition) is bool and name not in checks
checks[name] = condition
if not condition:
raise AssertionError(name)
parents = ((0, 2), (1, 4))
children = ((0, 0), (1, 0), (2, 1))
replacement = apply_parent_writes(parents, children, (("replace", 0, 7),))
update = apply_parent_writes(parents, children, (("upsert", 0, 7),))
final_parents = ((0, 7), (1, 4))
check("manual_equal_parent_rows_different_child_survival",
replacement[-1] == (final_parents, ((2, 1),)) and
update[-1] == (final_parents, children))
same = apply_parent_writes(parents, children, (("replace", 0, 2),))
check("same_revision_REPLACE_still_deletes_dependents",
same[-1] == (parents, ((2, 1),)))
new_replace = apply_parent_writes(parents, children, (("replace", 2, 9),))
new_upsert = apply_parent_writes(parents, children, (("upsert", 2, 9),))
check("absent_parent_insert_modes_agree_without_child_creation",
new_replace == new_upsert and
new_replace[-1] == (parents + ((2, 9),), children))
separate = apply_parent_writes(((0, 0), (2, 9)), ((0, 0), (3, 2)),
(("replace", 0, 1),))
check("unrelated_parent_and_maximum_child_id_preserved",
separate[-1] == (((0, 1), (2, 9)), ((3, 2),)))
childless = apply_parent_writes(parents, ((3, 1),), (("replace", 0, 8),))
check("childless_existing_parent_can_be_replaced_without_other_losses",
childless[-1] == (((0, 8), (1, 4)), ((3, 1),)))
check("empty_write_list_returns_exact_initial_state",
apply_parent_writes(parents, children, ()) == ((parents, children),) and
apply_parent_writes((), (), ()) == (((), ()),))
history = apply_parent_writes(parents, children,
(("upsert", 0, 5), ("replace", 0, 6),
("upsert", 0, 7), ("replace", 1, 8)))
check("history_snapshots_show_loss_and_no_automatic_resurrection",
history[1][1] == children and history[2][1] == ((2, 1),) and
history[3][1] == ((2, 1),) and history[4][1] == ())
canonical = all(apply_parent_writes(tuple(p), tuple(c), (("upsert", 0, 7),)) == update
for p in itertools.permutations(parents)
for c in itertools.permutations(children))
check("initial_relation_order_does_not_change_canonical_output", canonical)
boundaries = apply_parent_writes(((2, 9), (1, 0), (0, 9)),
((3, 2), (2, 1), (1, 0), (0, 2)),
(("upsert", 2, 0), ("replace", 1, 9)))
check("all_three_parent_ids_four_children_and_revision_endpoints",
boundaries[-1] == (((0, 9), (1, 9), (2, 0)),
((0, 2), (1, 0), (3, 2))))
invalid = invalid_input_probes()
check("strict_type_shape_domain_uniqueness_and_initial_FK_rejection",
invalid["types_ok"] and invalid["values_ok"])
check("whole_late_validation_no_SQL_or_generator_consumption",
invalid["late_invalid_write_prevents_all_SQL"] and
invalid["connection_attempts"] == 0 and not invalid["lazy_source_consumed"])
cleanup = cleanup_probes()
check("finally_closes_success_snapshot_error_and_FK_refusal",
cleanup["all_three_paths_closed"] and
cleanup["expected_injected_error_identity"] and cleanup["disabled_FK_readback_refused"])
first = apply_parent_writes(parents, children, (("upsert", 0, 7),))
second = apply_parent_writes(parents, children, (("upsert", 0, 7),))
# Item assignment, rather than calling a nonexistent tuple method.
def assign_snapshot():
first[-1][0][0][1] = 0
immutable = _rejects(TypeError, assign_snapshot)
check("fresh_private_calls_and_deeply_immutable_snapshots",
first == second == update and immutable and parents == ((0, 2), (1, 4)) and
children == ((0, 0), (1, 0), (2, 1)))
alphabet = (("replace", 0, 0), ("upsert", 0, 1),
("replace", 1, 1), ("upsert", 1, 0),
("replace", 2, 2), ("upsert", 2, 3))
profiles = observed_snapshots = initial_relations = 0
for presence in range(4):
initial_parents = tuple((key, key + 1) for key in range(2) if presence >> key & 1)
choices = (None,) + tuple(row[0] for row in initial_parents)
for assignments in itertools.product(choices, repeat=2):
initial_children = tuple((child_id, key) for child_id, key in enumerate(assignments)
if key is not None)
initial_relations += 1
for length in range(4):
for writes in itertools.product(alphabet, repeat=length):
actual = apply_parent_writes(initial_parents, initial_children, writes)
expected = row_reference(initial_parents, initial_children, writes)
if actual != expected:
raise AssertionError((initial_parents, initial_children, writes, actual, expected))
profiles += 1
observed_snapshots += len(actual)
check("finite_pure_row_reference_family_ONE_group",
initial_relations > 0 and profiles > 0 and observed_snapshots >= profiles)
assert len(checks) == 14 and all(type(value) is bool and value for value in checks.values())
return {"runtime": {"python": platform.python_version(),
"implementation": platform.python_implementation(),
"sqlite": sqlite3.sqlite_version},
"contract": "private :memory:, PK-only conflict, explicit FK ON/readback, fixed SQL, no key-changing update",
"units": {"id": "synthetic integer identifier", "revision": "unitless synthetic version tag"},
"limits": {"parents": 3, "children": 4, "writes": 4,
"parent_id": [0, 2], "child_id": [0, 3], "revision": [0, 9]},
"manual_REPLACE": replacement, "manual_UPSERT": update,
"same_revision_REPLACE": same, "history": history,
"invalid_inputs": invalid, "cleanup": cleanup,
"finite_family_ONE_group": {"initial_relations": initial_relations,
"profiles": profiles, "initial_and_after_write_snapshots": observed_snapshots,
"alphabet_size": len(alphabet), "maximum_word_length": 3,
"counted_as_check_groups": 1},
"groups_total": len(checks), "checks": checks,
"limitations": ["No files, network, hardware measurement, benchmark, concurrency or durability trial.",
"No nullable UNIQUE, second unique target, user triggers or change-counter claims.",
"FK relation does not authorize deletion or establish a retention policy.",
"The finite family is evidence for this authored domain, not a proof of SQLite behavior everywhere."]}
if __name__ == "__main__":
result = run_example()
sys.stdout.buffer.write((json.dumps(result, ensure_ascii=True, indent=2) + "\n").encode("utf-8"))
مرجعِ row_reference هیچ SQL اجرا نمیکند. رابطهٔ والد و فرزند را در tupleهای جدا نگه میدارد؛ در جایگزینیِ یک والدِ موجود، فرزندانِ دارای همان شناسهٔ والد را کنار میگذارد و سپس زوجِ نسخه را در فهرستِ والدها عوض میکند. مسیرِ مورد انتظار، از پرسوجوی پایگاه داده، تابعِ snapshot یا اعتبارسنجِ مسیرِ اصلی کمک نمیگیرد. این استقلالِ پیادهسازی، فرضِ معناییِ مشترک را به اثباتِ عمومی تبدیل نمیکند.
۴. کدام ردیفها پس از هر گام باقی میمانند؟

در خروجیِ دستی، هر دو مسیر والدهای صفر با نسخهٔ هفت و یک با نسخهٔ چهار را دارند. اما REPLACE فقط فرزندِ دو را نگه میدارد؛ UPSERT هر سه فرزندِ اولیه را حفظ میکند. شاهدِ جدا، نسخهٔ دو را دوباره روی والدِ صفر مینویسد: برابر بودنِ مقدارِ ورودی با مقدارِ فعلی، REPLACE را به بهروزرسانیِ بیاثر تبدیل نمیکند و فرزندانِ وابسته همچنان حذف میشوند.
ترتیبِ رویدادها نیز در snapshotها دیده میشود. ابتدا UPSERT نسخه را تغییر میدهد و فرزندان میمانند؛ REPLACE بعدی آنها را حذف میکند؛ UPSERT پس از آن، فرزندِ حذفشده را بازسازی نمیکند. برای والدِ تازهای که قبلاً نبوده، هر دو دستور درجِ یکسانی دارند و خودشان فرزند نمیسازند. این تفاوتِ «درجِ تازه» و «حلِ تعارضِ ردیفِ موجود» بخشِ مهمِ قرارداد است.
{
"runtime": {
"python": "3.14.5",
"implementation": "CPython",
"sqlite": "3.50.4"
},
"contract": "private :memory:, PK-only conflict, explicit FK ON/readback, fixed SQL, no key-changing update",
"units": {
"id": "synthetic integer identifier",
"revision": "unitless synthetic version tag"
},
"limits": {
"parents": 3,
"children": 4,
"writes": 4,
"parent_id": [
0,
2
],
"child_id": [
0,
3
],
"revision": [
0,
9
]
},
"manual_REPLACE": [
[
[
[
0,
2
],
[
1,
4
]
],
[
[
0,
0
],
[
1,
0
],
[
2,
1
]
]
],
[
[
[
0,
7
],
[
1,
4
]
],
[
[
2,
1
]
]
]
],
"manual_UPSERT": [
[
[
[
0,
2
],
[
1,
4
]
],
[
[
0,
0
],
[
1,
0
],
[
2,
1
]
]
],
[
[
[
0,
7
],
[
1,
4
]
],
[
[
0,
0
],
[
1,
0
],
[
2,
1
]
]
]
],
"same_revision_REPLACE": [
[
[
[
0,
2
],
[
1,
4
]
],
[
[
0,
0
],
[
1,
0
],
[
2,
1
]
]
],
[
[
[
0,
2
],
[
1,
4
]
],
[
[
2,
1
]
]
]
],
"history": [
[
[
[
0,
2
],
[
1,
4
]
],
[
[
0,
0
],
[
1,
0
],
[
2,
1
]
]
],
[
[
[
0,
5
],
[
1,
4
]
],
[
[
0,
0
],
[
1,
0
],
[
2,
1
]
]
],
[
[
[
0,
6
],
[
1,
4
]
],
[
[
2,
1
]
]
],
[
[
[
0,
7
],
[
1,
4
]
],
[
[
2,
1
]
]
],
[
[
[
0,
7
],
[
1,
8
]
],
[]
]
],
"invalid_inputs": {
"types_ok": true,
"values_ok": true,
"type_profiles": 20,
"value_profiles": 19,
"connection_attempts": 0,
"lazy_source_consumed": false,
"late_invalid_write_prevents_all_SQL": true
},
"cleanup": {
"connections_created": 3,
"connections_closed": 3,
"expected_injected_error_identity": true,
"disabled_FK_readback_refused": true,
"all_three_paths_closed": true
},
"finite_family_ONE_group": {
"initial_relations": 18,
"profiles": 4662,
"initial_and_after_write_snapshots": 17730,
"alphabet_size": 6,
"maximum_word_length": 3,
"counted_as_check_groups": 1
},
"groups_total": 14,
"checks": {
"manual_equal_parent_rows_different_child_survival": true,
"same_revision_REPLACE_still_deletes_dependents": true,
"absent_parent_insert_modes_agree_without_child_creation": true,
"unrelated_parent_and_maximum_child_id_preserved": true,
"childless_existing_parent_can_be_replaced_without_other_losses": true,
"empty_write_list_returns_exact_initial_state": true,
"history_snapshots_show_loss_and_no_automatic_resurrection": true,
"initial_relation_order_does_not_change_canonical_output": true,
"all_three_parent_ids_four_children_and_revision_endpoints": true,
"strict_type_shape_domain_uniqueness_and_initial_FK_rejection": true,
"whole_late_validation_no_SQL_or_generator_consumption": true,
"finally_closes_success_snapshot_error_and_FK_refusal": true,
"fresh_private_calls_and_deeply_immutable_snapshots": true,
"finite_pure_row_reference_family_ONE_group": true
},
"limitations": [
"No files, network, hardware measurement, benchmark, concurrency or durability trial.",
"No nullable UNIQUE, second unique target, user triggers or change-counter claims.",
"FK relation does not authorize deletion or establish a retention policy.",
"The finite family is evidence for this authored domain, not a proof of SQLite behavior everywhere."
]
}
چهارده گروهِ نامدار، همین شاهدهای دستی، مرزهای ورودی، ترتیبِ ردیفها، بستنِ اتصال و خانوادهٔ متناهیِ مرجع را در بر میگیرند. تمامِ حالتهای خانواده، یک گروهاند و تعدادِ واقعیِ درخواستها و snapshotها در خروجی محاسبه میشود. دو خطای عمدیِ محلی، شکستِ خواندنِ snapshot و نبودِ تأییدِ فعالبودنِ کلید خارجی را میسنجند؛ اینها خرابیِ مشاهدهشدهٔ سرویس یا شکستِ اجرای کاملِ برنامه نیستند.
درسِ حذفِ وابسته و قیدِ معوق، کنشهای DELETE و زمانِ خطا را مقایسه میکند. مسئلهٔ اینجا متفاوت است: دستوری با ظاهرِ نوشتنِ والد، میتواند حذفِ ضمنی ایجاد کند. در آزمونِ ثبتِ داده و بستهٔ تکراری نیز هویتِ مشاهده و سیاستِ بازپخش مطرح بود؛ اینجا برچسبِ نسخه عمداً قابلتغییر است. این دو پیوند از نسخههای منتشرشدهٔ ذخیرهشده خوانده شدهاند؛ دسترسیِ زندهٔ امروزِ آنها تأیید نشده است.
سه سندِ فنی در ۲۲ اکتبر ۲۰۲۶ خوانده شدند و همگی از یک ناشرِ رسمیاند؛ سه تأییدِ مستقلِ بیرونی نیستند. تاریخِ بازنگریِ صفحهها از تاریخِ انتشارِ اولیه، که معلوم نیست، جدا ثبت شده است. معرفیِ UPSERT در نسخهٔ 3.24.0 در سالِ ۲۰۱۸ نیز تاریخِ ویژگی است. شمارهٔ واقعیِ Python و SQLite را باید از runtime همین خروجی خواند، نه از تاریخِ دسترسی یا آخرین نسخهٔ وبسایت.
۵. چهار پرسش با پاسخِ کامل

پرسش اول: اگر ردیفِ نهاییِ والد در دو مسیر یکسان باشد، نگهداریِ داده هم یکسان بوده است؟ پاسخ: خیر. در شاهدِ دستی، زوجهای والد برابرند اما دو فرزند فقط در مسیرِ UPSERT باقی میمانند. باید وابستهها و وضعیتِ بینِ گامها نیز جزوِ خروجیِ مورد انتظار باشند.
پرسش دوم: نوشتنِ دوبارهٔ همان نسخه با REPLACE بیاثر است؟ پاسخ: برای وابستهها خیر؛ تعارضِ کلید میتواند همان حذف و درج را انجام دهد. برابر بودنِ نسخه، شرطِ ویژهای برای حفظِ فرزندان در دستورِ ما ایجاد نمیکند. برای تغییرِ فیلدِ غیرکلیدی، مسیرِ صریحِ بهروزرسانی با این نیاز سازگار است.
پرسش سوم: UPSERT بعدی سابقهٔ حذفشده را برمیگرداند؟ پاسخ: خیر؛ دستور فقط ردیفِ والد را درج یا اصلاح میکند. ساختنِ دوبارهٔ فرزندان نیازمند داده و سیاستِ جداست. موفقیتِ مرجعِ کلید خارجی، مجوزِ حذف یا کافیبودنِ سیاستِ نگهداریِ سابقه را تعیین نمیکند.
پرسش چهارم: این نتیجه برای هر طرحِ واقعی تضمین است؟ پاسخ: خیر. طرحِ ما یک کلیدِ غیرتهی، فیلدِ نسخهٔ غیرکلیدی و CASCADE فعال دارد. چند قیدِ یکتا، trigger کاربری، تغییرِ کلید، همزمانی، فایل، مهاجرت، قطعِ برق و کارایی بررسی نشدهاند. تغییرِ هرکدام از این فرضها به قرارداد و آزمونِ جدا نیاز دارد؛ این نمونه هیچ اندازهگیریِ فیزیکی یا سامانهٔ تولیدی را تأیید نمیکند.
منابع و مطالعهٔ بیشتر
- SQLite؛ قرارداد کامل ON CONFLICT و REPLACE
- SQLite؛ قرارداد کامل UPSERT، excluded و محدودیتها
- SQLite؛ فعالسازی کلید خارجی و کنشهای وابسته
بررسی منابع: ۳۰ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

