Skip to content

wip: added docs related to defaults #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions contents/docs/writing-data.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<Note>
Columns with an `onInsert()` function do not need to be specified and will be automatically populated if omitted (or set to `undefined`).
</Note>

```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(),
Expand Down Expand Up @@ -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).

<Note>
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.
</Note>

## Update

Update an existing record. Does nothing if the specified record (by PK) does not exist.

<Note>
Columns with `onUpdate()` defaults will be automatically populated if you omit them.
</Note>

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',
Expand Down
61 changes: 54 additions & 7 deletions contents/docs/zero-schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 type="note" heading="Null and undefined">
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.

</Note>

### 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');
```

<Note type="warning">
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.
</Note>

#### 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).
Expand Down