Digisky

یک لایهٔ آداپتور، سی موتور پایگاه‌داده، ۶۴۷۱ خط

وقتی یک پایگاه کد واحد باید از PostgreSQL، SQL Server، Oracle، MySQL، ClickHouse، Snowflake، BigQuery، MongoDB، Cassandra و Elasticsearch پرس‌وجو کند، واقعاً چه چیزی میانشان فرق می‌کند: محصورسازی شناسه، محدودسازی سطر، حساب تاریخ، ترتیب NULL، تبدیل نوع، مهلت زمانی و تحمیل فقط‌خواندنی.

· Digisky
آداپتور پایگاه‌دادهگویش‌های SQLPostgreSQLOracleClickHouseElasticsearchCassandra

چرا این نوشته

شرحی ملموس از هفت محور رفتاری که یک لایهٔ آداپتور پایگاه‌داده را روی ۳۰ موتور به ۶٬۴۷۱ خط می‌رساند — با جزئیات هر موتور که در مستندات هیچ فروشنده‌ای یک‌جا نیست، از جمله اینکه کدام موتورها مهلت زمانی سمت سرور ندارند، کدام‌ها اصلاً تراکنش فقط‌خواندنی ندارند، و چرا COUNT DISTINCT در Elasticsearch عددی تقریبی برمی‌گرداند که دقیق به نظر می‌رسد.

لایهٔ آداپتور دیتاکوپایلوت ۶٬۴۷۱ خط روی ۳۰ موتور پایگاه‌دادهٔ ثبت‌شده است — ۲۵ موتور دست‌نویس به‌علاوهٔ ۵ موتور تولیدشده از خانوادهٔ PostgreSQL — و بزرگ‌ترین تک‌فایل بک‌اند است.

اولین واکنش هر کسی که این عدد را می‌شنود این است که «خب، ترجمهٔ گویش که این‌قدر کار ندارد». درست هم می‌گوید: ساختن SQL معتبر برای گویشی دیگر بخش کوچکی از ماجراست. حجم اصلی در هفت جایی است که استاندارد SQL ساکت است، یا هر فروشنده آن را در جهتی متفاوت نادیده گرفته — جاهایی که متن یکسانِ یک کوئری، بسته به اینکه به کجا نشانه رفته، سطرهای متفاوت، نوع‌های متفاوت، یا اصلاً هیچ امکان لغوی تولید می‌کند.

این نوشته همان هفت جا را، موتور به موتور، شرح می‌دهد.

خط‌ها واقعاً کجا خرج می‌شوند

توزیع، آن چیزی نیست که آدم حدس می‌زند. ساخت کوئری — همان بخشی که شبیه خودِ کار به نظر می‌رسد — اقلیت کد است.

دغدغهچرا به کدِ مخصوص هر موتور نیاز دارد
اتصال و احراز هویتهر درایور دستور زبان DSN، داستان TLS و رفتار استخر اتصال خودش را دارد
درون‌نگری اسکیماinformation_schema هست، ناقص است، و روی چند موتور اصلاً نیست
محصورسازی شناسه و تاخوردگی حروفچهار کاراکتر محصورکنندهٔ متفاوت، سه قاعدهٔ تاخوردگی متفاوت
محدودسازی سطر و صفحه‌بندیپنج نحو، که یکی‌شان وجود ORDER BY را الزامی می‌کند
حساب تاریخ و بازههیچ دو موتوری بر سر «هفت روز پیش» توافق ندارند
تبدیل نوع در مرز درایورجایی که اشکال‌های واقعی تولید آنجاست
مهلت زمانی، لغو و تحمیل فقط‌خواندنیجایی که قطعی‌های واقعی سرویس آنجاست

قراردادی که هر آداپتور پیاده می‌کند کوچک است. کوچک نگه‌داشتن همین قرارداد است که نگه‌داری سی‌تای آن‌ها را ممکن می‌کند:

