2025, Sep 28 21:33
Dialect‑aware तरीके से SQLAlchemy में identifiers को सुरक्षित quote करें
SQLAlchemy Core में raw SQL लिखते समय टेबल, स्कीमा और कॉलम नाम सुरक्षित quote करें। IdentifierPreparer, quote_identifier व quote_schema से dialect‑aware, पोर्टेबल समाधान।
SQLAlchemy Core में जब आपको टेबल, स्कीमा या कॉलम के नाम रनटाइम पर देने होते हैं, तो raw SQL में identifiers को सुरक्षित रूप से quote करना बार‑बार आने वाली समस्या बन जाता है। मैन्युअल escaping कमज़ोर होती है और अक्सर किसी एक डेटाबेस तक सीमित रह जाती है। लेकिन एक dialect‑aware तरीका मौजूद है, जिससे आप हर backend के नियमों का अनुमान लगाए बिना काम कर सकते हैं।
समस्या
मान लीजिए आप raw SQL चलाना चाहते हैं, जिसमें टेबल, स्कीमा और कॉलम के नाम बदलते रहते हैं, और आपने इन्हें खुद ही escape करने की कोशिश की। उद्देश्य है ऐसा क्वेरी बनाना जो अलग‑अलग डेटाबेस में सुरक्षित रहे, लेकिन हाथ से लिखा हुआ escape‑helper अक्सर किसी एक backend पर निर्भर हो जाता है और भरोसेमंद नहीं रहता।
import re, sqlalchemy
def _safe_ident(token, force_quotes=False):
    # चेतावनी: यह केवल Postgres में काम करता है और प्रोडक्शन कोड के लिए नहीं है।
    if not force_quotes and re.match(r'^[a-z_][a-z0-9$_]*$', token, re.IGNORECASE):
        return token
    return '"' + token.replace('"', '""') + '"'
def fetch_row(tbl_name, pk_col, pk_value, sch_name='public'):
    qi = _safe_ident
    stmt = sqlalchemy.sql.text(f'''
                                SELECT *
                                FROM {qi(sch_name)}.{qi(tbl_name)}
                                WHERE {qi(pk_col)} = :k;
                                ''')
    args = dict(k=pk_value)
    # दिए गए आर्ग्युमेंट्स के साथ स्टेटमेंट चलाएँ और डेटा लौटाएँ
    # ...
यह तरीका Postgres के QUOTE_IDENT जैसा व्यवहार नकल करता है, पर SQLite, MySQL या MSSQL जैसे अन्य dialects पर साफ़ तौर से लागू नहीं होता। यहां तक कि Postgres में भी यह मान लेना कठिन है कि यह हर किनारे‑मामले को संभाल लेगा, और pre‑quoting के लिए डेटाबेस‑पक्ष के फ़ंक्शन्स पर निर्भर होना एक अतिरिक्त round‑trip जोड़ देता है, जिसकी आपको शायद ज़रूरत नहीं।
वास्तव में हो क्या रहा है
Identifier को quote करने के नियम एक जैसे नहीं होते। अलग‑अलग backends अलग quote‑चिन्ह और escaping नियम अपनाते हैं। किसी regex और quote‑लॉजिक को हार्डकोड कर देना आपके कोड को एक ही डेटाबेस से बाँध देता है और तब सूक्ष्म बग्स का जोखिम बढ़ जाता है, जब नाम आरक्षित शब्दों से टकराते हैं, स्पेसेज़ होते हैं, या मिश्रित केस में लिखे जाते हैं। SQLAlchemy पहले से ही इन नियमों को प्रति‑dialect समेट कर रखता है; raw स्ट्रिंग जोड़‑जुगाड़ उस मशीनरी को दरकिनार कर देता है।
समाधान
SQLAlchemy के ज़रिए dialect का IdentifierPreparer इस्तेमाल करें। यह ऐसे मेथड्स देता है जो लक्ष्य डेटाबेस के लिए सही quoting नियम लागू करते हैं। इसका एंट्री‑पॉइंट Engine से मिलता है, और आप इससे अलग‑अलग identifiers के साथ‑साथ schema नाम भी सही तरह quote कर सकते हैं।
from sqlalchemy import create_engine
# अपने लक्षित DB के लिए एक engine बनाएँ; सुनिश्चित करें कि ड्राइवर इंस्टॉल है
# उदाहरण: PostgreSQL के लिए psycopg2 या MSSQL के लिए pyodbc
eng = create_engine('postgresql://')
prep = eng.dialect.identifier_preparer
# dialect के नियमों के अनुसार identifiers को quote करें
quoted_tbl = prep.quote_identifier('foo bar')   # "foo bar" लौटाता है
quoted_sch = prep.quote_schema('public')
quoted_col = prep.quote_identifier('id')
# quoted भागों के साथ raw SQL सुरक्षित रूप से बनाएँ
from sqlalchemy import sql
stmt = sql.text(f'''
                 SELECT *
                 FROM {quoted_sch}.{quoted_tbl}
                 WHERE {quoted_col} = :key
                 ''')
