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

۱. قرارداد رکورد: نوع، واحد و دامنه سه تصمیم جدا هستند

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

رکورد ما سه مؤلفه دارد: شمارهٔ مثبت seq، زمان نامنفی time_us و ولتاژ صحیح voltage_mv. واحد زمان میکروثانیه نسبت به مبدأ ساختگی آزمایش است؛ ساعت تقویمی یا زمان دریافت شبکه نیست. ولتاژ با واحد میلی‌ولت و در بازهٔ بستهٔ منفی ۵۰۰۰ تا مثبت ۵۰۰۰ تعریف می‌شود. این دامنه انتخاب آموزشی نویسنده است و از مشخصات یک مبدل یا دستگاه واقعی استخراج نشده است.

جدول برای هر مؤلفه INTEGER و NOT NULL دارد. شرط CHECK شمارهٔ مثبت، زمان نامنفی و دامنهٔ ولتاژ را اعمال می‌کند؛ UNIQUE نیز تکرار شماره را رد می‌کند. این قیود جای همدیگر را نمی‌گیرند. مثلاً صحیح بودن نوع، مثبت بودن شماره را تضمین نمی‌کند و یکتایی شماره به معنی صعودی بودن زمان‌ها نیست. در این طرح، شماره کلید اصلی خودکار نیست؛ عمداً مقدار آن را درخواست می‌کنیم و با UNIQUE از تکرارش جلوگیری می‌کنیم.

سه رکورد نهایی عبارت‌اند از (1,0,1010)، (2,1000,-900) و (3,2000,0). بنابراین فاصلهٔ زمانی انتخاب‌شده یک میلی‌ثانیه است، اما برنامه نرخ نمونه‌برداری واقعی را اندازه نمی‌گیرد. پیش از ذخیره، می‌توان قرارداد رکورد را مطابق آموزش اعتبارسنجی دادهٔ حسگر با dataclass بررسی کرد؛ جدول همچنان مسئول قرارداد ذخیره‌سازی خودش باقی می‌ماند.

۲. STRICT تبدیل بدون اتلاف را حذف نمی‌کند

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

طبق مستندات رسمی جدول‌های STRICT، SQLite برای ستون دارای نوع مشخص، تبدیل متعارف به آن نوع را امتحان می‌کند؛ اگر تبدیل بدون اتلاف ممکن نباشد، خطا می‌دهد. پس STRICT به معنای ممنوعیت همهٔ تبدیل‌ها نیست. قابلیت یادشده از SQLite نسخهٔ ۳٫۳۷٫۰ وجود دارد؛ این تاریخ معرفی قابلیت است، نه تاریخ تولید این آزمایش.

در مسیر مستقیم SQL، رشتهٔ عددی '1200' و عدد اعشاری 1200.0 هر دو در ستون ولتاژ به صحیح ۱۲۰۰ تبدیل می‌شوند. این تبدیل مقدار عددی این دو ورودی مشخص را حفظ می‌کند؛ نوع اولیهٔ پایتون را حفظ نمی‌کند. در مقابل، 1200.5 در همان ستون رد می‌شود. از این چند مورد نمی‌توان قاعده‌ای برای پذیرش تمام رشته‌ها یا همهٔ مقادیر اعشاری ساخت؛ مقصود، دیدن نمونه‌های کنترل‌شدهٔ تبدیل است.

آزمایش مستقیم دیگری True را می‌فرستد و مقدار صحیح ۱ ذخیره می‌شود. راهنمای نوع‌های SQLite توضیح می‌دهد که نوع ذخیره‌سازی مستقلی برای بولی وجود ندارد. بنابراین خواندن عدد ۱ از این ستون ثابت نمی‌کند فرستنده دقیقاً عدد صحیح ۱ داده بوده است. اگر منشأ نوع ورودی اهمیت دارد، باید پیش از تبدیل بررسی یا در طرح مستقلی ثبت شود؛ مقدار نهایی به تنهایی سابقهٔ تبدیل را بازسازی نمی‌کند.

۳. کد کامل: نوع دقیق در پایتون و قیود مستقل در SQL

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