# adapters/base.py — the contract. Python 3.11+, typing.Protocol.
from typing import Protocol, Any, Sequence
from datetime import timedelta


class Adapter(Protocol):
    name: str
    supports_read_only_transaction: bool
    supports_server_side_timeout: bool
    supports_wide_integers: bool
    default_null_ordering_asc: str          # "first" | "last"

    def quote_identifier(self, ident: str) -> str: ...
    def apply_limit(self, sql: str, n: int, *, has_order_by: bool) -> str: ...
    def interval_ago(self, delta: timedelta) -> str: ...
    def date_trunc(self, unit: str, expr: str) -> str: ...
    def order_by_nulls(self, expr: str, *, asc: bool, nulls: str) -> str: ...
    def begin_read_only(self, cur: Any, timeout_s: int) -> None: ...
    def normalise_row(self, row: Sequence[Any]) -> list[Any]: ...

    # Probes the conformance suite runs against a live engine.
    def wide_integer_probe(self) -> str: ...
    def sleep_probe(self, seconds: int) -> str: ...

نُه متد و چهار پرچم قابلیت. هر تصمیم مخصوص موتور که در ادامه می‌آید، پشت یکی از این‌ها زندگی می‌کند.

محصورسازی شناسه و تاخوردگی حروف

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

موتورمحصورکنندهشناسهٔ بدون محصور به چه تا می‌خوردتله
PostgreSQL"x"حروف کوچکجدولی که به شکل "MyTable" ساخته شده، با MyTable در دسترس نیست
Oracle"x"حروف بزرگمحصورکردن یک نام کوچک، کوئری‌ای را که بدون محصور کار می‌کرد می‌شکند
Snowflake"x"حروف بزرگمثل Oracle؛ دیر گرفته می‌شود چون درون‌نگری هم حروف بزرگ برمی‌گرداند
Db2"x"حروف بزرگمانند بالا
SQL Server[x] یا "x"حفظ می‌شود؛ مقایسه طبق collation"x" فقط با QUOTED_IDENTIFIER ON کار می‌کند
MySQL / MariaDB`x`حفظ می‌شودحساسیت نام جدول به lower_case_table_names و فایل‌سیستم میزبان بستگی دارد
BigQuery`x`حفظ می‌شودنام دیتاست و جدول به بزرگی و کوچکی حروف حساس است
ClickHouse`x` یا "x"حفظ می‌شودسرتاسر به حروف حساس است
DuckDB"x"حفظ می‌شود، مقایسه بدون حساسیتبا وجود نحو مشابه، رفت‌وبرگشتش با PostgreSQL فرق دارد

قاعده‌ای که از این جدول بیرون می‌آید: همیشه محصور کنید، و همیشه با همان شناسه‌ای که درون‌نگری برگردانده. هرگز در آداپتور بزرگی و کوچکی حروف را نرمال نکنید. تاخوردگی به حروف بزرگ در Oracle و رفتار وابسته به فایل‌سیستم در MySQL را هیچ نرمال‌سازی‌ای که انتخاب کنید نمی‌تواند هم‌زمان راضی کند.

محدودسازی سطر

پنج نحو، که یکی‌شان به‌جای اضافه‌کردن به کوئری، معنای آن را عوض می‌کند.

موتورشکلیادداشت
PostgreSQL، MySQL، ClickHouse، DuckDB، Redshift، CockroachDB، SQLiteLIMIT n OFFSET mحالت آسان
SQL Server 2012 به بعدOFFSET m ROWS FETCH NEXT n ROWS ONLYوجود ORDER BY را الزامی می‌کند؛ وقتی کوئری ندارد، ORDER BY (SELECT NULL) بسازید
SQL Server، نسخه‌های قدیمی‌ترSELECT TOP (n)بدون offset
Oracle 12c به بعد، Db2FETCH FIRST n ROWS ONLYSQL استاندارد، که دیر رسید
Oracle، پیش از 12cWHERE ROWNUM <= nپیش از ORDER BY اعمال می‌شود — باید در زیرکوئری پیچیده شود وگرنه n سطر نادرست برمی‌گرداند
Cassandra (CQL)LIMIT nصفحه‌بندی عمیق از توکن مبهم paging-state استفاده می‌کند، نه از offset
Elasticsearchsizefrom + size با index.max_result_window روی 10,000 سقف می‌خورد
MongoDB.limit(n)متد مکان‌نما، نه متن کوئری

