When most people think about databases, they think about storing business transactions, customer activity, or financial records that span days, years, or maybe a few decades. But what if your data needs to reach back centuries… or even millennia?
PostgreSQL (and therefore YugabyteDB) supports the date type from 4713 BC through 5,874,897 AD. That lower bound is tied to the Julian Day Number (JDN) system, a standard used in astronomy. Try to go earlier, and you’ll hit this error:
yugabyte=# SELECT '4714-01-01 BC'::DATE;
ERROR: date out of range: "4714-01-01 BC"
LINE 1: SELECT '4714-01-01 BC'::DATE;
^
So what if you need to work with truly ancient timelines, whether for history, archaeology, astronomy, or just curiosity?
The good news: with a little modeling trick, you can extend YugabyteDB to handle dates earlier than 4713 BC, while still exposing them in a way that feels natural to query.
Why This Matters (And a Fun History Note)
Long before digital ledgers, humans were already keeping records. One of the earliest known business transactions dates to around 2570 BC, when King En-hegal of Lagash recorded the purchase of land on a clay tablet. And even earlier, during the Uruk period (~3200 BC), tablets bearing the name Kushim show formal accounting for barley trades…. arguably the first “accountant” in history.
Being able to store dates that far back may never show up in your e-commerce workload, but it’s pretty cool to know YugabyteDB can be stretched to cover the earliest ledgers ever recorded.
The Extended-Dates Pattern
The idea is simple: instead of relying on the built-in date type, we store year, month, and day as integers, with a few extras to make it usable:
●
year,month,daycolumns: stored using astronomical year numbering (… -2, -1, 0, 1, 2 …, where year 0 = 1 BC).● Validation check: ensure month/day are valid, leap years accounted for.
● Julian Day Number (JDN): a generated column that converts the Y/M/D into a single monotonic integer, making it easy to sort and query ranges.
● Pretty AD/BC formatter: generate a text version like
04714-01-01 BCfor readability.● Optional view: expose the data so it looks and feels like a native
datecolumn for modern ranges.
Step 1: Helper Functions
Here’s a bundle of SQL functions you can drop straight into YugabyteDB to validate dates, calculate Julian Day Numbers, and render pretty BC/AD text.
-- Leap year check
CREATE OR REPLACE FUNCTION ext_is_leap_year(y integer)
RETURNS boolean LANGUAGE sql IMMUTABLE AS $$
SELECT (y % 400 = 0) OR ((y % 4 = 0) AND (y % 100 <> 0));
$$;
-- Days in month
CREATE OR REPLACE FUNCTION ext_days_in_month(y integer, m integer)
RETURNS integer LANGUAGE sql IMMUTABLE AS $$
SELECT CASE m
WHEN 1 THEN 31
WHEN 2 THEN CASE WHEN ext_is_leap_year(y) THEN 29 ELSE 28 END
WHEN 3 THEN 31
WHEN 4 THEN 30
WHEN 5 THEN 31
WHEN 6 THEN 30
WHEN 7 THEN 31
WHEN 8 THEN 31
WHEN 9 THEN 30
WHEN 10 THEN 31
WHEN 11 THEN 30
WHEN 12 THEN 31
END;
$$;
-- Validate Y/M/D
CREATE OR REPLACE FUNCTION ext_valid_ymd(y integer, m integer, d integer)
RETURNS boolean LANGUAGE sql IMMUTABLE AS $$
SELECT m BETWEEN 1 AND 12
AND d BETWEEN 1 AND ext_days_in_month(y, m);
$$;
-- Julian Day Number for astronomical years
CREATE OR REPLACE FUNCTION ext_jdn_gregorian(y integer, m integer, d integer)
RETURNS bigint LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE
a int; yp int; mp int; jdn bigint;
BEGIN
IF NOT ext_valid_ymd(y,m,d) THEN
RAISE EXCEPTION 'Invalid date parts: %-%-%', y,m,d;
END IF;
a := (14 - m) / 12;
yp := y + 4800 - a;
mp := m + 12*a - 3;
jdn := d + ((153*mp + 2)/5)
+ 365*yp + (yp/4) - (yp/100) + (yp/400)
- 32045;
RETURN jdn;
END;
$$;
-- Pretty AD/BC format
CREATE OR REPLACE FUNCTION ext_fmt_ad_bc(y int, m int, d int)
RETURNS text LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE
era text; y_abs int;
BEGIN
IF y >= 1 THEN
era := 'AD'; y_abs := y;
ELSE
era := 'BC'; y_abs := 1 - y; -- 0 -> 1 BC, -1 -> 2 BC, etc.
END IF;
RETURN lpad(y_abs::text,4,'0') || '-' ||
lpad(m::text,2,'0') || '-' ||
lpad(d::text,2,'0') || ' ' || era;
END;
$$;
-- Parser for 'YYYY-MM-DD' + optional 'BC'
CREATE OR REPLACE FUNCTION ext_parse_text_date(t text)
RETURNS TABLE (y integer, m integer, d integer)
LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE
s text := trim(t);
is_bc boolean := false;
core text;
parts text[];
BEGIN
IF s ~* '\s*B\s*C\s*$' THEN
is_bc := true;
core := regexp_replace(s, '\s*B\s*C\s*$', '', 'i');
ELSE
core := s;
END IF;
parts := regexp_matches(core, '^\s*([+-]?\d+)-(\d{1,2})-(\d{1,2})\s*$', 'i');
IF parts IS NULL THEN
RAISE EXCEPTION 'Invalid date literal: %', t;
END IF;
y := parts[1]::int;
m := parts[2]::int;
d := parts[3]::int;
IF is_bc THEN y := 1 - y; END IF;
RETURN NEXT;
END;
$$;
Step 2: Extended Dates Table
CREATE TABLE extended_dates (
id bigserial PRIMARY KEY,
title text NOT NULL,
year int NOT NULL,
month int NOT NULL,
day int NOT NULL,
jdn bigint GENERATED ALWAYS AS (ext_jdn_gregorian(year,month,day)) STORED,
pretty_adbc text GENERATED ALWAYS AS (ext_fmt_ad_bc(year,month,day)) STORED,
CONSTRAINT extended_dates_valid CHECK (ext_valid_ymd(year,month,day))
);
Step 3: Insert Helper
CREATE OR REPLACE FUNCTION extended_dates_insert(p_title text, p_text_date text)
RETURNS bigint LANGUAGE plpgsql AS $$
DECLARE yy int; mm int; dd int; new_id bigint;
BEGIN
SELECT y,m,d INTO yy,mm,dd FROM ext_parse_text_date(p_text_date);
INSERT INTO extended_dates(title,year,month,day)
VALUES (p_title,yy,mm,dd)
RETURNING id INTO new_id;
RETURN new_id;
END;
$$;
Step 4: View That Feels Like date
CREATE VIEW v_extended_dates AS
SELECT
id,
title,
CASE
WHEN year >= -4712 THEN make_date(CASE WHEN year <= 0 THEN 1 ELSE year END, month, day)::date
ELSE NULL
END AS as_date,
pretty_adbc,
jdn
FROM extended_dates;
Step 5: Examples
Insert some milestones:
SELECT extended_dates_insert('Earliest known land deal', '04714-01-01 BC');
SELECT extended_dates_insert('Caesar assassination', '0044-03-15 BC');
SELECT extended_dates_insert('First millennium marker', '01000-01-01');
SELECT extended_dates_insert('Modern record', '2025-09-08');
Query them naturally:
SELECT id, title, COALESCE(as_date::text, pretty_adbc) AS display_date
FROM v_extended_dates
ORDER BY jdn;
What does that look like?
yugabyte=# SELECT extended_dates_insert('Earliest known land deal', '04714-01-01 BC');
extended_dates_insert
-----------------------
1
(1 row)
yugabyte=# SELECT extended_dates_insert('Caesar assassination', '0044-03-15 BC');
extended_dates_insert
-----------------------
2
(1 row)
yugabyte=# SELECT extended_dates_insert('First millennium marker', '01000-01-01');
extended_dates_insert
-----------------------
3
(1 row)
yugabyte=# SELECT extended_dates_insert('Modern record', '2025-09-08');
extended_dates_insert
-----------------------
4
(1 row)
yugabyte=# SELECT id, title, COALESCE(as_date::text, pretty_adbc) AS display_date
yugabyte-# FROM v_extended_dates
yugabyte-# ORDER BY jdn;
id | title | display_date
----+--------------------------+---------------
1 | Earliest known land deal | 4714-01-01 BC
2 | Caesar assassination | 0001-03-15
3 | First millennium marker | 1000-01-01
4 | Modern record | 2025-09-08
(4 rows)
Wrapping Up
● PostgreSQL/YugabyteDB
datestarts at 4713 BC.● With the Extended-Dates Pattern (year, month, day + JDN + formatter), you can model dates far earlier.
● A view makes it feel like a native
date.● While not a daily requirement for modern apps, it’s a fun and practical way to stretch YugabyteDB into the deep past… back to the first business ledgers ever recorded.
Have Fun!
