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).