دو سطر از این جدول به‌جای خطا، پاسخ نادرستِ خاموش تولید می‌کنند. ROWNUM در Oracle پیش از مرتب‌سازی تخصیص می‌یابد، پس SELECT ... WHERE ROWNUM <= 10 ORDER BY amount DESC ده سطر دلخواه مرتب‌شده برمی‌گرداند، نه ده سطر برتر — اجرا می‌شود، داده برمی‌گرداند، و نادرست است. و پنجرهٔ ۱۰٬۰۰۰ نتیجه‌ای Elasticsearch یعنی صفحه‌بندی ساده‌لوحانه با from/size وسط یک مجموعهٔ بزرگ از تولید نتیجه بازمی‌ماند؛ جایگزین مستندشده search_after است، که به یک فیلد شکنندهٔ تساوی نیاز دارد وگرنه سندها را جا می‌اندازد و تکرار می‌کند.

حساب تاریخ

شکل مشترکی برای «هفت روز پیش» وجود ندارد. همین بخش است که آدم‌ها را تا وقتی امتحان نکرده‌اند متقاعد نگه می‌دارد که یک مدل زبانی می‌تواند SQL قابل حمل بنویسد.

موتورهفت روز پیشبرش تا ابتدای ماه
PostgreSQLnow() - interval '7 days'date_trunc('month', ts)
MySQLDATE_SUB(NOW(), INTERVAL 7 DAY)DATE_FORMAT(ts, '%Y-%m-01')
SQL ServerDATEADD(day, -7, SYSUTCDATETIME())DATETRUNC(month, ts) (2022 به بعد)، وگرنه DATEFROMPARTS(...)
OracleSYSTIMESTAMP - INTERVAL '7' DAYTRUNC(ts, 'MM')
ClickHousesubtractDays(now(), 7)toStartOfMonth(ts)
BigQueryTIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)TIMESTAMP_TRUNC(ts, MONTH)
SnowflakeDATEADD(day, -7, CURRENT_TIMESTAMP())DATE_TRUNC('MONTH', ts)

زیر این نحو، یک واگرایی معنایی هست که گران‌تر تمام می‌شود. نوع DATE در Oracle مؤلفهٔ زمان هم دارد، پس TRUNC روی ستونی که کاربر آن را «تاریخ» می‌داند بی‌اثر نیست. PostgreSQL میان timestamp و timestamptz فرق می‌گذارد و فقط دومی در تبدیل منطقهٔ زمانی شرکت می‌کند. نوع TIMESTAMP در MySQL هنگام خواندن طبق منطقهٔ زمانی نشست تبدیل می‌شود ولی DATETIME نمی‌شود — پس دو ستون در یک جدول می‌توانند بر سر معنای «دیروز» با هم اختلاف داشته باشند. لایهٔ آداپتوری که فقط نحو را نرمال کند، نیمهٔ آسان کار را انجام داده است.

ترتیب NULL

جای پیش‌فرض NULL در ORDER BY موتورها را دو به دو تقسیم می‌کند، و دو تای آن‌ها اصلاً نمی‌شود خلافش را به آن‌ها گفت. همین تفاوت است که باعث می‌شود «یک کوئری واحد» روی موتور دیگر ده‌تای برتر متفاوتی برگرداند، بدون هیچ خطایی در هیچ‌جا.

