فهرست مقاله
دو نامِ Sensor-A و sensor-a در دفترِ حسگرها ثبت شدهاند. آیا دو دستگاه داریم یا دو شیوهٔ نوشتنِ یک نام؟ پایگاه داده از روی این متن، هویتِ فیزیکیِ دستگاه را کشف نمیکند. برنامه باید قراردادِ برابری را انتخاب کند و بعد نشان دهد ذخیره و جستوجو همان قرارداد را اجرا میکنند. در این درس، با نامهای ساختگی و پایگاهِ حافظهای، محدودیتِ NOCASE داخلیِ SQLite را میبینیم و کلیدِ صریحِ casefold را آزمایش میکنیم؛ هیچ حسگر، فایلِ موجود یا شبکهای در اجرای مثال حضور ندارد.
۱. نامِ نمایشی و هویتِ متنی یک تصمیم نیستند

نامِ نمایشی همان متنی است که دریافت کردهایم؛ مثلاً بزرگیِ حرفِ نخست یا املایِ آلمانیِ Straße را نگه میدارد. کلیدِ هویت، نمایشِ دیگری است که فقط برای قراردادِ مقایسهٔ این دفتر تولید میکنیم. در سیاستِ انتخابشده، نامهایی با خروجیِ یکسانِ casefold یک کلید دارند. این تصمیمِ آموزشی به معنیِ اثباتِ یکسانبودنِ دو دستگاه، مالک یا محلِ نصب نیست؛ اگر شناسهٔ سختافزاری لازم داریم، باید فیلد و قراردادِ دیگری تعریف کنیم.
برای نمونه، کلیدِ Sensor-A برابر sensor-a و کلیدِ Straße برابر strasse است. پس ثبتِ STRASSE بعد از Straße در این دفتر، تکرار محسوب میشود. نامِ نخست را نگه میداریم و درخواستِ دوم را با نتیجهٔ False گزارش میکنیم. نام را بیصدا بازنویسی یا جایگزین نمیکنیم؛ تصمیم دربارهٔ تغییرِ نامِ نمایشی از ثبتِ تکراری جداست. فاصلهٔ انتهایی نیز در این سیاست حفظ میشود، بنابراین Sensor-A و Sensor-A با یک فاصله در انتها دو کلید دارند.
پیش از مقایسه، ورودی باید دقیقاً str باشد، یک تا شصتوچهار نقطهٔ کد داشته باشد، NUL نداشته باشد و با UTF-8 سختگیرانه رمزگذاری شود. اینها مرزهایِ ساختهشده برای مثالاند. تعدادِ نقطهٔ کد، تعدادِ بایت یا شمارِ نویسههایِ ادراکی نیست. برای تفکیکِ نوعِ اولیه از مقدارِ ذخیرهشده، درسِ STRICT و قراردادِ نوعِ ورودی زمینهٔ مناسبی دارد؛ معتبر بودنِ نوع بهتنهایی سیاستِ برابریِ نام را تعیین نمیکند.
۲. NOCASE چه چیزی را برابر میگیرد؟

طبقِ تعریفِ رسمیِ توابعِ مقایسهٔ SQLite، NOCASE داخلی فقط بیستوشش حرفِ بزرگِ ASCII را به همتایِ کوچکشان برمیگرداند. بنابراین جفتِ Sensor-A و sensor-a برابر میشود، اما جفتِ Straße و STRASSE برابر نمیشود. آزمایشهایِ É و é، و شکلهایِ سیگمایِ یونانی نیز محدودیتِ مقایسهٔ داخلی را در نمونههایِ مشخص نشان میدهند. این نتیجه دربارهٔ تابعِ داخلی است؛ در مثال، افزونه یا تابعِ مقایسهٔ سفارشی ثبت نکردهایم.
همان مستند مشخص میکند که ستون میتواند تابعِ مقایسهٔ پیشفرض داشته باشد و COLLATE صریح در عبارت، آن را تغییر دهد. در جدولِ کوچکِ آزمایشی، شرطِ label=? از NOCASE ستون استفاده میکند؛ شرطِ label COLLATE BINARY=? همان جفت را برابر نمیگیرد. پس اتصالِ پارامتر و انتخابِ مقایسه دو موضوعِ جدا هستند. درسِ مقدارهایِ متصل و فیلترِ دقیقِ نام، قراردادِ BINARY را برای تطبیقِ دقیق انتخاب کرده بود؛ اینجا سیاست را آشکارا تغییر میدهیم.
در مقابل، مستنداتِ casefold پایتون این روش را برای تطبیقِ بدونِ تمایزِ بزرگیِ حروف معرفی میکند؛ گسترشِ ß به ss نمونهٔ روشنِ تفاوت با کوچککردنِ ساده است. بااینحال، مثالِ ما یکسانسازیِ شکلهایِ ترکیبی، حذفِ فاصله یا قواعدِ مخصوصِ زبان اضافه نمیکند. شکلِ پیشساختهٔ é و ترکیبِ e با علامتِ تکیه در فیکسچر، کلیدهایِ متفاوت دارند. شباهتِ ظاهریِ کافِ فارسی و عربی نیز در این سیاست به معنیِ برابری نیست.
۳. کلیدِ صریح، یکتایی و مرزِ نویسندهٔ داده

