jed

Types

The type system is the product. Columns are strictly, statically typed — a value is never silently reinterpreted at runtime. Where the SQL raises a question of semantics, jed follows PostgreSQL closely.

Every example below is a live database in your browser. Edit the SQL and run it.

Exact decimals

numeric / decimal is exact base-10, not binary floating point. So 0.1 + 0.2 is exactly 0.3, 1.50 equals 1.5 by value, and a value keeps its display scale.

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

Integers with defined overflow

Integers are fixed-width (i16, i32, i64) and trap on overflow — there is no silent wraparound. Adding 1 to the largest i16 raises error 22003:

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

Boolean ⇄ integer casts

Following PostgreSQL, boolean casts to and from int (i32) only, and always with an explicit CAST / :: — never implicitly. true becomes 1 and false becomes 0; an integer becomes false when it is 0 and true for any nonzero value (including negatives):

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

Only i32 is involved: a boolean ⇄ i16 or boolean ⇄ i64 cast is rejected (42804), matching PostgreSQL’s choice of int4 as the sole boolean-integer cast.

Parsing text into numbers and booleans

An explicit CAST / :: parses a text value into a number or boolean at runtimei16 / i32 / i64, decimal (with a numeric(p,s) re-scale), f32 / f64, and boolean. The string is trimmed, a leading sign is accepted, and a boolean follows PostgreSQL’s spellings (t/true/yes/on/1 …). A malformed string raises 22P02 and an out-of-range integer raises 22003:

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

The type must be named — a bare string never silently becomes a number, so int_col = '42' stays a type error (42804). jed uses its own literal grammar, so hex, digit underscores, and NaN are rejected (22P02) where PostgreSQL accepts them. (Parsing a string into a date / timestamp / interval / bytea is a separate feature — use that type’s literal form, e.g. date '2024-01-15'.)

varchar(n) length limits

varchar(n) (and the jed alias string(n)) is text with a maximum length, counted in Unicode code points — so 'café' (4 code points) fits varchar(4). An explicit CAST / :: silently truncates to n, exactly like PostgreSQL, so 'hello world'::varchar(5) is 'hello':

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

Storing into a varchar(n) column is stricter: an over-length value raises 22001 (string_data_right_truncation), unless the excess characters are all spaces — then it is truncated to n (the SQL-standard trailing-space rule). UPDATE re-checks the length the same way. The limit lives on the value, not the type: a varchar(4) and an unbounded text are the same type underneath, with the same collation, comparison, and key encoding.

UUID ⇄ text and bytea casts

A uuid casts to and from text and bytea, always with an explicit CAST / ::. text → uuid parses PostgreSQL-flexibly (braces, hyphens after any byte pair, a bare 32-hex run, any case), and uuid → text always renders the canonical lowercase 8-4-4-4-12 form. Because a UUID is exactly 16 bytes, it also casts to and from bytea (those 16 raw bytes); bytea → uuid requires exactly 16 bytes and raises 22P02 otherwise:

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

text → uuid matches PostgreSQL. The other three are deliberately stricter or additional: jed makes uuid → text explicit-only (PostgreSQL assignment-casts any type to text), and the byteauuid casts are a jed convenience PostgreSQL does not offer at all.

Array casts

An array casts three ways, all with an explicit CAST / ::. array → text renders the {…} form (array_out); text → T[] parses the {…} form per row (array_in); and an array → array of a different element type casts each element through the scalar cast, preserving the array’s shape (its dimensions, lengths, and lower bounds):

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

The element cast follows the scalar matrix exactly — so widening (i32[] → i64[]), numeric[] → i32[] (rounding half away from zero), and text[] → i32[] all work, while an element pair with no scalar cast is a type error (42804). Like uuid/json → text, array → text is explicit-only: an array never silently lands in a text column.

Three-valued NULL logic

Comparisons with NULL yield NULL (unknown), not false — three-valued logic, as in PostgreSQL. Note that NULL = NULL is NULL, while NULL IS NULL is true:

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

Range types

A range is a structural type over a scalar element — PostgreSQL’s six built-in ranges (int4range/int8range/numrange/tsrange/tstzrange/daterange; jed also spells the integer ones i32range/i64range). A range carries inclusive/exclusive ([1,5)) and unbounded ((,5)) endpoints and a distinguished empty; discrete ranges are stored in the canonical [) form.

Construct one with a literal cast ('[1,5)'::int4range) or a constructor (int4range(1, 10)), test it with the boolean operators (@> contains, && overlaps, <</>> strictly left/right, -|- adjacent), and combine ranges with the set operators — + (union), * (intersection), - (difference), and range_merge. A union of non-adjacent ranges raises 22000; range_merge spans the gap instead:

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

Time zones

timestamptz stores a UTC instant; a time zone is an I/O-time interpretation, never part of the stored value or its order (PostgreSQL’s model). The AT TIME ZONE operator converts both ways — timestamptz AT TIME ZONE zone renders the instant as the local wall clock in zone, and timestamp AT TIME ZONE zone interprets a wall clock as being in zone and gives back the UTC instant. UTC and fixed offsets like +05:30 are built in (note the POSIX sign: '+05:30' means UTC−5:30, matching PostgreSQL):

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

Named IANA zones (America/New_York, Europe/Paris, …) come from a time-zone database the host loads as bytes — the engine ships none by itself, so a query can only ever use an already-loaded zone (an unknown zone raises 22023). This is the same host-loaded-data model jed uses for Unicode collation; see the design notes for the JTZ bundle and the db.loadTimeZoneData host call.

Truncating, extracting, and converting

date_trunc(unit, value) rounds a timestamp / timestamptz / interval down to a unit (hour, day, week, month, quarter, year, …); EXTRACT(field FROM value) pulls a single field out as exact numeric (year, dow, epoch, doy, ISO week, …); and the timestamp / timestamptz / date types cast across each other. For a timestamptz, both date_trunc and EXTRACT (and the casts) decompose the instant in the session time zone — the panel below runs in the default UTC session, and date_trunc(unit, timestamptz, zone) takes an explicit zone:

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

(date_part is deferred — it returns double precision, and jed has no binary float type — as are the text↔datetime casts; cast a string with the timestamp '…' / date '…' literal form instead.)

Date arithmetic

A date does calendar arithmetic, matching PostgreSQL. Adding or subtracting an integer shifts the day count and stays a date; subtracting one date from another gives the number of days between as an i32; and adding or subtracting an interval widens the date to midnight and returns a timestamp (month steps clamp the day-of-month, so Jan 31 + 1 month is Feb 29 in a leap year). The ±infinity dates absorb any shift, and an out-of-range result raises 22008:

Ctrl/⌘ + Enter to run · in-memory

No results yet. Run a query to see output.

See the full type reference for every scalar type and its range.

jed — an embeddable, strictly-typed SQL database.