PostgreSQL-এ পুনরাবৃত্ত সময়সূচি ও টাইমজোন: নমুনা
PostgreSQL-এ পুনরাবৃত্ত শিডিউল এবং টাইমজোনগুলো কীভাবে সঠিকভাবে সংরক্ষণ ও জেনারেট করবেন—স্টোরেজ ফরম্যাট, রিকারেন্স নিয়ম, ব্যতিক্রম এবং কুয়েরি প্যাটার্নের ব্যবহার যা ক্যালেন্ডারকে সঠিক রাখে।

কেন টাইমজোন ও পুনরাবৃত্ত ইভেন্ট ভ্রান্ত হয়
অধিকাংশ ক্যালেন্ডার বাগ গণিতগত না; এগুলো মানে-সংক্রান্ত বাগ। আপনি একটি জিনিস সংরক্ষণ করেন (একটি মুহূর্ত সময়), কিন্তু ব্যবহারকারীরা অন্য কিছু আশা করে (নির্দিষ্ট স্থানের একটি স্থানীয় ক্লক টাইম)। এই ব্যবধানই কারণ যে পুনরাবৃত্ত শিডিউল এবং টাইমজোন পরীক্ষায় ঠিক দাঁড়ায়, কিন্তু বাস্তব ব্যবহারকারী আসলে ভেঙে দেয়।
ডে লাইট সেভিং টাইম (DST) হল ক্লাসিক ট্রিগার। "প্রতিটি রবিবার 09:00" বলতে আপনি যদি এটি বোঝান না যে এটা স্থানীয় ক্লক টাইমে প্রতিবার 09:00, এবং যদি আপনি এটাকে "প্রারম্ভিক টাইমস্ট্যাম্প থেকে প্রতি 7 দিন" হিসেবে গণনা করেন, তখন অফসেট বদলে গেলে দুইটি ধারণা এক ঘণ্টা করে বিচ্ছিন্ন হবে এবং আপনার ক্যালেন্ডার ঢেঁকুরিয়ে যাবে।
ভ্রমণ ও মিশ্র টাইমজোন আরও এক স্তর যোগ করে। একটি বুকিং হয়তো কোনো স্থানের সঙ্গে (উদাহরণ: শিকাগোর একটি সেলুন চেয়ার) আবদ্ধ, যখন দেখছেন সেই ব্যক্তি লন্ডনে থাকতে পারেন। যদি আপনি স্থান-ভিত্তিক শিডিউলকে ব্যক্তি-ভিত্তিক হিসাবে বিবেচনা করেন, তাহলে অন্তত একপক্ষকে ভুল স্থানীয় সময় দেখাবেন।
সাধারণ ব্যর্থতার ধরনসমূহ:
- আপনি একটি সংরক্ষিত টাইমস্ট্যাম্পে ইনটারভাল যোগ করে recurrences তৈরি করেন, তারপর DST বদলায়।
- আপনি জোন রুল ছাড়া "লোকাল টাইম" সংরক্ষণ করেন, তাই পরবর্তীতে উদ্দেশ্যকৃত ইনস্ট্যান্টগুলো পুনর্নির্মাণ করতে পারেন না।
- আপনি কেবল এমন তারিখগুলো টেস্ট করেন যা কখনও DST সীমা অতিক্রম করে না।
- আপনি একক কুয়েরিতে "ইভেন্ট টাইমজোন", "ইউজার টাইমজোন" এবং "সার্ভার টাইমজোন" মিলিয়ে ফেলেন।
স্কিমা বেছে নেওয়ার আগে নির্ধারণ করুন আপনার প্রোডাক্টে "সঠিক" মানে কী।
একটি বুকিংয়ের জন্য, "সঠিক" সাধারণত মানে: অ্যাপয়েন্টমেন্টটি ভেন্যুর টাইমজোনে প্রত্যাশিত ওয়াল-ক্লক টাইম অনুযায়ী হচ্ছে, এবং যেকেউ সেটি দেখুক সঠিক রূপান্তর পায়।
একটি শিফটের ক্ষেত্রে, "সঠিক" প্রায়ই মানে: শিফটটি স্টোরের জন্য একটি স্থির স্থানীয় সময়ে শুরু হয়, এমনকি কর্মচারী ভ্রমণ করলে হলেও।
এই এক সিদ্ধান্ত (শিডিউল কি স্থানের সাথে যুক্ত হবে নাকি ব্যক্তির সাথে) সবকিছু নির্ধারণ করে: আপনি কী সংরক্ষণ করবেন, কিভাবে recurrences তৈরি করবেন, এবং কিভাবে একটি ক্যালেন্ডার ভিউ কুয়েরি করবেন যেন এক-ঘন্টার অপ্রত্যাশিত পরিবর্তন না ঘটে।
সঠিক মানসিক মডেল বেছে নিন: ইনস্ট্যান্ট বনাম লোকাল টাইম
অনেক বাগ আসে দুটি ভিন্ন সময় ধারণা মিশ্রিত করার ফলে:
- একটি ইনস্ট্যান্ট: একটি নির্দিষ্ট মুহূর্ত যা একবার ঘটে।
- একটি লোকাল টাইম রুল: একটি ওয়াল ক্লক টাইম যেমন "প্রতিটি সোমবার প্যারিসে সকাল 9:00"।
একটি ইনস্ট্যান্ট সর্বত্র একই। "2026-03-10 14:00 UTC" একটি ইনস্ট্যান্ট। ভিডিও কল, ফ্লাইট ডিপারচার, এবং "নির্দিষ্ট মুহূর্তে নোটিফাই পাঠাও" সাধারণত ইনস্ট্যান্ট।
লোকাল টাইম হলো লোকেরা যে ঘড়িতে দেখে সেই স্থানীয় সময়। "প্রতিটি সপ্তাহের দিন প্যারিসে সকাল 9:00" হল লোকাল টাইম। স্টোরের সময়, পুনরাবৃত্ত ক্লাস, এবং স্টাফ শিফট সাধারণত কোনো লোকেশনের টাইমজোনে অ্যাঙ্কর করা হয়। টাইমজোনটি প্রদর্শনের পছন্দ নয়—এটি অর্থের অংশ।
সরল একটি নীতিসূত্র:
- ইভেন্ট যদি সারা বিশ্বের জন্য এক বাস্তব মুহূর্তে ঘটতে হবে, তাহলে start/end কে ইনস্ট্যান্ট হিসেবে সংরক্ষণ করুন (timestamptz)।
- ইভেন্ট যদি একটি স্থানের ঘড়ির সময় অনুসরণ করে, তাহলে লোকাল তারিখ ও লোকাল সময় প্লাস একটি zone ID সংরক্ষণ করুন।
- ব্যবহারকারী যদি ভ্রমণ করে, দেখানোর সময় ভিউয়ারের জোনে সময় দেখান, কিন্তু শিডিউলকে তার নিজেস্ব জোনে অ্যাঙ্কর করুন।
- offsets যেমন "+02:00" থেকে জোন অনুমান করবেন না — offsets এ DST নিয়ম থাকে না।
উদাহরণ: একটি হাসপাতালের শিফট "সোম-শুক্র 09:00-17:00 America/New_York"। DST পরিবর্তনের সপ্তাহে শিফট লোকালি এখনো 9 থেকে 5 থাকবে, যদিও UTC ইনস্ট্যান্টগুলো এক ঘণ্টা সরে যাবে।
PostgreSQL-এ গুরুত্বপূর্ন টাইপগুলো (এবং কী এড়ানো উচিত)
অধিকাংশ ক্যালেন্ডার বাগ শুরু হয় একটি ভুল কলাম টাইপ থেকে। মূল লক্ষ্য হল একটি বাস্তব মুহূর্তকে ওয়াল-ক্লক প্রত্যাশা থেকে আলাদা করা।
বাস্তব ইনস্ট্যান্টগুলোর জন্য timestamptz ব্যবহার করুন: বুকিং, ক্লক-ইন, নোটিফিকেশন এবং এমন কিছু যেটা ব্যবহারকারী বা অঞ্চলের উপর ক্রস-তুলনা করা হয়। PostgreSQL এটি একটি অ্যাবসলুট ইনস্ট্যান্ট হিসেবে সংরক্ষণ করে এবং প্রদর্শনের জন্য রূপান্তর করে, ফলে অর্ডার এবং ওভারল্যাপ চেক ঠিকভাবে কাজ করে।
লোকাল ওয়াল-ক্লক মানগুলোর জন্য timestamp without time zone ব্যবহার করুন, যেমন "প্রতিটি সোমবার 09:00" বা "দোকান খোলে 10:00"। এটি একটি টাইম জোন আইডির সাথে জোড়ান, এবং occurrences তৈরি করার সময়ই বাস্তব ইনস্ট্যান্টে রূপান্তর করবেন।
পুনরাবৃত্ত প্যাটার্নের জন্য মৌলিক টাইপগুলো সহায়তা করে:
dateদিনের-শুধু ব্যতিক্রম (ছুটির দিন) জন্যtimeদৈনন্দিন শুরুর সময়ের জন্যintervalমেয়াদ (যেমন 6 ঘন্টার শিফট) জন্য
টাইমজোনটি IANA নামে (উদাহরণ: America/New_York) text কলামে (বা ছোট লুকআপ টেবিলে) সংরক্ষণ করুন। -0500 মতো offsets যথেষ্ট নয় কারণ সেগুলোতে DST নিয়ম থাকে না।
অনেক অ্যাপের জন্য একটি ব্যবহারিক সেট:
- বুক করা অ্যাপয়েন্টমেন্টের start/end ইনস্ট্যান্টের জন্য
timestamptz - ব্যতিক্রম দিনের জন্য
date - পুনরাবৃত্ত লোকাল শুরুর সময়ের জন্য
time - ডিউরেশনের জন্য
interval - IANA টাইম জোন ID জন্য
text
বুকিং ও শিফট অ্যাপের জন্য ডেটা মডেল অপশনসমূহ
সেরা স্কিমা নির্ভর করে শিডিউল কত ঘন ঘন বদলায় এবং মানুষ কত দূর অগ্রিম ব্রাউজ করে। সাধারণত আপনি অনেক রো আগাম লিখবেন না কি রিড-টাইমে জেনারেট করবেন তা বেছে নিচ্ছেন।
অপশন A: প্রতিটি occurrence সংরক্ষণ করুন
প্রতি শিফট বা বুকিংয়ের জন্য (এক্সপ্যান্ড করা) একটি রো ইনসার্ট করুন। কুয়েরি করা সহজ এবং বোধগম্য। তদ্ব্যতীত অনেক লেখনী আর নিয়ম বদলে গেলে অনেক আপডেট করতে হবে।
এটি ভাল যখন ইভেন্টগুলো অধিকাংশই একক, অথবা যখন আপনি শুধুমাত্র সংক্ষিপ্ত আগাম (যেমন পরবর্তী ৩০ দিন) occurrence তৈরি করেন।
অপশন B: একটি রুল সংরক্ষণ করে রিড-টাইমে এক্সপ্যান্ড করুন
একটি শিডিউল রুল সংরক্ষণ করুন (যেমন "সপ্তাহে সোমবার ও বুধবার 09:00 America/New_York") এবং অন-ডিমান্ড অনুরোধকৃত রেঞ্জের জন্য occurrences জেনারেট করুন।
এটি নমনীয় এবং স্টোরেজ হালকা, কিন্তু কুয়েরি জটিল হয়ে যায়। মাস ভিউ ধীর হতে পারে যদি আপনি ক্যাশিং না করেন।
অপশন C: রুল প্লাস ক্যাশড occurrences (হাইব্রিড)
রুলকে সোর্স অফ ট্রুথ হিসেবে রাখুন, এবং একই সাথে একটি রোলিং উইন্ডোর জন্য তৈরি করা occurrences সংরক্ষণ করুন (উদাহরণ: 60-90 দিন)। রুল বদলালে ক্যাশ রিজেনারেট করুন।
এটি শিফট অ্যাপের জন্য শক্তিশালী ডিফল্ট: মাস ভিউ দ্রুত থাকে, কিন্তু প্যাটার্ন সম্পাদনা করার এক জায়গা থাকে।
প্র্যাকটিক্যাল টেবিল সেট:
- schedule: owner/resource, time zone, local start time, duration, recurrence rule
- occurrence: বিস্তৃত ইনস্ট্যান্সগুলো
start_at timestamptz,end_at timestamptz, এবং স্ট্যাটাসসহ - exception: "এই তারিখটি বাদ দিন" বা "এই তারিখটি আলাদা" চিন্হগুলো
- override: প্রতি-occurrence এডিট যেমন পরিবর্তিত শুরুর সময়, বদলে যাওয়া স্টাফ, বাতিল ফ্ল্যাগ
- (ঐচ্ছিক) schedule_cache_state: শেষ জেনারেট করা রেঞ্জ যাতে আপনি জানেন পরেরবার কি পূরণ করতে হবে
ক্যালেন্ডার রেঞ্জ কুয়েরিসের জন্য, "এই উইন্ডোতে সব দেখাও" ধাঁচে ইনডেক্স করুন:
- occurrence তে:
btree (resource_id, start_at)এবং প্রায়ইbtree (resource_id, end_at) - যদি আপনি প্রায়ই "overlaps range" কুয়েরি করেন: একটি সৃষ্ট
tstzrange(start_at, end_at)এবংgistইনডেক্স
পুনরাবৃত্তি নিয়মগুলো কিভাবে দুর্বল করা যাবে না এমনভাবে উপস্থাপন করবেন
রুলগুলো ভাঙ্গে যখন তা খুব চতুর, খুব নমনীয় বা অন অনুসন্ধানযোগ্য ব্লব হিসেবে সংরক্ষিত থাকে। একটি ভাল রুল ফরম্যাট এমন হওয়া উচিত যেটা আপনার অ্যাপ যাচাই করতে পারে এবং আপনার টিম দ্রুত ব্যাখ্যা করতে পারে।
দুইটি সাধারণ পদ্ধতি:
- আপনি যেসব প্যাটার্ন বাস্তবে সাপোর্ট করেন সেগুলোর জন্য সরল কাস্টম ফিল্ড।
- অনেক কম্বিনেশন ইমপোর্ট/এক্সপোর্ট করতে হলে iCalendar-সদৃশ RRULE।
একটি ব্যবহারিক মধ্য্মপথ: সীমিত সেটের অপশন অনুমোদন করে সেগুলো কলামে সংরক্ষণ করুন, এবং যেকোন RRULE স্ট্রিংকে শুধু বিনিময়ের উদ্দেশ্যে রাখুন।
উদাহরণস্বরূপ, একটি সাপ্তাহিক শিফট রুল নিচের ফিল্ডগুলোতে প্রকাশ করা যেতে পারে:
freq(daily/weekly/monthly) এবংinterval(প্রতি N)byweekday(0-6 অ্যারের মত অথবা বিটমাস্ক)- মাসিক নিয়মের জন্য ঐচ্ছিক
bymonthday(1-31) starts_at_local(যে লোকাল তারিখ+সময় ব্যবহারকারী বেছে নিয়েছে) এবংtzid- ঐচ্ছিক
until_dateঅথবাcount(উভয়ই সমর্থন করবেন না যদি প্রয়োজন না থাকে)
সীমার জন্য, duration (যেমন 8 ঘন্টা) সংরক্ষণ করা ভাল, প্রতিটি occurrence-এর জন্য আলাদা end timestamp রাখার বদলে। ক্লক সরে গেলে duration স্থিতিশীল থাকে। আপনি প্রতিটি occurrence-এর জন্য end time হিসাব করতে পারেন: occurrence start + duration।
রুল এক্সপ্যান্ড করার সময় নিরাপদ ও সীমিত রাখুন:
- শুধুমাত্র
window_startএবংwindow_endএর মধ্যে এক্সপ্যান্ড করুন। - ওভারনাইট ইভেন্টের জন্য একটি ছোট বাফার যোগ করুন (উদাহরণ: 1 দিন)।
- সর্বোচ্চ ইন্সট্যান্সের পরে থামুন (যেমন 500)।
- জেনারেট করার আগে প্রথমে প্রার্থী ফিল্টার করুন (
tzid,freq, এবং start date দ্বারা)।
ধাপে ধাপে: DST-নিরাপদ পুনরাবৃত্ত শিডিউল তৈরি করা
একটি নির্ভরযোগ্য প্যাটার্ন হল: প্রতিটি occurrence-কে প্রথমে একটি লোকাল ক্যালেন্ডার ধারণা হিসেবে বিবেচনা করুন (তারিখ + লোকাল সময় + লোকেশনের টাইমজোন), তারপর কেবল যখন আপনাকে সাজানো, কনফ্লিক্ট চেক বা প্রদর্শন করতে হবে তখনই ইনস্ট্যান্টে রূপান্তর করুন।
1) UTC অনুমান নয়, লোকাল ইচ্ছা সংরক্ষণ করুন
শিডিউলের লোকেশন টাইমজোন (IANA নাম যেমন America/New_York) এবং একটি লোকাল শুরুর সময় (উদাহরণ 09:00) সংরক্ষণ করুন। এই লোকাল সময় ব্যবসার উদ্দেশ্যই, এমনকি যখন DST বদলে যায়।
একটি ডিউরেশন এবং রুলের স্পষ্ট সীমাও সংরক্ষণ করুন: একটি start date, এবং অথবা একটি end date বা repeat count। সীমারেখা "অনন্ত এক্সপ্যানশন" বাগ প্রতিরোধ করে।
2) ব্যতিক্রম এবং ওভাররাইড আলাদাভাবে মডেল করুন
দুটো ছোট টেবিল ব্যবহার করুন: একটি স্কিপ-তারিখের জন্য, আরেকটি পরিবর্তিত occurrence-র জন্য। সেগুলোকে schedule_id + local_date দ্বারা কীগুলো করে রাখুন যাতে মূল recurrence পরিষ্কারভাবে ম্যাচ করা যায়।
একটি ব্যবহারিক কাঠামো দেখতে এরকম:
-- core schedule
-- tz is the location time zone
-- start_time is local wall-clock time
schedule(id, tz text, start_date date, end_date date, start_time time, duration_mins int, by_dow int[])
schedule_skip(schedule_id, local_date date)
schedule_override(schedule_id, local_date date, new_start_time time, new_duration_mins int)
3) শুধুমাত্র অনুরোধকৃত উইন্ডোর মধ্যে এক্সপ্যান্ড করুন
যে রেঞ্জটি আপনি রেন্ডার করছেন (সপ্তাহ, মাস) সেই রেঞ্জের জন্য প্রার্থী লোকাল তারিখগুলো জেনারেট করুন, তারপর দিন-অফ-উইক দ্বারা ফিল্টার করুন, এবং পরে স্কিপ ও ওভাররাইড প্রয়োগ করুন।
WITH days AS (
SELECT d::date AS local_date
FROM generate_series($1::date, $2::date, interval '1 day') d
), base AS (
SELECT s.id, s.tz, days.local_date,
make_timestamp(extract(year from days.local_date)::int,
extract(month from days.local_date)::int,
extract(day from days.local_date)::int,
extract(hour from s.start_time)::int,
extract(minute from s.start_time)::int, 0) AS local_start
FROM schedule s
JOIN days ON days.local_date BETWEEN s.start_date AND s.end_date
WHERE extract(dow from days.local_date)::int = ANY (s.by_dow)
)
SELECT b.id,
(b.local_start AT TIME ZONE b.tz) AS start_utc
FROM base b
LEFT JOIN schedule_skip sk
ON sk.schedule_id = b.id AND sk.local_date = b.local_date
WHERE sk.schedule_id IS NULL;
4) শেষে ভিউয়ারের জন্য রূপান্তর করুন
start_utc কে timestamptz হিসেবে রাখুন সাজানোর, কনফ্লিক্ট চেক এবং বুকিংয়ের জন্য। কেবল প্রদর্শনের সময় ভিউয়ারের টাইমজোনে রূপান্তর করুন। এতে DST-সম্পর্কিত অপ্রত্যাশিত ঘটনা এড়ানো যায় এবং ক্যালেন্ডার ভিউ স্থির থাকে।
সঠিক ক্যালেন্ডার ভিউ জেনারেট করার কুয়েরি প্যাটার্নসমূহ
একটি ক্যালেন্ডার স্ক্রিন সাধারণত একটি রেঞ্জ কুয়েরি: "আমাকে সব দেখাও from_ts এবং to_ts এর মধ্যে"। একটি নিরাপদ ধারা:
- শুধুমাত্র ঐ উইন্ডোর ভেতরের প্রার্থী এক্সপ্যান্ড করুন।
- ব্যতিক্রম/ওভাররাইড প্রয়োগ করুন।
- ফাইনাল রো আউটপুট করুন
start_atএবংend_atহিসেবেtimestamptz।
দৈনিক বা সাপ্তাহিক এক্সপানশন generate_series দিয়ে
সহজ সাপ্তাহিক রুলগুলোর জন্য (যেমন "প্রতিটি সোম-শুক্র 09:00 লোকাল"), প্রথমে লোকাল তারিখগুলো জেনারেট করুন শিডিউলের টাইমজোনে, তারপর প্রতিটি লোকাল তারিখ + লোকাল টাইমকে একটি ইনস্ট্যান্টে রূপান্তর করুন।
-- Inputs: :from_ts, :to_ts are timestamptz
-- rule.tz is an IANA zone like 'America/New_York'
WITH bounds AS (
SELECT
(:from_ts AT TIME ZONE rule.tz)::date AS from_local_date,
(:to_ts AT TIME ZONE rule.tz)::date AS to_local_date
FROM rule
WHERE rule.id = :rule_id
), days AS (
SELECT d::date AS local_date
FROM bounds, generate_series(from_local_date, to_local_date, interval '1 day') AS g(d)
)
SELECT
(local_date + rule.start_local_time) AT TIME ZONE rule.tz AS start_at,
(local_date + rule.end_local_time) AT TIME ZONE rule.tz AS end_at
FROM rule
JOIN days ON true
WHERE EXTRACT(ISODOW FROM local_date) = ANY(rule.by_isodow);
এটি ভালভাবে কাজ করে কারণ প্রতিটি occurrence-এ timestamptz-এ রূপান্তর হয়, তাই DST বদল সঠিক দিন অনুযায়ী প্রয়োগ করা হয়।
"nth weekday" বা কাস্টম ইন্টারভাল হলে রিকার্সিভ CTE
যখন রুলগুলো "nth weekday"-এর ওপর নির্ভর করে, ফাঁক থাকে, বা কাস্টম ইন্টারভাল থাকে, তখন একটি রিকার্সিভ CTE প্রতিবার পরের occurrence জেনারেট করতে পারে যতক্ষণ না এটি to_ts পার করে। রিকার্সনকে উইন্ডোর সঙ্গে অ্যাঙ্কর করে রাখুন যাতে এটি অনন্ত চালিত না হয়।
প্রার্থী রো পাওয়া গেলে, exception এবং cancellation প্রয়োগ করুন (rule_id, start_at) বা স্থানীয় কী (rule_id, local_date) দ্বারা জোয়াইনে। যদি ক্যানসেল রেকর্ড থাকে, রো বাদ দিন। ওভাররাইড থাকলে start_at/end_at কে ওভাররাইড মান দিয়ে প্রতিস্থাপন করুন।
কার্যক্ষমতা বিষয়ক প্যাটার্ন যা সবচেয়ে বেশি গুরুত্বপূর্ণ:
- প্রথমেই রেঞ্জ সীমাবদ্ধ করুন: আগে রুলগুলো ফিল্টার করুন, তারপর কেবল
[from_ts, to_ts)-এর মধ্যে এক্সপ্যান্ড করুন। - exception/override টেবিলগুলোকে
(rule_id, start_at)বা(rule_id, local_date)-এ ইনডেক্স করুন। - মাস ভিউয়ের জন্য বছরের ডাটা এক্সপ্যান্ড করা থেকে বিরত থাকুন।
- রুল বদলে গেলে ক্যাশ নিষ্ক্রিয় করতে পারলে কেবল তখনই expanded occurrences ক্যাশ করুন।
ব্যতিক্রম এবং ওভাররাইড পরিষ্কারভাবে হ্যান্ডল করা
পুনরাবৃত্ত শিডিউলগুলা তখনই ব্যবহারযোগ্য যখন আপনি তা সহজে ভাঙতে পারেন। বুকিং ও শিফট অ্যাপে, "সাধারণ" সপ্তাহ হলো বেস রুল, এবং বাকী সবকিছু ব্যতিক্রম: ছুটি, বাতিল, সরানো অ্যাপয়েন্টমেন্ট বা স্টাফ বদল। ব্যতিক্রম পরে লাগালে ক্যালেন্ডার ভিউ ড্রিফট করে এবং ডুপলিকেট তৈরি হয়।
তিনটি ধারণা আলাদা রাখুন:
- একটি বেস স্কিডিউল (পুনরাবৃত্ত রুল এবং তার টাইমজোন)
- স্কিপ (যে তারিখ বা ইনস্ট্যান্সগুলো ঘটবে না)
- ওভাররাইড (একটি occurrence আছে, কিন্তু বিবরণ বদলে গেছে)
একটি স্থির প্রাধান্য নিয়ম ব্যবহার করুন
একটি ক্রম নির্বাচন করুন এবং তা কনসিস্টেন্ট রাখুন। একটি সাধারণ পছন্দ:
- বেস recurrence থেকে প্রার্থীরা জেনারেট করুন।
- ওভাররাইড প্রয়োগ করুন (জেনারেট হওয়া রো প্রতিস্থাপন করুন)।
- স্কিপ প্রয়োগ করুন (গোপন করুন)।
নিয়মটি এক বাক্যে ব্যবহারকারীদের কাছে সহজে বোঝানো সম্ভব কিনা নিশ্চিত করুন।
যখন ওভাররাইড একটি ইনস্ট্যান্স প্রতিস্থাপন করে তখন ডুপ্লিকেট এড়ান
ডুপ্লিকেট সাধারণত তখন হয় যখন কুয়েরি উভয়: জেনারেটেড occurrence এবং ওভাররাইড রো ফিরিয়ে দেয়। এটা প্রতিরোধ করুন একটি স্থিতিশীল কী দিয়ে:
- প্রতিটি জেনারেটেড ইনস্ট্যান্সকে একটি স্থিতিশীল কী দিন, যেমন
(schedule_id, local_date, start_time, tzid)। - ওভাররাইড রোতে সেটি "original occurrence key" হিসেবে স্টোর করুন।
- একটি unique constraint যোগ করুন যাতে প্রতিটি বেস occurrence-র জন্য শুধু একটি ওভাররাইড থাকতে পারে।
তারপর কুয়েরিতে, মিল আছে এমন জেনারেটেড occurrence বাদ দিন এবং ওভাররাইড রো ইউনিয়ন করে যুক্ত করুন।
অডিটযোগ্যতা যোগ করুন কিন্তু ঝামেলা বাড়াবেন না
ব্যতিক্রমগুলোই বিতর্কের জায়গা ("কে আমার শিফট বদলালো?")। স্কিপ ও ওভাররাইডে বেসিক অডিট ফিল্ড যোগ করুন: created_by, created_at, updated_by, updated_at, এবং একটি ঐচ্ছিক কারণ।
এক-ঘন্টার দূরতত্রুটি ঘটানোর সাধারণ ভুলগুলো
অধিকাংশ এক-ঘন্টার বাগ আসে দুই ধরনের সময়ের মানে মিশে যাওয়ার ফলে: একটি ইনস্ট্যান্ট (UTC টাইমলাইনে একটি বিন্দু) এবং একটি লোকাল ক্লক রিডিং (যেমন নিউইর্কে প্রতিটি সোমবার 09:00)।
একটি ক্লাসিক ভুল হল একটি লোকাল ওয়াল-ক্লক রুল timestamptz হিসেবে সংরক্ষণ করা। আপনি যদি "সোমবার 09:00 America/New_York" একটি timestamptz হিসেবে সংরক্ষণ করেন, আপনি ইতোমধ্যেই একটি নির্দিষ্ট তারিখ (এবং DST অবস্থা) বেছে নিয়েছেন। ভবিষ্যত সোমগুলো জেনারেট করার সময় আসল উদ্দেশ্য ("সবসময় লোকালি 09:00") চলে যায়।
আরেকটি সাধারণ কারণ হল স্থির UTC offsets (-05:00) নির্ভর করা। offsets-এ DST নিয়ম থাকে না। America/New_York এর মত IANA zone ID সংরক্ষণ করুন এবং PostgreSQL-কে প্রতিটি তারিখে সঠিক নিয়ম প্রয়োগ করতে দিন।
কখন রূপান্তর করবেন সে ব্যাপারে সতর্ক থাকুন। যদি আপনি এক্সপ্যান্সন করার সময় খুব দ্রুত UTC তে রূপান্তর করেন, আপনি একটি DST অফসেট স্থির করে সেটি প্রতিটি occurrence-তে প্রয়োগ করতে পারেন। নিরাপদ প্যাটার্ন হল: occurrences লোকাল শব্দে (তারিখ + লোকাল সময় + জোন) জেনারেট করুন, তারপর প্রতিটি occurrence আলাদা করে একটি ইনস্ট্যান্টে রূপান্তর করুন।
ঘটনাগুলো যা বারংবার দেখা যায়:
- পুনরাবৃত্ত লোকাল টাইম-অফ-ডে
timestamptzহিসেবে সংরক্ষণ করা (আপনিtime+tzid+ একটি রুল দরকার ছিল)। - শুধু একটি অফসেট সংরক্ষণ করা, IANA জোন না রাখা।
- রূপান্তর এক্সপ্যানশন করার সময় করা বরং শেষে না করা।
- "চিরদিন" রুল এক্সপ্যান্ড করা বিনা কঠোর টাইম উইন্ডো ছাড়া।
- DST শুরু ও DST শেষ সপ্তাহগুলো টেস্ট না করা।
একটি সরল টেস্ট যা অধিকাংশ সমস্যা ধরবে: একটি DST-যুক্ত জোন নিন, একটি সাপ্তাহিক 09:00 শিফট তৈরি করুন, এবং এমন দুটি মাসের ক্যালেন্ডার রেন্ডার করুন যা DST পরিবর্তন ক্রস করে। নিশ্চিত করুন প্রতিটি ইনস্ট্যান্স লোকালি 09:00 দেখায়, যদিও আন্ডারলাইং UTC ইনস্ট্যান্টগুলো ভিন্ন।
চালানোর আগে দ্রুত চেকলিস্ট
রিলিজের আগে মৌলিক বিষয়গুলো পরীক্ষা করুন:
- প্রতিটি শিডিউল একটি স্থানের (বা বিজনেস ইউনিটের) সাথে যুক্ত এবং তাতে একটি নামকৃত টাইমজোন স্টোর করা আছে।
- আপনি IANA জোন ID (যেমন
America/New_York) সংরক্ষণ করছেন, কাঁচা অফসেট নয়। - পুনরাবৃত্তি এক্সপ্যানশন শুধুমাত্র অনুরোধকৃত রেঞ্জের মধ্যে ঘটে।
- ব্যতিক্রম ও ওভাররাইডগুলোর একটি একক, ডকুমেন্টেড প্রাধান্য ক্রমানুসারে আছে।
- আপনি DST পরিবর্তন সপ্তাহগুলো এবং শিডিউল থেকে ভিন্ন টাইমজোনের একজন ভিউয়ারকে টেস্ট করেছেন।
একটি বাস্তবসম্মত ড্রাই রান করুন: একটি দোকান Europe/Berlin-এ প্রতি সপ্তাহে 09:00 লোকাল শিফট আছে। একটি ম্যানেজার America/Los_Angeles থেকে দেখে। নিশ্চিত করুন শিফট প্রতিটি সপ্তাহে Berlin টাইমে 09:00 থাকে, এমনকি যখন প্রতিটি অঞ্চল ভিন্ন সময়ে DST পরিবর্তন করে।
উদাহরণ: সাপ্তাহিক স্টাফ শিফট এক ছুটি ও DST পরিবর্তনসহ
একটি ছোট ক্লিনিক একটি পুনরাবৃত্ত শিফট চালায়: প্রতিটি সোমবার, 09:00 থেকে 17:00 ক্লিনিকের লোকাল টাইমে (America/New_York)। ক্লিনিক এক বিশেষ সোমবার বন্ধ আছে (ছুটি)। একজন স্টাফ দুই সপ্তাহ ইউরোপে ভ্রমণ করছে, কিন্তু ক্লিনিকের শিডিউল কর্মচারীর বর্তমান অবস্থানের উপর নয়, ক্লিনিকের ওয়াল-ঘড়ির উপর অ্যাঙ্কর করা থাকা উচিত।
এটি সঠিকভাবে কাজ করানোর জন্য:
- লোকাল তারিখে অ্যাঙ্কর করা একটি রিকারেন্স রুল সংরক্ষণ করুন (weekday = Monday, লোকাল সময় = 09:00-17:00)
- শিডিউলের টাইমজোন সংরক্ষণ করুন (
America/New_York) - রুলকে একটি কার্যকর start date দিন যাতে স্পষ্ট অ্যাঙ্কর থাকে
- একটি ব্যতিক্রম সংরক্ষণ করুন যা সেই ছুটি সোমবার বাতিল করে (এবং এক-বারের জন্য পরিবর্তনগুলোর জন্য ওভাররাইড)
এখন একটি দুই সপ্তাহের ক্যালেন্ডার রেঞ্জ রেন্ডার করুন যা New York-এ একটি DST পরিবর্তন অন্তর্ভুক্ত করে। কুয়েরি ঐ লোকাল তারিখ রেঞ্জে সোমবারগুলো জেনারেট করে, ক্লিনিকের লোকাল সময় যুক্ত করে, তারপর প্রতিটি occurrence-কে একটি অ্যাবসলুট ইনস্ট্যান্ট (timestamptz) এ রূপান্তর করে। যেহেতু রূপান্তর প্রতিটি occurrence-এ আলাদা করে করা হচ্ছে, DST সঠিকভাবে হ্যান্ডল করা হয়।
ভিন্ন ভিউয়াররা একই ইনস্ট্যান্টের জন্য ভিন্ন লোকাল ক্লক টাইম দেখবে:
- Los Angeles-এ থাকা ম্যানেজারটি ঘড়িতে এটি আগে দেখবে।
- Berlin-এ ভ্রমণরত স্টাফ এটি পরে দেখবে।
ক্লিনিক যা চেয়েছিল সেটা বজায় থাকে: প্রতিটি বাতিল না করা সোমবারে New York টাইমে 09:00 থেকে 17:00।
পরবর্তী ধাপ: বাস্তবায়ন, টেস্ট, এবং রক্ষণযোগ্যতা বজায় রাখুন
আপনার টাইম আপ্রোচ প্রথমে তালাবদ্ধ করুন: আপনি কেবল রুল সংরক্ষণ করবেন, কেবল occurrence সংরক্ষণ করবেন, না কি হাইব্রিড ব্যবহার করবেন? অনেক বুকিং ও শিফট প্রোডাক্টের জন্য একটি হাইব্রিড ভালো: রুলকে সোর্স অব ট্রুথ রাখুন, প্রয়োজন হলে রোলিং ক্যাশ রাখুন, এবং ব্যতিক্রম ও ওভাররাইডগুলো বাস্তব রো হিসেবে সংরক্ষণ করুন।
আপনার "টাইম কনট্রাক্ট" এক জায়গায় লিখে রাখুন: কী ইনস্ট্যান্ট হিসেবে গণ্য, কী লোকাল ওয়াল টাইম হিসেবে গণ্য, এবং কোন কলাম কোনটি সংরক্ষণ করে। এটি প্রতিরোধ করে যে কোনো এক এন্ডপয়েন্ট লোকাল সময় ফিরায় আর অন্যটি UTC।
রিকারেন্স জেনারেশনকে একটি মডিউল রাখুন, ছড়ানো SQL টুকরো না। যদি কখনো আপনি "লোকালি 9:00 AM" কিভাবে ইন্টারপ্রেট করবেন তা বদলান, তখন একটি জায়গা আপডেট করলেই হবে।
যদি আপনি সবকিছুকে নিজে কোড না করে কোনো scheduling টুল বানাচ্ছেন, AppMaster (appmaster.io) এই ধরনের কাজের জন্য বাস্তবসম্মত। আপনি Data Designer-এ ডাটাবেস মডেল করতে পারবেন, বিজনেস প্রসেসে রিকারেন্স ও ব্যতিক্রম লজিক বানাতে পারবেন, এবং তবুও শেষ পর্যন্ত বাস্তব জেনারেটেড ব্যাকএন্ড ও অ্যাপ কোড পেতে পারবেন।