جدول دو ستون دارد: label برای نمایشِ اصلی و identity_key برای کلیدِ محاسبهشده. کلید، TEXT با COLLATE BINARY و قیودِ UNIQUE و NOT NULL است. طبقِ تعریفِ رسمیِ یکتایی و مقدارِ الزامی، UNIQUE جایِ NOT NULL را نمیگیرد؛ مقدارهایِ NULL در قیدِ یکتایی از هم متمایزند. ما برای هر دو ستون مقدارِ غیرتهی میخواهیم و برای کلید، یکتایی را جدا اعمال میکنیم. ستونِ نمایشیِ این طرح قیدِ یکتایی ندارد.
تابعِ register کلید را در پایتون میسازد و هر دو مقدار را با جاینگهدار میفرستد؛ این شیوه در راهنمایِ اتصالِ مقدارهایِ sqlite3 توضیح داده شده است. فقط خطایِ مشخصِ یکتایی به نتیجهٔ تکرار تبدیل میشود؛ خطاهایِ دیگر پنهان نمیشوند. جستوجو نیز کلیدِ ورودی را با همان تابع میسازد. این تابعها برای اتصال و طرحِ متعلق به همین مثال تعریف شدهاند، نه برای هر اتصالِ دلخواه.
قیدِ جدول فقط کلیدِ ذخیرهشده را میسنجد. اگر نویسندهٔ دیگری مستقیم label را Sensor-A و کلید را WRONG وارد کند، جدول ارتباطِ این دو را بررسی نمیکند. برنامه عمداً چنین ردیفی میسازد و نشان میدهد جستوجویِ sensor-a آن را پیدا نمیکند. این شاهد، مرزِ مسئولیتِ مسیرِ نوشتن است. برای سامانهای با نویسندگانِ متعدد، هماهنگیِ تولیدِ کلید، بهروزرسانی و مهاجرتِ داده باید جدا طراحی شود؛ اجرایِ حاضر آن طرح را پیاده نکرده است.
۴. کدِ کامل و شاهدِ مستقل برای برابری

فایل را با نامِ sqlite_nocase_unicode_key.py و دستورِ python sqlite_nocase_unicode_key.py اجرا کنید. کتابخانهٔ خارجی لازم نیست؛ استفاده از autocommit=True به Python نسخهٔ ۳٫۱۲ یا جدیدتر نیاز دارد. اجرایِ ثبتشده Python 3.14.5، هستهٔ SQLite 3.50.4 و پایگاهِ یونیکدِ 16.0.0 داشته است. صفحههایِ مستنداتِ شاخهٔ 3.14 هنگامِ مطالعه نسخهٔ 3.14.8 را نمایش میدادند؛ آن شماره، نسخهٔ اجرایِ برنامه نیست.
هر دسته tuple دقیق و دارایِ حداکثر شصتوچهار نامِ درخواستی است؛ اعتبارسنجیِ همهٔ نامها پیش از بازکردنِ پایگاهِ خصوصی انجام میشود. این سقف به run_batch تعلق دارد و فراخوانیهایِ نامحدودِ register را محدود نمیکند. کلیدِ حاصل نیز سقفِ صریحِ ۱۹۲ نقطهٔ کد و ۷۶۸ بایتِ UTF-8 دارد؛ این اعداد سیاستِ مثالاند، نه آزمونِ کارایی یا ادعایِ دفاع در برابرِ مصرفِ منابع. در فیکسچرِ گسترش، شصتوچهار ß به ۱۲۸ نقطهٔ کد تبدیل میشود.
"""Bounded synthetic SQLite label identity; no network, files or real sensors."""
from contextlib import closing
from itertools import product
import json
import platform
import sqlite3
import unicodedata
SCHEMA = """CREATE TABLE sensor(
label TEXT NOT NULL,
identity_key TEXT NOT NULL COLLATE BINARY UNIQUE
)"""
def key_for_name(name):
if type(name) is not str:
raise TypeError("name must be exact str")
if not 1 <= len(name) <= 64 or "\x00" in name:
raise ValueError("name must contain 1..64 codepoints without NUL")
try:
name.encode("utf-8", errors="strict")
except UnicodeEncodeError as error:
raise ValueError("name must encode as strict UTF-8") from error
key = name.casefold()
if len(key) > 192 or len(key.encode("utf-8")) > 768:
raise ValueError("folded key exceeds this example's explicit cap")
return key
def database():
con = sqlite3.connect(":memory:", autocommit=True)
try:
con.execute(SCHEMA)
except BaseException:
con.close()
raise
return con
def register(con, name):
# This helper is for our own schema. SQL checks stored-key uniqueness;
# this Python write path is responsible for deriving a matching key.
key = key_for_name(name)
try:
con.execute("INSERT INTO sensor(label,identity_key) VALUES(?,?)", (name, key))
except sqlite3.IntegrityError as error:
if error.sqlite_errorcode != sqlite3.SQLITE_CONSTRAINT_UNIQUE:
raise
return False
return True
def lookup(con, name):
return tuple(row[0] for row in con.execute(
"SELECT label FROM sensor WHERE identity_key = ? COLLATE BINARY",
(key_for_name(name),)))
def rows(con):
return tuple(con.execute("SELECT label,identity_key FROM sensor ORDER BY rowid"))
def run_batch(names):
if type(names) is not tuple:
raise TypeError("batch must be exact tuple")
if len(names) > 64:
raise ValueError("at most 64 attempted names per batch")
# Validate the complete batch before opening its private memory database.
for name in names:
key_for_name(name)
with closing(database()) as con:
accepted = tuple(register(con, name) for name in names)
return accepted, rows(con)
def ascii_fold_reference(text):
# Independent of str.casefold/lower, SQL, register and key_for_name.
return "".join(chr(ord(ch) + 32) if "A" <= ch <= "Z" else ch for ch in text)
def manual_registry_reference(fixtures):
# Authored identity classes and literal expected keys, no folding or SQL.
seen = set()
accepted, stored = [], []
for label, expected_key, identity_class in fixtures:
new = identity_class not in seen
accepted.append(new)
if new:
stored.append((label, expected_key))
seen.add(identity_class)
return tuple(accepted), tuple(stored)
def rejects(exception, function, *args):
try:
function(*args)
except exception:
return True
return False
def compare(con, left, right, collation):
# Structural choice is a fixed program allowlist, never an input label.
sql = {"BINARY": "SELECT ? = ? COLLATE BINARY",
"NOCASE": "SELECT ? = ? COLLATE NOCASE"}[collation]
return con.execute(sql, (left, right)).fetchone()[0] == 1
def run():
# Keys and class IDs are manually authored expected answers. In particular,
# precomposed and combining forms deliberately have different classes.
fixtures = (
("Sensor-A", "sensor-a", 0), ("sensor-a", "sensor-a", 0),
("Stra\u00dfe", "strasse", 1), ("STRASSE", "strasse", 1),
("\u03a3", "\u03c3", 2), ("\u03c3", "\u03c3", 2),
("\u03c2", "\u03c3", 2),
("\u00c9", "\u00e9", 3), ("\u00e9", "\u00e9", 3),
("E\u0301", "e\u0301", 4), ("e\u0301", "e\u0301", 4),
("I", "i", 5), ("i", "i", 5),
("\u0130", "i\u0307", 6), ("\u0131", "\u0131", 7),
("\u06a9", "\u06a9", 8), ("\u0643", "\u0643", 9),
("Sensor-A ", "sensor-a ", 10),
)
checks = {}
with closing(database()) as con:
checks["binary_and_ascii_nocase_manual_contrast"] = (
not compare(con, "Sensor-A", "sensor-a", "BINARY")
and compare(con, "Sensor-A", "sensor-a", "NOCASE"))
nonascii = (("Stra\u00dfe", "STRASSE"), ("\u00c9", "\u00e9"),
("\u03a3", "\u03c3"), ("\u03c2", "\u03c3"))
checks["builtin_nocase_does_not_fold_nonascii_pairs"] = all(
not compare(con, a, b, "NOCASE") for a, b in nonascii)
alphabet = ("A", "a", "Z", "z", "0", "-", "\u00e9")
words = ("",) + alphabet + tuple("".join(x) for x in product(alphabet, repeat=2))
pair_count = 0
for a, b in product(words, repeat=2):
expected = ascii_fold_reference(a) == ascii_fold_reference(b)
assert compare(con, a, b, "NOCASE") == expected, (a, b)
pair_count += 1
checks["independent_ascii_ordinal_oracle_family"] = pair_count == 3249
con.execute("CREATE TABLE nocase_probe(label TEXT COLLATE NOCASE)")
con.execute("INSERT INTO nocase_probe VALUES(?)", ("Sensor-A",))
inherited = tuple(con.execute("SELECT label FROM nocase_probe WHERE label=?", ("sensor-a",)))
overridden = tuple(con.execute("SELECT label FROM nocase_probe WHERE label COLLATE BINARY=?", ("sensor-a",)))
checks["column_collation_and_explicit_override"] = inherited == (("Sensor-A",),) and overridden == ()
checks["literal_expected_unicode_fold_keys"] = all(key_for_name(label) == key for label, key, _ in fixtures)
matrix_count = 0
for a, _, class_a in fixtures:
for b, _, class_b in fixtures:
assert (key_for_name(a) == key_for_name(b)) == (class_a == class_b), (a, b)
matrix_count += 1
checks["manual_unicode_identity_matrix_family"] = matrix_count == 324
expected = manual_registry_reference(fixtures)
actual = run_batch(tuple(row[0] for row in fixtures))
checks["unique_key_registry_matches_manual_first_wins_oracle"] = actual == expected
with closing(database()) as con:
for name in ("Sensor-A", "Stra\u00dfe", "\u03a3"):
assert register(con, name)
checks["lookup_applies_same_key_policy_and_absence_is_empty"] = (
lookup(con, "SENSOR-A") == ("Sensor-A",)
and lookup(con, "STRASSE") == ("Stra\u00dfe",)
and lookup(con, "\u03c2") == ("\u03a3",)
and lookup(con, "unknown") == ())
before = rows(con)
accepted = register(con, "STRASSE")
checks["collision_preserves_first_display_and_all_rows"] = not accepted and rows(con) == before
# Separate fixed counterexamples: folding is not normalization/trim.
assert register(con, "\u00e9") and register(con, "e\u0301")
assert register(con, "Sensor-A ")
checks["no_normalization_or_whitespace_trimming"] = (
lookup(con, "\u00e9") == ("\u00e9",)
and lookup(con, "e\u0301") == ("e\u0301",)
and lookup(con, "sensor-a ") == ("Sensor-A ",))
with closing(database()) as con:
con.execute("INSERT INTO sensor VALUES(?,?)", ("Sensor-A", "WRONG"))
checks["raw_writer_can_bypass_application_key_consistency"] = (
rows(con) == (("Sensor-A", "WRONG"),) and lookup(con, "sensor-a") == ())
with closing(database()) as con:
assert register(con, "x")
checks["database_not_null_and_duplicate_key_constraints"] = (
rejects(sqlite3.IntegrityError, con.execute, "INSERT INTO sensor VALUES(?,?)", (None, "y"))
and rejects(sqlite3.IntegrityError, con.execute, "INSERT INTO sensor VALUES(?,?)", ("y", None))
and rejects(sqlite3.IntegrityError, con.execute, "INSERT INTO sensor VALUES(?,?)", ("other", "x"))
and rows(con) == (("x", "x"),))
class NameSubclass(str):
pass
checks["exact_string_type_contract"] = all(rejects(TypeError, key_for_name, value)
for value in (None, True, 1, 1.5, b"x", NameSubclass("x")))
checks["codepoint_length_endpoints_and_key_expansion"] = (
key_for_name("x") == "x" and key_for_name("A" * 64) == "a" * 64
and key_for_name("\u00df" * 64) == "ss" * 64
and rejects(ValueError, key_for_name, "")
and rejects(ValueError, key_for_name, "x" * 65))
checks["nul_and_unencodable_surrogates_rejected"] = all(
rejects(ValueError, key_for_name, value) for value in ("a\x00b", "\ud800", "\udfff"))
cap_names = tuple("sensor-" + str(i) for i in range(64))
checks["batch_shape_zero_full_cap_and_validation_before_database"] = (
run_batch(()) == ((), ()) and run_batch(("x",)) == ((True,), (("x", "x"),))
and run_batch(cap_names)[0] == (True,) * 64
and rejects(TypeError, run_batch, ["x"])
and rejects(ValueError, run_batch, cap_names + ("extra",))
and rejects(TypeError, run_batch, ("valid", None)))
main_names = tuple(row[0] for row in fixtures[:4]) + ("\u03a3", "\u03c2")
main_accepted, main_rows = run_batch(main_names)
assert len(checks) == 16 and all(type(v) is bool and v for v in checks.values()), checks
return dict(python=platform.python_version(), sqlite_runtime=sqlite3.sqlite_version,
unicode_database=unicodedata.unidata_version,
main_attempted_names=main_names, main_accepted=main_accepted,
main_stored_display_and_key=main_rows,
independent_ascii_pairs_in_one_group=pair_count,
manual_unicode_pairs_in_one_group=matrix_count,
manual_registry_fixture_names=len(fixtures),
bounds=dict(name_codepoints=[1,64], key_codepoints_max=192,
key_utf8_bytes_max=768, batch_attempts_max=64),
units="Text codepoints and encoded bytes; no electrical measurement units.",
checks=checks,
scope="Finite local equality contract; no normalization, locale collation, confusable detection or real sensor/network/file execution.")
if __name__ == "__main__":
print(json.dumps(run(), ensure_ascii=True, indent=2))
برای مرجعِ ASCII، تبدیلِ مستقل با بازهٔ حرفها و اختلافِ عددیِ نقطهٔ کد نوشتهایم؛ از casefold، lower یا پرسوجویِ دوم کمک نمیگیرد. پنجاهوهفت رشتهٔ کوتاه، ۳۲۴۹ جفت میسازند. برای یونیکد، هجده نام با کلیدِ موردِ انتظار و شمارهٔ کلاسِ دستی داریم؛ ۳۲۴ جفت را با این کلاسها مقایسه میکنیم. سپس مرجعِ ثبتِ دستی، نخستین عضوِ هر کلاس را نگه میدارد و نتیجه با ثبتِ واقعی مقایسه میشود. اینها فیکسچرهایِ محدودند، نه پیادهسازیِ مستقلِ کاملِ الگوریتمِ یونیکد.
هر شانزده گروهِ نامدار در اجرایِ ثبتشده موفق بودهاند؛ تعدادِ جفتها تعدادِ گروهها را زیاد نمیکند. در نمونهٔ اصلی شش درخواست داریم: Sensor-A، sensor-a، Straße، STRASSE، Σ و ς. تصمیمها به ترتیب True، False، True، False، True و False هستند و سه نمایشِ نخستِ پذیرفتهشده حفظ میشوند. آزمونها همچنین نوعِ دقیق، دو انتهایِ طول، ورودیِ نامعتبر، کلیدِ تکراری، NULL و تغییرنکردنِ ردیفها پس از برخورد را بررسی میکنند. واحدهایِ گزارش، نقطهٔ کد و بایتاند؛ کمیتِ الکتریکی اندازهگیری نمیشود.
{
"python": "3.14.5",
"sqlite_runtime": "3.50.4",
"unicode_database": "16.0.0",
"main_attempted_names": [
"Sensor-A",
"sensor-a",
"Stra\u00dfe",
"STRASSE",
"\u03a3",
"\u03c2"
],
"main_accepted": [
true,
false,
true,
false,
true,
false
],
"main_stored_display_and_key": [
[
"Sensor-A",
"sensor-a"
],
[
"Stra\u00dfe",
"strasse"
],
[
"\u03a3",
"\u03c3"
]
],
"independent_ascii_pairs_in_one_group": 3249,
"manual_unicode_pairs_in_one_group": 324,
"manual_registry_fixture_names": 18,
"bounds": {
"name_codepoints": [
1,
64
],
"key_codepoints_max": 192,
"key_utf8_bytes_max": 768,
"batch_attempts_max": 64
},
"units": "Text codepoints and encoded bytes; no electrical measurement units.",
"checks": {
"binary_and_ascii_nocase_manual_contrast": true,
"builtin_nocase_does_not_fold_nonascii_pairs": true,
"independent_ascii_ordinal_oracle_family": true,
"column_collation_and_explicit_override": true,
"literal_expected_unicode_fold_keys": true,
"manual_unicode_identity_matrix_family": true,
"unique_key_registry_matches_manual_first_wins_oracle": true,
"lookup_applies_same_key_policy_and_absence_is_empty": true,
"collision_preserves_first_display_and_all_rows": true,
"no_normalization_or_whitespace_trimming": true,
"raw_writer_can_bypass_application_key_consistency": true,
"database_not_null_and_duplicate_key_constraints": true,
"exact_string_type_contract": true,
"codepoint_length_endpoints_and_key_expansion": true,
"nul_and_unencodable_surrogates_rejected": true,
"batch_shape_zero_full_cap_and_validation_before_database": true
},
"scope": "Finite local equality contract; no normalization, locale collation, confusable detection or real sensor/network/file execution."
}
۵. چهار پرسش دربارهٔ کلیدِ نام