موتورپیش‌فرض ASCپیش‌فرض DESCپشتیبانی از NULLS FIRST/LAST
PostgreSQLNULL در آخرNULL در اولدارد
OracleNULL در آخرNULL در اولدارد
MySQLNULL در اولNULL در آخرندارد
SQL ServerNULL در اولNULL در آخرندارد

PostgreSQL و Oracle با NULL مثل مقداری بزرگ‌تر از هر مقدار رفتار می‌کنند — مستندات مرتب‌سازی PostgreSQL می‌گوید NULLS FIRST پیش‌فرض DESC است و در بقیهٔ حالت‌ها NULLS LAST — در حالی که MySQL و SQL Server آن را کوچک‌تر می‌گیرند. روی آن دو موتوری که این نحو را ندارند، آداپتور به‌جایش یک کلید مرتب‌سازی تولید می‌کند:

-- MySQL / SQL Server: force NULLS LAST on an ascending sort.
ORDER BY CASE WHEN amount IS NULL THEN 1 ELSE 0 END, amount ASC

این عبارت درست است و در عین حال یک تصمیم کارایی هم هست — می‌تواند مانع شود که یک نمایه مرتب‌سازی را برآورده کند. و این شکل کلی کل این لایه است: صورت قابل حمل وجود دارد، و چیزی خرج می‌کند که صورت غیرقابل حمل خرج نمی‌کرد.

تبدیل نوع در مرز درایور

اشکال‌های واقعی تولید اینجا هستند. کوئری درست است، سطرها درست‌اند، و مقداری که به کاربر می‌رسد نادرست است.

اعداد صحیح بزرگ‌تر از ۲۵۳. نوع UInt64 در ClickHouse، NUMBER(38,0) در Snowflake و NUMBER در Oracle همگی می‌توانند مقادیری نگه دارند که جاوااسکریپت نمی‌تواند دقیق نمایششان دهد. آن‌ها را به‌صورت عدد به JSON سریالایز کنید و مرورگر بی‌صدا گردشان می‌کند. شناسه‌ای که به ...992 ختم می‌شود ...990 می‌شود، هیچ خطایی هیچ‌جا بلند نمی‌شود، و کاربر گزارش می‌دهد که یک رکورد پیدا نمی‌شود. راه‌حل این است که اعداد صحیح بزرگ را از آداپتور به بیرون به‌صورت رشته حمل کنید، با تصمیمی که به ازای هر نوع ستون در زمان درون‌نگری گرفته می‌شود.

اعشاری‌ها. NUMERIC و DECIMAL به‌صورت Decimal پایتون می‌رسند، که رمزگذار استاندارد JSON از آن سر باز می‌زند. تبدیل به float برای اینکه خطا برود، روی مقادیر پولی بی‌صدا دقت را از دست می‌دهد. به‌صورت رشته سریالایز کنید و قالب‌بندی را به لایهٔ نمایش بسپارید.

تاریخ‌زمان بی‌منطقه در برابر منطقه‌دار. timestamptz در PostgreSQL تاریخ‌زمان منطقه‌دار می‌دهد و timestamp بی‌منطقه. DATE در Oracle تاریخ‌زمان بی‌منطقه‌ای می‌دهد که مؤلفهٔ زمان دارد. نوع قدیمی datetime در SQL Server به گام‌های تقریباً ۳٫۳۳ میلی‌ثانیه گرد می‌شود. آمیختن این‌ها در یک مجموعه نتیجه، مقایسه‌هایی می‌سازد که به اندازهٔ ساعت‌ها نادرست‌اند.

مقادیری که معادل پایتونی ندارند. MySQL تاریخ '0000-00-00' را مجاز می‌داند، که با datetime.date نمایش‌پذیر نیست. ClickHouse بولین‌ها را به‌صورت UInt8 برمی‌گرداند. هر آداپتور به یک normalise_row نیاز دارد که دربارهٔ این‌ها صریح باشد، نه به پیش‌فرض درایوری که بین نسخه‌ها فرق می‌کند.

مهلت زمانی، لغو و تحمیل فقط‌خواندنی

