diff --git a/contents/docs/writing-data.mdx b/contents/docs/writing-data.mdx
index 10bf4e1..7b351b2 100644
--- a/contents/docs/writing-data.mdx
+++ b/contents/docs/writing-data.mdx
@@ -30,25 +30,37 @@ z.mutate.user.insert({
});
```
-Optional fields can be set to `null` to explicitly set the new field to `null`. They can also be set to `undefined` to take the default value (which is often `null` but can also be some generated value server-side).
+Nullable fields can be set to `null` to explicitly set the new field to `null`. They can also be set to `undefined` to take the default value (which is often `null` but can also be some generated value server-side).
+
+
+ Columns with an `onInsert()` function do not need to be specified and will be automatically populated if omitted (or set to `undefined`).
+
```tsx
// schema.ts
-import {createTableSchema} from '@rocicorp/zero';
-
-const userSchema = createTableSchema({
- tableName: 'user',
- columns: {
- id: {type: 'string'},
- name: {type: 'string'},
- language: {type: 'string', optional: true},
- },
- primaryKey: ['id'],
- relationships: {},
-});
+import {table, string, number} from '@rocicorp/zero';
+
+const user = table('user')
+ .columns({
+ id: string(),
+ username: string(),
+ language: string().nullable(),
+ createdAt: number().onInsert(() => Date.now()),
+ updatedAt: number()
+ .onInsert(() => Date.now())
+ .onUpdate(() => Date.now()),
+ })
+ .primaryKey('id');
// app.tsx
+// Columns with onInsert() defaults are automatically populated
+z.mutate.user.insert({
+ id: '1',
+ username: 'John Doe',
+});
+// { id: '1', username: 'John Doe', createdAt: 1743018158777, updatedAt: 1743018158777 }
+
// Sets language to `null` specifically
z.mutate.user.insert({
id: nanoid(),
@@ -83,16 +95,25 @@ z.mutate.user.upsert({
});
```
-`upsert` supports the same `null` / `undefined` semantics for optional fields that `insert` does (see above).
+`upsert` supports the same `null` / `undefined` semantics for nullable fields that `insert` does (see above).
+
+
+ For columns with only an `onUpdate()` default, you must include that column (with `undefined` or a value) so Zero can use the value if it uses an insert.
+ This is because Zero doesn't know whether it will be an insert or update.
+
## Update
Update an existing record. Does nothing if the specified record (by PK) does not exist.
+
+ Columns with `onUpdate()` defaults will be automatically populated if you omit them.
+
+
You can pass a partial, leaving fields out that you don’t want to change. For example here we leave the username the same:
```tsx
-// Leaves username field to previous value.
+// Leaves username field to previous value. updatedAt is automatically set.
z.mutate.user.update({
id: samID,
language: 'golang',
diff --git a/contents/docs/zero-schema.mdx b/contents/docs/zero-schema.mdx
index 4b3d6c9..3b57742 100644
--- a/contents/docs/zero-schema.mdx
+++ b/contents/docs/zero-schema.mdx
@@ -70,31 +70,78 @@ You can also use `from()` to access other Postgres schemas:
const event = table('event').from('analytics.event');
```
-### Optional Columns
+### Nullable Columns
-Columns can be marked _optional_. This corresponds to the SQL concept `nullable`.
+Columns can be marked _nullable_. This is equivalent to the SQL concept `nullable`.
```tsx
const user = table('user')
.columns({
id: string(),
name: string(),
- nickName: string().optional(),
+ nickName: string().nullable(),
})
.primaryKey('id');
```
-An optional column can store a value of the specified type or `null` to mean _no value_.
+A nullable column can store a value of the specified type or `null` to mean _no value_.
Note that `null` and `undefined` mean different things when working with Zero rows.
- - When reading, if a column is `optional`, Zero can return `null` for that field. `undefined` is not used at all when Reading from Zero.
- - When writing, you can specify `null` for an optional field to explicitly write `null` to the datastore, unsetting any previous value.
- - For `create` and `upsert` you can set optional fields to `undefined` (or leave the field off completely) to take the default value as specified by backend schema for that column. For `update` you can set any non-PK field to `undefined` to leave the previous value unmodified.
+ - When reading, if a column is `nullable`, Zero can return `null` for that field. `undefined` is not used at all when Reading from Zero.
+ - When writing, you can specify `null` for a nullable field to explicitly write `null` to the datastore, unsetting any previous value.
+ - For `create` and `upsert` you can set nullable fields to `undefined` (or leave the field off completely) to take the default value as specified by backend schema for that column. For `update` you can set any non-PK field to `undefined` to leave the previous value unmodified.
+### Insert and Update Defaults
+
+Use `onInsert()` and `onUpdate()` to have Zero automatically populate a column when a row is inserted or updated. These helpers take a function that will be **executed independently** on both the client and the server.
+
+```tsx
+const issue = table('issue')
+ .columns({
+ id: string(),
+ // Set when the row is first created
+ createdAt: number().onInsert(() => Date.now()),
+
+ // Set on insert **and** every subsequent update
+ updatedAt: number()
+ .onInsert(() => Date.now())
+ .onUpdate(() => Date.now()),
+ })
+ .primaryKey('id');
+```
+
+
+ 1. The value generated on the client is never sent to the server - it is executed on each side separately. If you need the exact same value for the client and server (for instance an `id`) you should generate that value _outside_ the mutation and pass it in to the mutation explicitly.
+ 2. Columns that use `onInsert` or `onUpdate` **cannot** be part of the primary key. The key must be stable and known ahead of time.
+
+
+#### Database-generated defaults
+
+If the value is already generated by the database (for example via a `DEFAULT` expression or trigger) you can tell Zero not to run your default function on the server by adding `dbGenerated()`:
+
+```tsx
+const issue = table('issue')
+ .columns({
+ id: string(),
+ // onInsert is only run on the client
+ // server expects the db to set a value instead (e.g. w/ `DEFAULT`)
+ createdAt: number()
+ .onInsert(() => Date.now())
+ .dbGenerated('insert'),
+
+ // onUpdate is only run on the client
+ // server expects the db to update the value (e.g. w/ a trigger)
+ updatedAt: number()
+ .onUpdate(() => Date.now())
+ .dbGenerated('update'),
+ })
+ .primaryKey('id');
+```
+
### Enumerations
Use the `enumeration` helper to define a column that can only take on a specific set of values. This is most often used alongside an [`enum` Postgres column type](postgres-support#column-types).