Replace the age example in the README and document datetime columns

Use the release year from Neo4j's Movies dataset for the typed column
example, and show how to turn a Unix timestamp and an ISO 8601 string
into timestamp and timestamptz generated columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Bart BroereandClaude Opus 5.5 committed 2026-10-21 14:12:07 +02:00
1 parent ee980b27fe
commit ad2dde4d2f
1 file changed
+46 -8
+46 -8
View File
@@ -89,25 +89,63 @@ returns a property as text. For typed columns, add them yourself, either as a vi
as a generated column:
```sql
CREATE VIEW neo4j.people AS
SELECT id, properties ->> 'name'::text AS name, (properties ->> 'age'::text)::bigint AS age
FROM neo4j."Person";
CREATE VIEW neo4j.movies AS
SELECT id, properties ->> 'title'::text AS title, (properties ->> 'released'::text)::integer AS released
FROM neo4j."Movie";
ALTER TABLE neo4j."Person"
ADD COLUMN age bigint GENERATED ALWAYS AS ((properties ->> 'age'::text)::bigint) STORED;
ALTER TABLE neo4j."Movie"
ADD COLUMN released integer GENERATED ALWAYS AS ((properties ->> 'released'::text)::integer) STORED;
```
PostgreSQL keeps generated columns up to date on every Cypher `CREATE`, `MERGE`, `SET`
and `REMOVE`, and they can be indexed. Some things to keep in mind:
- A value that does not fit the column's type makes the Cypher write fail: with the
column above, `CREATE (:Person {age: 'old'})` is rejected. A view only fails when it
column above, `CREATE (:Movie {released: 'soon'})` is rejected. A view only fails when it
is read.
- AGE does not allow dropping columns from its label tables, so a generated column
cannot be removed again.
- Adding a stored generated column rewrites the table and locks it while doing so.
- AGE has no date or time types, so store those as ISO 8601 strings and cast them in
SQL.
### Dates and times
AGE has no date or time types, so store a point in time either as a Unix timestamp
(seconds since 1970-01-01 UTC) or as an ISO 8601 string, and convert it in SQL. A Unix
timestamp converts with the built-in `to_timestamp`, here to a `timestamp` in UTC:
```sql
ALTER TABLE neo4j."Movie"
ADD COLUMN updated timestamp GENERATED ALWAYS AS
(to_timestamp((properties ->> 'updated'::text)::bigint) AT TIME ZONE 'UTC') STORED;
```
A generated column only accepts immutable functions, and casting text to `timestamptz`
is not immutable, because the result depends on the `TimeZone` and `DateStyle`
settings. A view can use the cast as is, but a generated column needs a function that
only accepts strings with a UTC offset and parses them the same way whatever the
session settings are:
```sql
CREATE FUNCTION public.iso8601_timestamptz(value text) RETURNS timestamptz
LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE
SET TimeZone = 'UTC'
AS $$
BEGIN
IF value !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}(:?\d{2})?)$' THEN
RAISE EXCEPTION 'not an ISO 8601 timestamp with a UTC offset: %', value;
END IF;
RETURN value::timestamptz;
END
$$;
ALTER TABLE neo4j."Movie"
ADD COLUMN premiere timestamptz GENERATED ALWAYS AS
(public.iso8601_timestamptz(properties ->> 'premiere'::text)) STORED;
```
With these columns, `CREATE (:Movie {title: 'The Matrix', updated: 1729500000,
premiere: '1999-03-24T19:00:00-08:00'})` fills in both, and a `premiere` without an
offset, such as `'1999-03-24T19:00:00'`, makes the write fail.
## Container images