تابع insert_exact فقط ورودی‌هایی را می‌پذیرد که نوعشان دقیقاً int باشد. این انتخاب آگاهانه، بولی، رشته، اعشاری و NaN را پیش از ارسال به SQL رد می‌کند. سپس همهٔ مؤلفه‌ها باید در دامنهٔ صحیح علامت‌دار ۶۴ بیتی جا شوند؛ اعداد صحیح پایتون می‌توانند از این دامنه بزرگ‌تر باشند. کنترل دامنهٔ کاربردی، مانند ولتاژ مجاز، بر عهدهٔ قیود جدول می‌ماند. هیچ عدد اعشاری در این قرارداد به میلی‌ولت گرد نمی‌شود.

مقادیر با جای‌نگهدارهای ? به دستور INSERT داده می‌شوند؛ رشتهٔ SQL از داده ساخته نمی‌شود. جزئیات اتصال، اتصال پارامترها و استثناها در مستندات sqlite3 پایتون آمده است. اجرای ثبت‌شده Python 3.14.5 و هستهٔ SQLite 3.50.4 دارد؛ شمارهٔ مستندات برخط هنگام مطالعه 3.14.8 بوده و نباید آن را نسخهٔ اجرای کد معرفی کرد.

برای جدا کردن قواعد، جدول کوچکی فقط با CHECK می‌سازیم و NULL در آن وارد می‌کنیم. این مقدار پذیرفته می‌شود، زیرا نتیجهٔ NULL در عبارت CHECK نقض آن محسوب نمی‌شود؛ NOT NULL باید مستقل اضافه شود. تعریف رسمی CHECK و NOT NULL همین تفکیک را مشخص می‌کند. سپس تهی بودن تک‌تک فیلدهای جدول اصلی، دامنه‌های نامعتبر و دو انتهای مجاز ولتاژ بررسی می‌شوند.

"""SQLite STRICT coercion versus exact Python int contracts; synthetic local records."""
import json
import platform
from pathlib import Path
import sqlite3
import tempfile

SCHEMA = '''CREATE TABLE reading(
    seq INTEGER NOT NULL UNIQUE CHECK(seq > 0),
    time_us INTEGER NOT NULL CHECK(time_us >= 0),
    voltage_mv INTEGER NOT NULL CHECK(voltage_mv BETWEEN -5000 AND 5000)
) STRICT'''
INSERT = 'INSERT INTO reading(seq,time_us,voltage_mv) VALUES(?,?,?)'
FIXTURE = ((1, 0, 1010), (2, 1000, -900), (3, 2000, 0))


def connect(path):
    if sqlite3.sqlite_version_info < (3, 37, 0):
        raise RuntimeError('SQLite3.37+ required for STRICT')
    return sqlite3.connect(path, autocommit=True)


def insert_exact(con, seq, time_us, voltage_mv):
    values = (seq, time_us, voltage_mv)
    if any(type(value) is not int for value in values):
        raise TypeError('Exact int fields required; bool, float and strings are excluded')
    if any(not -(2**63) <= value <= 2**63 - 1 for value in values):
        raise ValueError('Fields must fit a signed64bit SQLite integer')
    con.execute(INSERT, values)


def rows(con):
    return con.execute('SELECT seq,time_us,voltage_mv FROM reading ORDER BY seq').fetchall()


def rejects(call, exception):
    try:
        call()
    except exception:
        return True
    return False


