পাইথন স্ট্রিং-এর মধ্যে পোস্টগ্রেসকিউএল ডলার $$ কোটিং
Table of Contents
ডলার কোটিং কী?
PostgreSQL একটি নন-স্ট্যান্ডার্ড কোটিং পদ্ধতি সাপোর্ট করে, যাকে বলা হয় ডলার কোটিং ($$)। এর মাধ্যমে আপনি লিটারাল স্ট্রিং — এমনকি সিঙ্গেল কোট সহ — কোনো এসকেপিং ছাড়াই এমবেড করতে পারেন। পাইথন থেকে সরাসরি স্টোরড প্রসিডিউর ও ফাংশন লেখার সময় এটি অমূল্য।
স্ট্যান্ডার্ড সিঙ্গেল-কোট কোটিং তখনই ভেঙে পড়ে, যখন আপনার SQL বডিতে নিজস্ব কোট থাকে:
-- ফাংশন বডিতে সিঙ্গেল কোট থাকলে এটি ফেইল করে
CREATE OR REPLACE FUNCTION greet(name TEXT) RETURNS TEXT AS '
BEGIN
RETURN ''Hello, '' || name || ''!''; -- ডাবল-এসকেপড — অগোছালো
END;
' LANGUAGE plpgsql;
ডলার কোটিং এসকেপিং সম্পূর্ণভাবে দূর করে দেয়:
CREATE OR REPLACE FUNCTION greet(name TEXT) RETURNS TEXT AS $$
BEGIN
RETURN 'Hello, ' || name || '!'; -- পরিষ্কার, কোনো এসকেপিং নেই
END;
$$ LANGUAGE plpgsql;
পাইথন স্ট্রিং-এর সমস্যা
যখন আপনি ডলার-কোটেড SQL পাইথন স্ট্রিং-এর ভেতরে এমবেড করেন, তখন একটি সূক্ষ্ম সমস্যার মুখোমুখি হন: পাইথনের f-string এবং .format() উভয়েই { ও } কে ইন্টারপোলেশন ডেলিমিটার হিসেবে ব্যবহার করে, তবে এগুলো $$ এর সাথে সংঘর্ষ করে না। আসল ফাঁদটি হলো raw string বা triple-quote ভুলভাবে ব্যবহার করা।
নিরাপদ প্যাটার্ন
Triple-quoted স্ট্রিং (প্রস্তাবিত):
import psycopg2
CREATE_FUNCTION_SQL = """
CREATE OR REPLACE FUNCTION calculate_percentile(
p_table TEXT,
p_column TEXT,
p_percentile FLOAT
) RETURNS FLOAT AS $$
DECLARE
result FLOAT;
BEGIN
EXECUTE format(
'SELECT percentile_cont(%L) WITHIN GROUP (ORDER BY %I) FROM %I',
p_percentile, p_column, p_table
) INTO result;
RETURN result;
END;
$$ LANGUAGE plpgsql;
"""
def create_analytics_functions(conn):
with conn.cursor() as cur:
cur.execute(CREATE_FUNCTION_SQL)
conn.commit()
ডলার-কোটেড ফাংশনসহ Django মাইগ্রেশন:
from django.db import migrations
FORWARD_SQL = """
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(NEW.body, '')), 'B');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER articles_search_vector_update
BEFORE INSERT OR UPDATE ON articles_article
FOR EACH ROW EXECUTE FUNCTION update_search_vector();
"""
REVERSE_SQL = """
DROP TRIGGER IF EXISTS articles_search_vector_update ON articles_article;
DROP FUNCTION IF EXISTS update_search_vector();
"""
class Migration(migrations.Migration):
dependencies = [("articles", "0003_add_search_vector_field")]
operations = [
migrations.RunSQL(sql=FORWARD_SQL, reverse_sql=REVERSE_SQL),
]
প্যারামিটারাইজড কোয়েরি এখনও প্রযোজ্য
ডলার কোটিং SQL-এর স্ট্রাকচার সামলায়, ইউজার ডেটা নয়। ডলার-কোটেড ব্লকের ভেতরে কখনোই ইউজার ইনপুট ইন্টারপোলেট করবেন না — ভ্যালুর জন্য সবসময় প্যারামিটারাইজড কোয়েরি ব্যবহার করুন:
# খারাপ: ডলার কোটিং থাকা সত্ত্বেও SQL ইনজেকশনের ঝুঁকি
def bad_search(conn, user_query: str):
sql = f"""
SELECT * FROM articles
WHERE body @@ to_tsquery('english', $${user_query}$$);
"""
# ^^^ এটি করবেন না
# ভালো: প্যারামিটারাইজড ভ্যালু, ডলার-কোটেড ফাংশন বডি
def safe_search(conn, user_query: str):
sql = """
SELECT id, title, ts_rank(search_vector, query) AS rank
FROM articles, to_tsquery('english', %s) query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;
"""
with conn.cursor() as cur:
cur.execute(sql, (user_query,))
return cur.fetchall()
FastAPI + asyncpg উদাহরণ
asyncpg-এর ক্ষেত্রে ডলার কোটিং হুবহু একইভাবে কাজ করে — শুধু positional প্যারামিটারের জন্য %s এর বদলে $1, $2 (asyncpg-স্টাইল) ব্যবহার করুন:
from fastapi import FastAPI, Depends
import asyncpg
app = FastAPI()
CREATE_UPSERT_FUNCTION = """
CREATE OR REPLACE FUNCTION upsert_post(
p_slug TEXT,
p_title TEXT,
p_body TEXT,
p_author_id INT
) RETURNS INT AS $$
DECLARE
v_id INT;
BEGIN
INSERT INTO posts (slug, title, body, author_id, updated_at)
VALUES (p_slug, p_title, p_body, p_author_id, NOW())
ON CONFLICT (slug) DO UPDATE
SET title = EXCLUDED.title,
body = EXCLUDED.body,
updated_at = NOW()
RETURNING id INTO v_id;
RETURN v_id;
END;
$$ LANGUAGE plpgsql;
"""
@app.on_event("startup")
async def setup_db(pool: asyncpg.Pool = Depends(get_pool)):
async with pool.acquire() as conn:
await conn.execute(CREATE_UPSERT_FUNCTION)
Elasticsearch ইন্টিগ্রেশন নোট
PostgreSQL-এর কনটেন্ট Elasticsearch-এ ইনডেক্স করার সময় ফুল-টেক্সট সার্চ পাইপলাইনটি দেখতে এমন হয়:
- PostgreSQL — সোর্স অব ট্রুথ; tsvector ট্রিগার
search_vectorকলামটি আপডেটেড রাখে - Redis — হট রিডের জন্য write-through ক্যাশ; pub/sub চ্যানেল ইনডেক্সারকে নোটিফাই করে
- Elasticsearch — একটি পাইথন ওয়ার্কারের মাধ্যমে ডিনরমালাইজড ডকুমেন্ট গ্রহণ করে, যা Redis চ্যানেল শোনে
উপরের ডলার-কোটেড ট্রিগার ফাংশনটি (update_search_vector) এই পাইপলাইনের প্রথম ধাপ। যে ইভেন্টটি search_vector আপডেট করে, সেটিই Redis-এ নোটিফিকেশন পাঠাতে পারে:
# একই মাইগ্রেশনে ট্রিগার ফাংশনটি সম্প্রসারিত করুন
UPDATE_TRIGGER_SQL = """
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(NEW.body, '')), 'B');
-- pg_notify-এর মাধ্যমে Elasticsearch ইনডেক্সারকে নোটিফাই করুন
PERFORM pg_notify('es_index_channel', NEW.id::TEXT);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
সারসংক্ষেপ
| প্যাটার্ন | ব্যবহারের ক্ষেত্র |
|---|---|
$$...$$ |
মাইগ্রেশনে ফাংশন/প্রসিডিউর বডি |
$body$...$body$ |
নেস্টেড ডলার কোটিং, যখন বডিতেই $$ থাকে |
%s / $1 |
ইউজার-সরবরাহকৃত ভ্যালু — সবসময় প্যারামিটারাইজড |
psycopg2.sql.Identifier |
ডায়নামিক টেবিল/কলামের নাম — কখনোই f-string নয় |
ডলার কোটিং PostgreSQL-এর সেই ফিচারগুলোর একটি, যা প্রথমে খুবই নিস (niche) মনে হয় — কিন্তু যখনই আপনি বাস্তব ব্যাকএন্ড ইনফ্রাস্ট্রাকচার লেখা শুরু করেন, তখন এটি অপরিহার্য হয়ে ওঠে।