پرچم‌های قابلیت در قرارداد آداپتور به این دلیل وجود دارند که دو موتور پرکاربرد نمی‌توانند کاری را بکنند که آن بیست‌وهشت تای دیگر می‌کنند، و هر دو شکاف به ایمنی مربوط‌اند.

موتورمهلت زمانی دستور در سمت سرورتراکنش فقط‌خواندنی
PostgreSQLSET LOCAL statement_timeout = '30s'BEGIN READ ONLY
MySQLراهنمای MAX_EXECUTION_TIME(30000)فقط SELECTSTART TRANSACTION READ ONLY
Oracleفقط از راه Resource Manager (دستورهای CANCEL_SQL)SET TRANSACTION READ ONLY
SQL Serverندارد. مهلت کوئری یک مفهوم سمت کلاینت استندارد. تراکنش فقط‌خواندنی وجود ندارد
ClickHouseتنظیم max_execution_timeتنظیم پروفایل readonly=1
SnowflakeSTATEMENT_TIMEOUT_IN_SECONDSندارد — از نقشی با دسترسی فقط SELECT استفاده کنید
BigQueryمهلت جاب؛ و maximum_bytes_billed که محدودیت مهم‌تر استندارد — از IAM استفاده کنید (نقش viewer، بدون DML)

سطر SQL Server همان است که باید در ذهن بماند. مستندات خود مایکروسافت صریح می‌گوید: مهلت کوئری یک مفهوم سمت کلاینت است، و تنظیم remote query timeout فقط بر کوئری‌هایی اعمال می‌شود که خود موتور به بیرون صادر می‌کند. روی SQL Server، مهلت آداپتور یعنی CommandTimeout درایور به‌علاوهٔ یک لغو صادرشده از سمت کلاینت — که یعنی یک پارگی شبکه میان اپلیکیشن و پایگاه‌داده، کوئری را روی سرور در حال اجرا رها می‌کند بدون آنکه چیزی متوقفش کند.

BigQuery ناهنجارِ دیگر است، در جهت مخالف. کوئری از کنترل خارج‌شده آنجا مسئلهٔ مدت نیست، مسئلهٔ صورت‌حساب است. maximum_bytes_billed کنترلی است که اهمیت دارد، و آداپتوری که فقط مهلت زمانی را پیاده کرده، از منبع نادرستی محافظت کرده است.

اصل کلی در همهٔ این‌ها: پرچم تراکنش یک راحتی است، مجوز اعطایی همان کنترل است. دیتاکوپایلوت هر کوئری را در تراکنشی فقط‌خواندنی و زمان‌دار اجرا می‌کند، هر جا که موتور چنین چیزی داشته باشد؛ اما خاصیتی که واقعاً برقرار می‌ماند این است که اعتبارنامهٔ آن اتصال نمی‌تواند بنویسد. سه موتور از جدول بالا اصلاً تراکنش فقط‌خواندنی ندارند، و روی آن‌ها تنها چیزی که میان یک کوئری تولیدشده و یک جدول تغییریافته ایستاده، همان مجوز است.

سه موتوری که SQL نیستند

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

MongoDB خط لولهٔ تجمیع دارد. $lookup به معنای رابطه‌ای join نیست، اسکیمایی برای درون‌نگری وجود ندارد — به‌جایش از مجموعه‌ها نمونه‌برداری می‌شود — و یک فیلد واحد می‌تواند در سندهای مختلف نوع‌های متفاوت داشته باشد. آداپتور یک اسکیمای نمونه‌برداری‌شده بالا می‌آورد و صریح می‌گوید که نمونه‌برداری‌شده است.

