Skip to content

time: propagate NULL instead of raising - #169

Merged
nalgeon merged 7 commits into
nalgeon:mainfrom
RogerHYang:time-propagate-null
Sep 6, 2026
Merged

nalgeon merged 7 commits into
nalgeon:mainfrom
RogerHYang:time-propagate-null

Conversation

@RogerHYang

@RogerHYang RogerHYang commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

All argument-taking time_* functions return NULL if any argument is NULL, matching what time_parse already did. NULL is detected before type validation, so it takes precedence over an invalid argument in another position: both time_add(NULL, 'bad') and time_add('bad', NULL) return NULL. When no argument is NULL, results and type errors are unchanged.

Why

The functions were unusable over nullable expressions:

select time_fmt_date(l.at) from users u left join logins l on l.user_id = u.id;
-- Error: 1st parameter: should be a time blob

Wrapping the result in coalesce cannot catch the error, and filtering on the result still evaluates it. CASE works, but has to be repeated at every call site.

Indexes have no workaround. create index on events(time_get_year(at)) fails if the table holds a NULL row, and once created it rejects later NULL inserts, so an index added for performance refuses data.

Compatibility

No previously successful call changes. Calls containing NULL that previously raised now return NULL.

Schema-level behavior does change. CHECK constraints, generated columns, and expression indexes accept rows containing NULL where evaluation previously aborted the write:

create table events(at blob check (time_get_year(at) >= 2000));
insert into events values (NULL);   -- raised before

I think that is right for a nullable column. The CHECK says present values must satisfy the predicate, while the old behavior also effectively rejected NULL, which check (x >= 2000) on a nullable integer never did. NOT NULL remains the way to require a value.

Argument-taking time functions validated types with a positive gate and
rejected everything else, so a NULL argument raised an error rather than
yielding NULL. Only time_parse propagated. Unguarded calls over
null-extended expressions therefore aborted the statement: outer joins,
aggregates, ORDER BY, and expression indexes on a nullable column.

Check every argument for NULL ahead of the type gate. The scan precedes
type validation, so the result does not depend on argument order:
time_add(NULL, 'bad') and time_add('bad', NULL) both return NULL. Invalid
non-NULL arguments still error.

A CHECK constraint is satisfied by a NULL result, so
check (time_get_year(at) >= 2000) no longer rejects a NULL value; NOT NULL
requires one. Generated columns and expression indexes likewise accept rows
they previously refused.
Comment thread docs/time.md Outdated
Comment thread docs/time.md Outdated
Comment thread src/time/extension.c Outdated
Comment thread src/time/extension.c
Comment thread docs/time.md Outdated
@nalgeon
nalgeon merged commit 75d35f6 into nalgeon:main Sep 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants