SQL گونهدار یعنی گونهی پارامترها و نتیجههای یک پرسمان از طرحوارهی (schema) پایگاه داده بیاید، و ستون یا جدولی که اشتباه نوشته شده پیش از رسیدن پرسمان به پایگاه داده گرفته شود. زبان برنامهنویسی سلام این را در کتابخانهی استانداردش دارد: salam sql check کدهای SQL درون پروندههای .salam و .sql شما را میخواند و با دستورهای CREATE TABLE میسنجد، و salam sql generate پرسمانهای برچسبدار را به تابعهای گونهدار سلام و ساختارهای ردیف تبدیل میکند، به همان روحی که sqlc در Go دارد. زیر اینها درایورهای پایگاه داده برای SQLite، MySQL، MariaDB و PostgreSQL پشت یک میانجی مشترک قرار دارند.
این مقاله یک برنامهی پایگاه داده را از اتصال تا کد تولیدشده دنبال میکند، با برنامههایی که همانطور که هستند اجرا میشوند. هیچکدام سرور پایگاه داده لازم ندارند: یا از SQLite در حافظه استفاده میکنند یا اصلاً پایگاه دادهای نمیخواهند.
چرا رشتهی پرسمان منبع خطاست
در بیشتر زبانها SQL یک رشته است. کامپایلر درون آن را نگاه نمیکند، پس ستونی که در یک مهاجرت تغییر نام داده، نام جمع یک جدول یا جاینگهدار (placeholder) با گویش اشتباه را پایگاه داده پیدا میکند، در زمان اجرا و در همان مسیری از کد که کسی آزمونش نکرده است. ORMها با ساختن SQL از این مشکل دوری میکنند، به بهای یک زبان پرسمان دوم و کنترل کمتر. ابزارهایی مثل sqlc برای Go راه دیگری رفتند: SQL را همچنان بنویسید، ولی آن را تجزیه کنید، با طرحواره بسنجید و کد گونهدار بسازید.
سلام همین راه را درون زنجیرهی ابزارش ساخته است. طرحواره همان DDLی است که از پیش در مهاجرتهایتان دارید، پس پروندهی مدل جداگانهای نیست که بخواهید همگام نگهش دارید.
یک میانجی در سلام برای SQLite، MySQL و PostgreSQL
هر موتور بستهی خودش را دارد، و هر کدام را میتوان به پویا دیتابیس.اتصال (dyn db.Connection) مشترک از بستهی db تبدیل کرد:
| بسته | موتور | چطور وصل میشود |
|---|---|---|
| db.sqlite | SQLite، یک پرونده یا :memory: | کتابخانهی C خود SQLite را میپیچد، با آماده سازی و اتصال مقدار |
| db.mysql | MySQL و MariaDB | پروتکل کلاینت را به سلام پیاده میکند، با TLS و utf8mb4؛ بدون libmysqlclient |
| db.postgres | PostgreSQL | پروتکل سیم را به سلام پیاده میکند، با SCRAM-SHA-256 و TLS؛ بدون libpq |
کدی که روی پویا دیتابیس.اتصال نوشته شود روی هر سه اجرا میشود؛ آزمونها اینطور میتوانند از SQLite در حافظه استفاده کنند در حالی که محیط عملیاتی PostgreSQL دارد. میانجی اینها را میدهد: اجرا، پرسمان (ردیفها با بعدی، عدد صحیح، متن، عدد اعشار و خالی است خوانده میشوند)، شروع تراکنش برای تراکنشها، شناسه آخرین درج، خطا و نوع. کمکتابعهایی مثل دیتابیس.اجرا کن، دیتابیس.پرسمان عدد و دیتابیس.پرسمان متن کارهای یکخطی را پوشش میدهند و دیتابیس.در تراکنش فهرستی از دستورها را یا همه با هم یا هیچکدام اجرا میکند.
فراتر از SQL، همین خانواده کلاینتهایی برای Redis، ClickHouse و موتورهای جستوجوی Elasticsearch، OpenSearch، Meilisearch و Typesense دارد.
مهاجرت طرحواره و نقل قول امن
تغییرهای طرحواره مهاجرتهایی شمارهدار با یک دستور بالا و یک دستور پاییناند. دیتابیس.مهاجرت به بالا (db.MigrateUp) را هر بار که برنامه شروع میشود صدا بزنید: نسخهی اعمالشده را در جدولی از آنِ خودش ثبت میکند، پس نخستین اجرا طرحواره را میسازد، اجراهای بعدی کاری نمیکنند و انتشاری که مهاجرت ۳ را اضافه کند فقط همان را اعمال میکند.
واردسازی دیتابیس
واردسازی دیتابیس.اس کیو لایت
پایا طرح۱ := "CREATE TABLE customers (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, name TEXT)"
پایا طرح۲ := "CREATE TABLE orders (id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL, total_cents INTEGER NOT NULL, note TEXT)"
تابع مهاجرت(پ: پویا دیتابیس.اتصال):
ناپایا م := دیتابیس.مهاجرت گر جدید()
دیتابیس.افزودن مهاجرت(م، ۱، "customers"، طرح۱، "DROP TABLE customers")
دیتابیس.افزودن مهاجرت(م، ۲، "orders"، طرح۲، "DROP TABLE orders")
پیش := دیتابیس.نسخه طرحواره(م، پ)
اعمالشده := دیتابیس.مهاجرت به بالا(م، پ)
سرچاپ "طرحواره"، پیش، "->"، دیتابیس.نسخه طرحواره(م، پ)، "(" + اعمالشده + " مهاجرت اعمال شد)"
دیتابیس.آزادکردن مهاجرت گر(م)
پایان
تابع افزودن مشتری(پ: پویا دیتابیس.اتصال، رایانامه: رشته، نام: رشته): صحیح۶۴:
دستور := "INSERT INTO customers (email, name) VALUES (" + دیتابیس.نقل قول(رایانامه) + ", " + دیتابیس.نقل قول(نام) + ")"
اگر وارونه دیتابیس.اجرا کن(پ، دستور):
سرچاپ "درج ناموفق:"، پ.خطا()
برگشت ۰
پایان
برگشت پ.شناسه آخرین درج()
پایان
تابع ریشه:
پ := اس کیو لایت.اتصال(اس کیو لایت.بازکردن(":memory:"))
مهاجرت(پ)
مهاجرت(پ)
نخستین := افزودن مشتری(پ، "sara@example.com"، "سارا")
افزودن مشتری(پ، "omid@example.com"، "Omid O'Neil")
افزودن مشتری(پ، "sara@example.com"، "سارا دوباره")
سرچاپ "شمار مشتریها:"، دیتابیس.پرسمان عدد(پ، "SELECT COUNT(*) FROM customers")
ردیفها := پ.پرسمان("SELECT id, name FROM customers ORDER BY id")
تا ردیفها.بعدی():
سرچاپ "#" + ردیفها.عدد صحیح(۰)، ردیفها.متن(۱)
پایان
_ := ردیفها.بستن()
سرچاپ "شناسهی نخستین مشتری:"، نخستین
_ := پ.بستن()
پایاندومین فراخوانی مهاجرت چیزی اعمال نمیکند، رایانامهی تکراری با قید UNIQUE و با پیام خود موتور رد میشود، و آپاستروف درون O'Neil سالم میماند، چون مقدار از دیتابیس.نقل قول (db.QuoteLiteral) گذشته است. نام فارسی هم بیدردسر ذخیره و خوانده میشود. هرگز ورودی خام کاربر را به SQL نچسبانید. روی MySQL با نقل قولِ خود اتصال (c.Quote) نقل قول کنید که بکاسلش را هم گریز میدهد؛ روی SQLite دستورهای آماده با اس کیو لایت.آماده سازی و اتصال متن از این هم بهترند.
مرجع db دامهایی را که دانستنشان میارزد فهرست میکند: مهاجرت به بالا شمارهی نسخهها را مقایسه میکند، پس مهاجرتی که شمارهاش کمتر از بالاترین مهاجرت اعمالشده باشد بیصدا نادیده گرفته میشود؛ MySQL دستورهای DDL را یکییکی ثبت میکند، پس مهاجرتی که آنجا شکست بخورد ممکن است بخشی از آن اعمال شده باقی بماند؛ و SQL مهاجرت در نقطهویرگولها شکسته میشود، پس بدنهی یک تابع PostgreSQL به جای آن به اجرای مستقیم اتصال نیاز دارد.
بررسی SQL در زبان سلام بدون پایگاه داده
بستهی db.sql SQL را برای چهار گویش (SQLite، MySQL، MariaDB و PostgreSQL) واژهکاوی و تجزیه میکند. DDL خود را به آن بدهید تا فهرستی از جدولها و ستونها بسازد؛ سپس هر پرسمان را میتوان با آن سنجید. شکل خط فرمانی آن، رشتههای SQL را هم از کد سلام بیرون میکشد و هم از پروندههای .sql:
$ salam sql check src --schema=db
src/report.salam:6:68: error[SQL311]: no column named "totl_cents" in this statement
help: did you mean "total_cents"?
src/report.salam:7:45: warning[SQL320]: this DELETE has no WHERE clause and removes every row
help: add a WHERE clause, or use TRUNCATE when that is what you mean
src/report.salam:8:78: error[SQL317]: no table named "customer"
help: did you mean "customers"?
checked 3 statement(s) in 1 file(s): 2 error(s), 1 warning(s)این خروجی واقعی برای یک برنامهی کوتاه فارسی با سه خط SQL و سه اشتباه رایج است. پیامهای بررسیکنندهی SQL امروز انگلیسیاند، حتی وقتی برنامه به فارسی نوشته شده باشد. جایگاهها به پروندهی سلام اشاره میکنند و فرمان با وضعیت ۱ خارج میشود، پس در CI کنار salam check جا میگیرد. بدون --dialect، هر پرونده با موتوری که وارد کرده خوانده میشود، پس پروژهای که هم از db.sqlite و هم از db.mysql استفاده میکند بدون هیچ گزینهای درست بررسی میشود. پرسمانی که با + از تکهها ساخته شود بهعنوان تکه (fragment) شمرده و رد میشود و گزارش نمیشود، و خلاصهی پایانی میگوید چند مورد رد شده، پس هیچ چیز بیصدا نادیده گرفته نمیشود.
همین بررسیها به شکل کتابخانه هم در دسترساند، که در آزمونها به کار میآید. این برنامه یک طرحوارهی PostgreSQL را بار میکند و چهار پرسمان را از آن میگذراند، سپس میپرسد یکی از آنها چه میگیرد و چه برمیگرداند:
واردسازی دیتابیس.اس کیو ال
تابع گزارش کن(پرسش: رشته، طرح: اس کیو ال.فهرست جداول):
ناپایا گ := اس کیو ال.بررسی(پرسش، طرح)
اگر گ.بدون خطا و گ.هشدارها == ۰:
سرچاپ "درست"، پرسش
وگرنه:
سرچاپ "نادرست"، پرسش
هر ت در گ.موارد:
سرچاپ " "، ت.کد، ت.پیام
پایان
پایان
اس کیو ال.آزادسازی گزارش(گ)
پایان
تابع ریشه:
ناپایا طرح := اس کیو ال.فهرست تازه(اس کیو ال.گویش شماره(اس کیو ال.پستگرس))
دیرکن اس کیو ال.آزادسازی فهرست(طرح)
ناپایا تعریف := اس کیو ال.اعمال(طرح، """
CREATE TABLE customers (id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL, name TEXT);
CREATE TABLE orders (id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL, total_cents BIGINT NOT NULL);
""")
اس کیو ال.آزادسازی گزارش(تعریف)
گزارش کن("SELECT id, phone FROM customers WHERE id = $1"، طرح)
گزارش کن("SELECT id FROM customers WHERE email = ?"، طرح)
گزارش کن("UPDATE orders SET total_cents = 0"، طرح)
گزارش کن("SELECT c.name, o.total_cents FROM orders o JOIN customers c ON c.id = o.customer_id WHERE o.id = $1"، طرح)
ناپایا تحلیل := اس کیو ال.تحلیل کن("SELECT c.name, o.total_cents FROM orders o LEFT JOIN customers c ON c.id = o.customer_id WHERE o.id = $1"، طرح)
پارامتر := تحلیل.پارامترها.بگیر(۰)
سرچاپ "میگیرد"، پارامتر.ستون، "با گونهی"، اس کیو ال.گونه سلام(پارامتر.نوع، پارامتر.تهیناپذیر)
هر ستون در تحلیل.نتیجهها:
سرچاپ "میدهد"، ستون.نام، "با گونهی"، اس کیو ال.گونه سلام(ستون.نوع، وارونه ستون.تهیپذیر)
پایان
اس کیو ال.آزادسازی تحلیل(تحلیل)
پایانپرسمان دوم روی SQLite و MySQL درست است ولی روی PostgreSQL نه، چون PostgreSQL جاینگهدارهایش را $1، $2 شمارهگذاری میکند؛ گویش یک اشارهی صرف نیست. تحلیل پایانی نشان میدهد چرا این چیزی بیش از یک لینتر است: LEFT JOIN هر ستون customers را تهیپذیر میکند، پس name به شکل str? (در کد فارسی رشته?) برمیگردد، در حالی که total_cents قید NOT NULL دارد و یک i64 (صحیح۶۴) ساده برمیگرداند. اس کیو ال.گونه سلام نام گونه را با املای انگلیسیاش میدهد. این همان امضایی است که کد شما باید داشته باشد، برگرفته از طرحواره به جای حافظهی برنامهنویس.
کدهای تشخیص پایدارند: SQL0xx واژگانی، SQL1xx نحوی، SQL2xx قاعدههای گویش و SQL3xx نامها و شمارشها در برابر طرحواره. روی کد تطبیق دهید، نه روی متن پیام.
تولید تابعهای گونهدار برای پرسمانها
گام آخر مکانیکی است، پس زنجیرهی ابزار آن را انجام میدهد. یک پروندهی .sql را برای هر پرسمان با یک نام و یک نوع برچسب بزنید، همانطور که sqlc میکند:
-- name: CustomerByEmail :one
SELECT id, email, name FROM customers WHERE email = ?;
-- name: OrdersFor :many
SELECT id, total_cents, note FROM orders WHERE customer_id = ? ORDER BY id;
-- name: AddOrder :exec
INSERT INTO orders (customer_id, total_cents, note) VALUES (?, ?, ?);سپس کد بسازید:
$ salam sql generate queries --schema=db --output=gen/store.salam --package=store
wrote gen/store.salam (3 queries)نوعها :one (نخستین ردیف، با یک پارامتر خروجی ok)، :many (یک وکتور از ردیفها)، :exec (یک منطقی) و :execrows (شمار ردیفهای تغییرکرده) هستند. هر پرسمان پیش از تولید با طرحواره تحلیل میشود، پس غلط تایپی تولید کد را با نام پرسمان در پیام متوقف میکند و هرگز به پروندهی تولیدشده نمیرسد. پارامترها به نام ستونی نامگذاری میشوند که هر جاینگهدار با آن مقایسه شده و گونهشان را از همان ستون میگیرند.
مولد امروز کد سلام انگلیسی مینویسد و نام تابعها و ساختارها را از نام پرسمانها در پروندهی SQL برمیدارد. در پایین کد تولیدشده برای دو پرسمان آمده که با salam translate fa به سلام فارسی برگردانده شده و درون برنامهای گذاشته شده که از آن استفاده میکند، تا هم ببینید مولد چه مینویسد و هم اینکه اجرا میشود. نامهایی مثل OrdersFor و customer_id همانطور که مولد ساخته باقی ماندهاند. در یک پروژه، به جای این کار، بستهی تولیدشده را فراخوانی یا وارد میکنید.
واردسازی دیتابیس
واردسازی دیتابیس.اس کیو ال
واردسازی دیتابیس.اس کیو لایت
واردسازی حافظه
ساختار OrdersForRow:
همگانی id: صحیح۶۴ = 0 برگردان صحیح۶۴
همگانی total_cents: صحیح۶۴ = 0 برگردان صحیح۶۴
همگانی note: رشته? = پوچ
پایان
تابع fill_query(c: پویا دیتابیس.اتصال، raw: رشته، a: اس کیو ال.مقدارها): رشته:
ناپایا rep := اس کیو ال.گزارش {}
out := اس کیو ال.پرکردن(raw، a، اس کیو ال.گویش نام(c.نوع())، rep)
اس کیو ال.آزادسازی گزارش(rep)
برگشت out
پایان
همگانی تابع OrdersFor(c: پویا دیتابیس.اتصال، customer_id: صحیح۶۴): وکتور<OrdersForRow>:
ناپایا a := اس کیو ال.مقدارهای تازه()
اس کیو ال.افزودن عدد(a، customer_id)
q := fill_query(c، "SELECT id, total_cents, note FROM orders WHERE customer_id = ? ORDER BY id"، a)
اس کیو ال.آزادسازی مقدارها(a)
ناپایا rows := وکتور {} برگردان وکتور<OrdersForRow>
r := c.پرسمان(q)
اگر r.معتبر():
تا r.بعدی():
ناپایا out := OrdersForRow {}
out.id = r.عدد صحیح۶۴(0)
out.total_cents = r.عدد صحیح۶۴(1)
اگر r.خالی است(2):
out.note = پوچ
وگرنه:
out.note = r.متن(2)
پایان
rows.بیفزا(out)
پایان
r.بستن()
پایان
حافظه.آزادسازی(q برگردان تهی*)
برگشت rows
پایان
همگانی تابع AddOrder(c: پویا دیتابیس.اتصال، customer_id: صحیح۶۴، total_cents: صحیح۶۴، note: رشته): منطقی:
ناپایا a := اس کیو ال.مقدارهای تازه()
اس کیو ال.افزودن عدد(a، customer_id)
اس کیو ال.افزودن عدد(a، total_cents)
اس کیو ال.افزودن متن(a، note)
q := fill_query(c، "INSERT INTO orders (customer_id, total_cents, note) VALUES (?, ?, ?)"، a)
اس کیو ال.آزادسازی مقدارها(a)
done := c.اجرا(q) برابر 0
حافظه.آزادسازی(q برگردان تهی*)
برگشت done
پایان
تابع ریشه:
پ := اس کیو لایت.اتصال(اس کیو لایت.بازکردن(":memory:"))
_ := دیتابیس.اجرا کن(پ، "CREATE TABLE orders (id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL, total_cents INTEGER NOT NULL, note TEXT)")
_ := AddOrder(پ، ۱ برگردان صحیح۶۴، ۱۲۵۰ برگردان صحیح۶۴، "سفارش نخست")
_ := AddOrder(پ، ۱ برگردان صحیح۶۴، ۹۹۰ برگردان صحیح۶۴، "هدیهی 'نوروز'")
_ := دیتابیس.اجرا کن(پ، "INSERT INTO orders (customer_id, total_cents) VALUES (1, 400)")
سفارشها := OrdersFor(پ، ۱ برگردان صحیح۶۴)
هر س در سفارشها:
سرچاپ س.id، س.total_cents، س.note.یابرگردان("(بییادداشت)")
پایان
سفارشها.آزادکن()
_ := پ.بستن()
پایانسه چیز در کد تولیدشده به چشم میآید. ستون تهیپذیر note به یک رشته? تبدیل میشود و خوانندهی تولیدشده پیش از خواندن، خالی است را بررسی میکند، پس فراخواننده مجبور است تصمیم بگیرد یادداشت غایب چه معنایی دارد. تابعها پویا دیتابیس.اتصال میگیرند، پس یک پروندهی تولیدشده به SQLite، MySQL و PostgreSQL خدمت میدهد و نقل قول مخصوص هر گویش را در زمان اجرا از c.نوع() انتخاب میکند. و مقدارها از راه اس کیو ال.پرکردن (sql.Fill) وارد SQL میشوند که اول پرسمان را واژهکاوی میکند، پس ? درون یک لفظ رشتهای دست نمیخورد، شمار مقدارها باید درست باشد و نقل قولها برای آن گویش درست دوتایی میشوند؛ آپاستروفهای 'نوروز' در خروجی سالم ماندهاند.
همین نکتهی آخر یک محدودیت هم هست: پرکردن جایگزینی است، نه دستور آمادهی سمت سرور. نقل قول امنی است که یک بار انجام و بازبینی شده، نه استفادهی دوباره از نقشهی اجرای پرسمان. امروز فقط db.sqlite آمادهسازی و اتصال مقدار (Prepare و Bind) را دارد.
تراکنش، استخر اتصال و نخها
برای چند دستور که باید با هم موفق یا با هم ناموفق شوند، c.شروع تراکنش() یک پویا دیتابیس.تراکنش با اجرا و پرسمان خودش برمیگرداند که با ثبت تراکنش یا بازگردانی تمام میشود؛ پرسمانهای درون تراکنش تغییرهای ثبتنشدهی خودش را میبینند. دیتابیس.در تراکنش فهرستی ثابت از دستورها را در یک تراکنش اجرا میکند.
سرور چند اتصال را باز نگه میدارد و به هر درخواست یکی را قرض میدهد. دیتابیس.استخر جدید، افزودن، گرفتن، اتصال اجارهای و رهاکردن حسابوکتاب این کار را نگه میدارند. خود استخر قفلی نمیگیرد، پس قرض گرفتن و پس دادن را از یک نخ انجام دهید یا با یک قفل از همگام سازی (sync.Mutex) از آن محافظت کنید، و هرگز یک اتصال را همزمان از دو نخ به کار نبرید. سادهترین الگوی امن برای نخهای کارگر، یک اتصال برای هر نخ است. مقالهی همزمانی در سلام قفلها و استخرهای مربوط را پوشش میدهد.
مقایسه با ORM و sqlc، و آنچه کم است
در مقایسه با ORMی مثل ORM جنگو یا Entity Framework، سلام شما را در SQL نگه میدارد و جز ساختارهای ردیف تولیدشده نگاشت شیءای نمیدهد. در مقایسه با sqlc، ایده یکی است و یکپارچگی تنگتر: بررسیکننده همراه کامپایلر میآید، SQL را از پروندههای منبع معمولی میخواند و پروندهی پیکربندی جداگانهای نمیخواهد. در مقایسه با درایورهای خام در C یا پایتون، اطلاعات گونه و بررسی طرحواره را پیش از زمان اجرا به دست میآورید.
محدودیتها را باید گفت:
- تحلیل ایستا مرز دارد. جدولی که ستونهایش ناشناخته است، مثل جدولی که با
CREATE TABLE ... AS SELECTساخته شده، باز گذاشته میشود و ستونهایش هرگز غایب گزارش نمیشوند. اول خطاهای نحوی را درست کنید، چون بررسی نامها فقط روی دستورهایی اجرا میشود که تجزیه شوند. - دستور آماده روی MySQL و PostgreSQL از راه میانجی مشترک هنوز وجود ندارد؛ مقدارها درون SQL نقل قول میشوند.
- ردیفها روی PostgreSQL یکجا گرفته میشوند، پس
تعداد ردیفاز ابتدا معلوم است، ولی یک نتیجهی بسیار بزرگ یکجا در حافظه نگه داشته میشود. - درایورهای جوان. به سلام نوشته شدهاند و در ساختهای انتشار پروژه در برابر سرورهای واقعی MySQL و PostgreSQL آزمون میشوند، ولی سابقهی عملیاتیشان بسیار کمتر از libpq یا رابطهای رسمی MySQL است.
برای تصویر بزرگتر از آنچه میشود دور اینها ساخت، مقالههای توسعهی فولاستک با سلام و گشتی در کتابخانهی استاندارد را ببینید.
پرسشهای پرتکرار
آیا زبان سلام ORM دارد؟
نه. رویکرد سلام اول SQL است: پرسمانها را خودتان مینویسید، salam sql check آنها را با طرحواره میسنجد و salam sql generate تابعهای گونهدار و ساختارهای ردیف را مینویسد. نتیجه به sqlc نزدیکتر است تا به یک ORM.
آیا سلام به libpq یا libmysqlclient نیاز دارد؟
نه. درایورهای PostgreSQL و MySQL پروتکلهای سیم را به خود سلام پیاده میکنند، از جمله TLS و احراز هویت گذرواژهی مدرن. فقط SQLite به کتابخانهی C خودش پیوند میخورد.
آیا salam SQL check در CI اجرا میشود؟
بله. salam sql check src --schema=db وقتی خطایی پیدا کند با وضعیت ۱ خارج میشود، برای هر مشکل پرونده، خط و ستون را چاپ میکند و به سرور پایگاه داده نیازی ندارد؛ فقط پروندههای DDL را میخواهد.
زبان سلام کدام گویشهای SQL را میفهمد؟
SQLite، MySQL، MariaDB و PostgreSQL. جاینگهدارها، نقل قول، ستونهای شناسهای و نحوهای خاص هر گویش مثل ILIKE یا ON DUPLICATE KEY برای هر گویش جداگانه بررسی میشوند.