Cassandra به CQL حرف می‌زند، که آن‌قدر شبیه SQL هست که خطرناک باشد. join ندارد. شرط WHERE که کلید پارتیشن را در بر نگیرد یا شکست می‌خورد یا به ALLOW FILTERING نیاز دارد، که کوئری را به پویش کل خوشه تبدیل می‌کند. تجمیع روی چند پارتیشن به آن شکلی که یک تحلیل‌گر انتظار دارد پشتیبانی نمی‌شود. آداپتوری که SQL تولیدشدهٔ دلخواه را برای Cassandra بپذیرد، آداپتوری است که سرانجام یک خوشهٔ تولید را از کار می‌اندازد.

Elasticsearch ظریف‌ترین تلهٔ کل این لایه را دارد. تجمیع cardinality — نگاشت طبیعی برای COUNT(DISTINCT x)تقریبی است و روی یک طرح‌وارهٔ HyperLogLog++ پیاده شده که خطایش با تعداد مقادیر متمایز بالاتر از precision_threshold رشد می‌کند. یک عدد صحیح برمی‌گرداند. دقیق به نظر می‌رسد. نیست، و هیچ خطا، هشدار یا تمایز نوعی این را به فراخوان نمی‌گوید. تجمیع‌های terms هم به همین شکل یک کران خطای مستندشده گزارش می‌کنند که بیشتر کلاینت‌ها دورش می‌ریزند. آداپتوری که COUNT(DISTINCT ...) را روی این نگاشت کند بدون آنکه تقریبی‌بودن را منتشر کند، دارد دقت کاذب تولید می‌کند — و رفتار درست این است که نتیجه را تا خودِ رابط کاربری «تقریبی» برچسب بزند.

چه چیزی تولید می‌شود و چه چیزی دست‌نویس است

از آن ۳۰ آداپتور ثبت‌شده، ۲۵ تا اعلام‌شده و ۵ تا از خانوادهٔ PostgreSQL تولید می‌شوند. تولید خودکار فقط تحت یک شرط امن است: رفتار موتور مشتق‌شده باید تفریقی از پایه باشد — همان پروتکل سیم، همان محصورسازی، همان تاخوردگی، همان نحو محدودسازی، با قابلیت‌هایی که کم شده‌اند نه عوض.

این برای موتورهای سازگار با PostgreSQL برقرار است و جای دیگری برقرار نیست. هر چیزی که یک قاعده را عوض کند به‌جای اینکه قابلیتی را بردارد، آداپتور دست‌نویس می‌گیرد؛ و هر آداپتور تولیدشده هم فهرست بازنویسی‌های خودش را دارد، چون «سازگار با PostgreSQL» پیش از آنکه ادعایی فنی باشد ادعایی بازاریابی است. صرفه‌جویی تولید خودکار واقعی اما متوسط است؛ ارزش اصلی این است که پنج موتور روی آن ۸۰٪ مشترک نمی‌توانند از هم فاصله بگیرند.

مجموعهٔ آزمون انطباق: یک قرارداد، سی پیاده‌سازی

سی آداپتور فقط وقتی صادق می‌مانند که یک مجموعهٔ آزمون روی همه‌شان اجرا شود. نه سی فایل آزمون — یک مجموعهٔ پارامتری که قرارداد را مشخصات می‌داند و هر آداپتور را پیاده‌سازیِ تحت آزمون.

# tests/test_adapter_conformance.py — pytest 8, run against live engines
# in CI containers where possible and against recorded fixtures otherwise.
import pytest
from datetime import timedelta
from app.connections.adapters import ADAPTERS      # the registry: 30 entries

ALL = pytest.mark.parametrize("adapter", ADAPTERS.values(), ids=lambda a: a.name)


@ALL
def test_quoting_round_trips_reserved_words(adapter, live):
    ident = "select"                    # a reserved word as a column name
    quoted = adapter.quote_identifier(ident)
    rows = live(adapter, f"SELECT 1 AS {quoted}")
    assert list(rows[0].keys())[0].lower() == ident