پرسش اول: چرا NOCASE، Sensor-A و sensor-a را برابر میگیرد، اما Straße و STRASSE را نه؟ پاسخ: تابعِ داخلی فقط حروفِ ASCII را تا میکند؛ گسترشِ ß به ss در آن قرارداد نیست. در مسیرِ کلیدِ پایتونی، کلیدِ هر دو نامِ دوم strasse است؛ پس درخواستِ بعدی با قیدِ یکتایی برخورد میکند. دو مسیر عمداً دو سیاستِ متفاوت دارند.
پرسش دوم: آیا بعد از casefold باید é و e همراهِ علامتِ ترکیبی یکی شوند؟ پاسخ: در این مثال خیر؛ کلیدها به ترتیب U+00E9 و دنبالهٔ U+0065، U+0301 باقی میمانند. فیکسچر برایشان دو کلاسِ دستیِ جدا تعیین کرده است. اگر محصول یکسانسازیِ دیگری میخواهد، باید آن سیاست و اثرش بر دادههایِ قبلی را صریح طراحی و دوباره آزمایش کند.
پرسش سوم: آیا UNIQUE ثابت میکند identity_key از label درست ساخته شده است؟ پاسخ: خیر؛ ردیفِ دارایِ کلیدِ WRONG شاهدِ نقض است. قید، تکرارِ همان مقدارِ ذخیرهشده را رد میکند و رابطهٔ محاسباتیِ دو ستون را نمیداند. در قراردادِ فعلی، مسیرِ پایتونی مسئولِ تولیدِ کلید است؛ مجوزِ نوشتن یا هماهنگیِ نویسندگان در کد وجود ندارد.
پرسش چهارم: آیا ۳۲۴۹ مقایسهٔ موفق، همهٔ نامهایِ یونیکد را تأیید میکند؟ پاسخ: خیر؛ آن خانواده فقط رشتههایِ کوتاهِ الفبایِ تعیینشده را پوشش میدهد. هجده نامِ دستی نیز شاهدهایِ انتخابشدهاند. نتیجهٔ قابلِ اتکا برای همین اجرا، سازگاریِ مقایسه، جستوجو و یکتایی در قراردادِ محدود است؛ تشخیصِ شباهتِ دیداری، قواعدِ محلیِ زبان، هویتِ دستگاه و مهاجرتِ کلید میانِ نسخهها به بررسیِ مستقل نیاز دارند.
منابع و مطالعهٔ بیشتر
- SQLite؛ توابعِ مقایسه و انتخابِ COLLATE
- Python3.14؛ قراردادِ str.casefold
- SQLite؛ قیدِ یکتایی و مقدارِ الزامی
- Python3.14؛ sqlite3 و اتصالِ مقدارها
بررسی منابع: ۲۸ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