def run_example():
    checks = {}
    with tempfile.TemporaryDirectory(prefix='strict-contract-') as folder:
        path = Path(folder) / 'synthetic.db'
        con = connect(path)
        try:
            con.execute(SCHEMA)
            strict_flag = con.execute("PRAGMA table_list('reading')").fetchone()[5]
            con.execute('BEGIN')
            con.execute(INSERT, (90, 0, '1200'))
            con.execute(INSERT, (91, 0, 1200.0))
            con.execute(INSERT, (92, 0, True))
            coercions = con.execute(
                'SELECT seq,voltage_mv,typeof(voltage_mv) FROM reading ORDER BY seq').fetchall()
            fractional_rejected = rejects(lambda: con.execute(INSERT, (93, 0, 1200.5)), sqlite3.IntegrityError)
            checks['strict_table_flag_enabled'] = strict_flag == 1
            checks['lossless_numeric_text_stored_as_integer'] = coercions[0] == (90, 1200, 'integer')
            checks['lossless_integral_float_stored_as_integer'] = coercions[1] == (91, 1200, 'integer')
            checks['python_bool_binding_has_integer_storage'] = coercions[2] == (92, 1, 'integer')
            checks['fractional_real_rejected_by_strict_integer'] = fractional_rejected
            checks['failed_statement_does_not_rollback_prior_statements'] = con.in_transaction and len(rows(con)) == 3
            con.execute('ROLLBACK')
            checks['explicit_rollback_clears_probe_transaction'] = rows(con) == [] and not con.in_transaction
            con.execute('CREATE TABLE check_only(v INTEGER CHECK(v BETWEEN -5000 AND 5000)) STRICT')
            con.execute('INSERT INTO check_only VALUES(NULL)')
            checks['check_alone_does_not_reject_null'] = con.execute('SELECT v FROM check_only').fetchone() == (None,)
            checks['not_null_guards_each_required_field'] = all(rejects(
                lambda values=values: con.execute(INSERT, values), sqlite3.IntegrityError)
                for values in ((None, 0, 1000), (4, None, 1000), (4, 0, None)))
            checks['database_domain_checks_reject_invalid_values'] = all(rejects(
                lambda values=values: con.execute(INSERT, values), sqlite3.IntegrityError)
                for values in ((0, 0, 1000), (-1, 0, 1000), (4, -1, 1000), (4, 0, -5001), (4, 0, 5001)))
            checks['python_api_rejects_original_type_mismatch'] = all(rejects(
                lambda values=values: insert_exact(con, *values), TypeError)
                for values in ((True, 0, 1000), (4, False, 1000), (4, 0, True),
                               (4, 0, '1200'), (4, 0, 1200.0), (4, 0, float('nan'))))
            checks['oversized_python_integer_rejected_before_binding'] = rejects(
                lambda: insert_exact(con, 4, 0, 10**1000), ValueError)
            con.execute('BEGIN')
            insert_exact(con, 80, 0, -5000)
            insert_exact(con, 81, 0, 5000)
            checks['both_inclusive_voltage_boundaries_accepted'] = rows(con) == [(80, 0, -5000), (81, 0, 5000)]
            con.execute('ROLLBACK')
            con.execute('BEGIN')
            for values in FIXTURE:
                insert_exact(con, *values)
            con.execute('COMMIT')
            baseline = rows(con)
            types = con.execute('SELECT typeof(seq),typeof(time_us),typeof(voltage_mv) FROM reading ORDER BY seq').fetchall()
            checks['valid_integer_fixture_committed_exactly'] = tuple(baseline) == FIXTURE and not con.in_transaction
            checks['unique_sequence_rejected_without_altering_rows'] = rejects(
                lambda: insert_exact(con, 1, 3000, 10), sqlite3.IntegrityError) and rows(con) == baseline
            checks['all_stored_fixture_fields_have_integer_storage'] = types == [('integer', 'integer', 'integer')] * 3
        finally:
            con.close()
        con = connect(path)
        try:
            checks['close_and_reopen_preserve_fixture'] = rows(con) == baseline
        finally:
            con.close()
    assert all(checks.values()), checks
    return {'python': platform.python_version(), 'sqlite_runtime': sqlite3.sqlite_version,
            'table_is_strict': bool(strict_flag), 'coercion_probe_rows': coercions,
            'accepted_fixture_rows': baseline, 'stored_fixture_types': types,
            'units': {'seq': 'positivecount', 'time_us': 'syntheticmicroseconds', 'voltage_mv': 'integermillivolts'},
            'authored_voltage_interval_mv': [-5000, 5000],
            'temporary_local_file_only': True, 'network_or_physical_sensor_used': False,
            'checks': checks}


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

۴. خروجی واقعی چه چیزی را ثابت می‌کند؟

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

هر ۱۷ بررسی نام‌دار در این اجرا موفق بوده است. خروجی تبدیل‌ها سه ردیف با شماره‌های ۹۰، ۹۱ و ۹۲ نشان می‌دهد؛ ولتاژهایشان به ترتیب ۱۲۰۰، ۱۲۰۰ و ۱ هستند و نوع هر سه integer است. تابع typeof در SQLite نوع عبارت فعلی را گزارش می‌کند؛ در این پرس‌وجو نوع مقدار ذخیره‌شده را می‌بینیم، نه کلاس شیء اولیهٔ پایتون یا صحت فیزیکی ولتاژ را.