@ALL
def test_limit_is_applied_after_ordering(adapter, live):
    """Oracle's ROWNUM applies before ORDER BY. If apply_limit does not wrap,
    this returns arbitrary rows and the bug is invisible in every other engine."""
    sql = adapter.apply_limit(
        "SELECT n FROM conformance_numbers ORDER BY n DESC",
        n=3, has_order_by=True,
    )
    assert [r["n"] for r in live(adapter, sql)] == [100, 99, 98]


@ALL
def test_nulls_last_is_enforced_regardless_of_engine_default(adapter, live):
    expr = adapter.order_by_nulls("amount", asc=True, nulls="last")
    rows = live(adapter, f"SELECT amount FROM conformance_nulls ORDER BY {expr}")
    assert rows[-1]["amount"] is None


@ALL
def test_wide_integers_survive_as_strings(adapter, live):
    """9007199254740993 = 2**53 + 1. Any float path corrupts it."""
    if not adapter.supports_wide_integers:
        pytest.skip("engine has no 64-bit integer type")
    rows = live(adapter, adapter.wide_integer_probe())
    assert rows[0]["v"] == "9007199254740993"


@ALL
def test_write_is_refused(adapter, live):
    with pytest.raises(Exception):
        live(adapter, "CREATE TABLE conformance_should_not_exist (x int)")


@ALL
def test_timeout_capability_is_declared_truthfully(adapter, live):
    if not adapter.supports_server_side_timeout:
        pytest.skip("client-side cancellation only — see SQL Server")
    with pytest.raises(TimeoutError):
        live(adapter, adapter.sleep_probe(seconds=5), timeout_s=1)

آخرین آزمون همان است که ارزش کپی‌کردن دارد. ادعا نمی‌کند که مهلت زمانی کار می‌کند — ادعا می‌کند که پرچم قابلیت راست می‌گوید. آداپتوری که دربارهٔ توانایی‌اش دروغ بگوید بدتر از آداپتوری است که آن توانایی را ندارد، چون لایهٔ بالاتر بر مبنای همان پرچم تصمیم ایمنی می‌گیرد. این همان انضباطی است که ۱۷٬۱۳۷ تابع آزمون بک‌اند در این پایگاه کد را ساخته: آزمون‌ها قرارداد را رمزگذاری می‌کنند، پس موتور تازه با سبزکردن مجموعهٔ موجود اضافه می‌شود، نه با نوشتن آزمون‌های تازه‌ای که با کد تازه هم‌نظرند.

جایی که این لایه دیگر کار نمی‌کند

این لایه برای کوئری‌های تحلیلی فقط‌خواندنی که در زمان اجرا تولید می‌شوند و روی موتورهایی اجرا می‌شوند که اپراتور وصلشان کرده ساخته شده. چهار مرز دارد.

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

قابلیت حمل نوشتن را دنبال نمی‌کند. جداسازی تراکنش، نحو upsert، بندهای returning و معنای قفل‌گذاری به‌مراتب بیشتر از مسیر خواندن واگرا می‌شوند، و یک لایهٔ نوشتن قابل حمل روی این سی موتور چند برابر بزرگ‌تر می‌شد با حالت‌های شکست به‌مراتب بدتر.

پایگاه‌داده‌ای را که بد مدل شده قابل پرس‌وجو نمی‌کند. آداپتور تضمین می‌کند کوئری برای موتور مقصد معتبر و ایمن کران‌دار است. اینکه کوئریِ درستی باشد، مسئلهٔ اسکیما و مستندسازی است.

و جزئیات بالا برای نسخه‌های موتوری که تا ۲۷ اوت ۲۰۲۶ در استفاده‌اند به‌روز است. DATETRUNC در SQL Server 2022 آمد؛ FETCH FIRST در Oracle 12c آمد؛ ClickHouse نام توابع را بین انتشارها عوض می‌کند. هر جدول این نوشته، عکسی لحظه‌ای از هدفی متحرک است — و همین دلیل واقعی این است که لایه‌ای مثل این به مجموعهٔ آزمون انطباق نیاز دارد، نه به مستندات.