# स्टेटमेंट को सामान्य तरह से पैरामीटर्स के साथ चलाएँ
इस तरह आप raw SQL का आराम बनाए रखते हैं, जबकि identifiers को quote करने का कठिन काम सही लेयर के हवाले कर देते हैं। जिस engine को आप बनाते हैं, उसके लिए उपयुक्त ड्राइवर पैकेज चाहिए होगा—उदाहरण के लिए PostgreSQL के लिए psycopg2 या MSSQL के लिए pyodbc।
Engine बनाए बिना dialects
यदि आप Engine नहीं बना सकते, या बनाना नहीं चाहते, तो संभव है कि आप सीधे dialect बना लें। यह लाइव कनेक्शन से बचाता है, लेकिन इम्प्लीमेंटेशन‑डीटेल्स पर निर्भर है और वर्ज़न्स के बीच इसकी गारंटी नहीं है।
from sqlalchemy import URL
url = URL.create('mssql')
entrypoint = url._get_entrypoint()
dialect = entrypoint.get_dialect_cls(url)()  # या get_async_dialect_cls
quoted = dialect.identifier_preparer.quote_identifier('foo bar')  # [foo bar] लौटाता है
यह एक प्राइवेट मेथड तक पहुँच कर काम करता है, इसलिए इसे नाज़ुक मानें। जहाँ संभव हो, Engine‑backed तरीका ही प्राथमिकता दें।
यह क्यों मायने रखता है
ऐसा dynamic SQL, जिसमें identifiers interpolate किए जाते हैं, टूलिंग, माइग्रेशन्स और क्वेरी बिल्डर्स में आम ज़रूरत है। Quoting गलत होने पर portability टूट सकती है, ऐसे सिंटैक्स एरर बन सकते हैं जिनका निदान मुश्किल हो, या injection का जोखिम बढ़ सकता है। Dialect के अपने IdentifierPreparer पर भरोसा करके आप raw SQL का उपयोग जारी रखते हैं और साथ ही backend‑specific नियमों के अनुरूप रहते हैं।
व्यावहारिक निष्कर्ष
जब SQLAlchemy Core के साथ आपको टेबल, स्कीमा या कॉलम के नाम रनटाइम पर देने हों, तो identifier quoting नया बनाकर न करें। eng.dialect.identifier_preparer.quote_identifier का उपयोग करें और जहाँ ज़रूरत हो, quote_schema भी। यह सुनिश्चित करें कि सही ड्राइवर उपलब्ध हो ताकि dialect बन सके। यदि Engine बनाने से बचना अनिवार्य हो, तो आप URL और एक private entrypoint के ज़रिए dialect निकाल सकते हैं, लेकिन इस रास्ते को ऐसे इम्प्लीमेंटेशन‑डीटेल की तरह लें जो बदल भी सकता है। वैल्यूज़ को पहले की तरह bound parameters के रूप में रखें, और स्ट्रिंग interpolation केवल उन्हीं identifiers के लिए करें जिन्हें dialect ने prepare किया है।
यह लेख StackOverflow पर प्रश्न (लेखक: doekman) और snakecharmerb के उत्तर पर आधारित है।