در بررسی جانبی تراکنش، شکست درج اعشاری کسری سه درج قبلی را خودکار حذف نمی‌کند؛ تراکنش باز می‌ماند و ROLLBACK صریح آن را خالی می‌کند. این مشاهده با رفتار پیش‌فرض ABORT در مستند حل تعارض SQLite سازگار است. اتصال نمونه با autocommit=True ساخته شده؛ بنابراین BEGIN، COMMIT و ROLLBACK را به صورت دستور SQL اجرا می‌کنیم و به متدهای هم‌نام اتصال در این حالت تکیه نمی‌کنیم.

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

{
  "python": "3.14.5",
  "sqlite_runtime": "3.50.4",
  "table_is_strict": true,
  "coercion_probe_rows": [
    [
      90,
      1200,
      "integer"
    ],
    [
      91,
      1200,
      "integer"
    ],
    [
      92,
      1,
      "integer"
    ]
  ],
  "accepted_fixture_rows": [
    [
      1,
      0,
      1010
    ],
    [
      2,
      1000,
      -900
    ],
    [
      3,
      2000,
      0
    ]
  ],
  "stored_fixture_types": [
    [
      "integer",
      "integer",
      "integer"
    ],
    [
      "integer",
      "integer",
      "integer"
    ],
    [
      "integer",
      "integer",
      "integer"
    ]
  ],
  "units": {
    "seq": "positivecount",
    "time_us": "syntheticmicroseconds",
    "voltage_mv": "integermillivolts"
  },
  "authored_voltage_interval_mv": [
    -5000,
    5000
  ],
  "temporary_local_file_only": true,
  "network_or_physical_sensor_used": false,
  "checks": {
    "strict_table_flag_enabled": true,
    "lossless_numeric_text_stored_as_integer": true,
    "lossless_integral_float_stored_as_integer": true,
    "python_bool_binding_has_integer_storage": true,
    "fractional_real_rejected_by_strict_integer": true,
    "failed_statement_does_not_rollback_prior_statements": true,
    "explicit_rollback_clears_probe_transaction": true,
    "check_alone_does_not_reject_null": true,
    "not_null_guards_each_required_field": true,
    "database_domain_checks_reject_invalid_values": true,
    "python_api_rejects_original_type_mismatch": true,
    "oversized_python_integer_rejected_before_binding": true,
    "both_inclusive_voltage_boundaries_accepted": true,
    "valid_integer_fixture_committed_exactly": true,
    "unique_sequence_rejected_without_altering_rows": true,
    "all_stored_fixture_fields_have_integer_storage": true,
    "close_and_reopen_preserve_fixture": true
  }
}

۵. چهار تمرین پاسخ‌دار و مرز نتیجه‌گیری

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

تمرین اول: آیا رشتهٔ '1200' باید در هر دو مسیر پذیرفته شود؟ پاسخ: خیر. SQL مستقیم در این ستون تبدیل بدون اتلاف را می‌پذیرد، ولی تابع ورودی ما قرارداد دقیق int دارد و TypeError می‌دهد. تفاوت نتیجه از دو قرارداد متفاوت می‌آید، نه تناقض موتور پایگاه داده.

تمرین دوم: چرا CHECK دامنه برای جلوگیری از مقدار تهی کافی نیست؟ پاسخ: عبارت مقایسه با NULL می‌تواند NULL بدهد و CHECK آن را نقض حساب نمی‌کند. در طرح اصلی NOT NULL هر سه مؤلفه را الزامی کرده است؛ آزمون مستقل هر مؤلفه این تصمیم را بررسی می‌کند.

تمرین سوم: آیا integer بودن خروجی ثابت می‌کند ورودی بولی نبوده است؟ پاسخ: خیر؛ آزمایش True نمونهٔ نقض است. برای الزام منشأ int از ورودی دقیق پایتون استفاده کنید. برای ممیزی واقعی، ثبت نوع اولیه و منشأ داده سیاست دیگری می‌خواهد و این نمونه چنین سامانه‌ای ندارد.

تمرین چهارم: اگر درج چهارم شکست بخورد، آیا همهٔ درج‌های پیشین حتماً برمی‌گردند؟ پاسخ: در بررسی حاضر خیر؛ باید بازگشت صریح کل گروه را درخواست کرد. این پاسخ را به همهٔ خطاها و تنظیمات تعارض تعمیم ندهید. برای ادامهٔ مسیر داده، مرزبندی پیام حسگر روی جریان TCP را بخوانید؛ بازیابی پیام نیز به تنهایی نوع، واحد، کالیبراسیون یا اصالت داده را تضمین نمی‌کند.

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

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