# Course: Handling schema changes confidently
https://www.sanity.io/learn/course/handling-schema-changes-confidently

Expertly navigate schema changes, validate datasets, and execute content migrations using tools like the Sanity CLI and GROQ queries.

---

## Navigation

**Track:** [Mastering content operations](https://www.sanity.io/learn/track/sanity-developer-essentials) · [View as markdown](https://www.sanity.io/learn/track/sanity-developer-essentials.md)

## Contents

1. [Introduction to Schema Change Management](https://www.sanity.io/learn/course/handling-schema-changes-confidently/introduction-to-schema-change-management) · [markdown](https://www.sanity.io/learn/course/handling-schema-changes-confidently/introduction-to-schema-change-management.md)
2. [Validating documents in bulk](https://www.sanity.io/learn/course/handling-schema-changes-confidently/updating-the-schema-to-match-imported-content) · [markdown](https://www.sanity.io/learn/course/handling-schema-changes-confidently/updating-the-schema-to-match-imported-content.md)
3. [Changing a field name](https://www.sanity.io/learn/course/handling-schema-changes-confidently/changing-a-field-name) · [markdown](https://www.sanity.io/learn/course/handling-schema-changes-confidently/changing-a-field-name.md)
4. [Adapting a front end without downtime](https://www.sanity.io/learn/course/handling-schema-changes-confidently/adapting-a-frontend-without-downtime) · [markdown](https://www.sanity.io/learn/course/handling-schema-changes-confidently/adapting-a-frontend-without-downtime.md)
5. [Writing a content migration](https://www.sanity.io/learn/course/handling-schema-changes-confidently/writing-a-content-migration) · [markdown](https://www.sanity.io/learn/course/handling-schema-changes-confidently/writing-a-content-migration.md)
6. [Running a content migration](https://www.sanity.io/learn/course/handling-schema-changes-confidently/running-a-content-migration) · [markdown](https://www.sanity.io/learn/course/handling-schema-changes-confidently/running-a-content-migration.md)
7. [Tidy up the schema and front end code](https://www.sanity.io/learn/course/handling-schema-changes-confidently/tidy-up-the-schema-and-front-end-code) · [markdown](https://www.sanity.io/learn/course/handling-schema-changes-confidently/tidy-up-the-schema-and-front-end-code.md)
8. [Making the content migration (more) idempotent](https://www.sanity.io/learn/course/handling-schema-changes-confidently/making-the-content-migration-more-idempotent) · [markdown](https://www.sanity.io/learn/course/handling-schema-changes-confidently/making-the-content-migration-more-idempotent.md)
9. [Schema Change Management Quiz](https://www.sanity.io/learn/course/handling-schema-changes-confidently/schema-change-management-quiz) · [markdown](https://www.sanity.io/learn/course/handling-schema-changes-confidently/schema-change-management-quiz.md)

---

## Lesson 1: Introduction to Schema Change Management
https://www.sanity.io/learn/course/handling-schema-changes-confidently/introduction-to-schema-change-management

How to think about schema change management and preparing your project for this course's exercises.

> [Video: Introduction to Schema Change Management](https://www.sanity.io/learn/course/handling-schema-changes-confidently/introduction-to-schema-change-management)

Very few content projects get everything right from the start. Even when you take the time to plan out your content model ahead, there are always “unknown unknowns” that will have implications for how you structure your content and which guidelines and rules you need to apply to your content. It can be character limits that ensure that a specific design doesn’t break; it can be something that you anticipated was a singular thing, actually should be plural, or maybe you came up with a better way to name something.



Having to change your content model, as expressed in a schema, is most often a good thing — it means that you are learning and iterating on your project. Fortunately, Sanity comes with different tools to make it easier to make these changes in a controlled manner and with more confidence, especially when you have to change something running in production.



In this course module, you will learn how to change your schema and content accordingly:



- How to check document and field validation status across a dataset

- How to deprecate parts of your schema

- How to write and run content migrations

- How to write intermediary GROQ queries to support a seamless migration


## Prerequisites



You should be able to follow this course if you have completed, or have come from, the "Day One" course. 



> [!TIP]
> Go to the [Day one content operations](https://www.sanity.io/learn/course/day-one-with-sanity-studio) course



This course will build on the same studio, schema, and front end.



### Import example content to your dataset



If you have not yet already imported the example dataset, it will be easier to complete this course by doing so.



Download `production.tar.gz` below and import it into your Sanity project by running the following command in your studio folder:



- [ ] Download the dataset export and move it to the root of your Studio

- [ ] **Run** the following to import the archive into your project's `production` dataset


```sh
# in apps/studio
pnpm dlx sanity@latest dataset import production.tar.gz production
```

A successful import will give you a bunch of artists, venues, and events in the past and future between 2010–2030.



![Sanity Studio showing a listing of "upcoming" events](https://cdn.sanity.io/images/3do82whm/next/e5f0b4d549d670a84e6f11e531c5042dce7f0818-2240x1480.png)

---

## Lesson 2: Validating documents in bulk
https://www.sanity.io/learn/course/handling-schema-changes-confidently/updating-the-schema-to-match-imported-content

Check document validation status across a whole dataset. Update schema with missing fields for content that's found in the dataset's documents.

> [Video: Validating documents in bulk](https://www.sanity.io/learn/course/handling-schema-changes-confidently/updating-the-schema-to-match-imported-content)

Since you have only three document schema types, it’s not that hard to get a sense of what’s going on from looking through the documents. Most projects with some maturity might have a lot of schema types and a lot of content. This is where it becomes useful to use the Sanity CLI to investigate mismatches between your content and the schema that is supposed to describe it.



- [ ] **Run** the following to validate all documents from your terminal


```sh
# in apps/studio
pnpm dlx sanity@latest documents validate
```

The CLI will give you a small warning about potential pitfalls with this command, if you want to skip this warning in the future, you can run the following:



```sh
# in apps/studio
pnpm dlx sanity@latest documents validate -y
```

When the command has run, you may have a list of warnings or errors about missing field values:



```sh:Output from the command
Validation results:
✔ Valid:    408 documents
✖ Errors:   2 documents, 2 errors
⚠ Warnings: 2 documents, 2 warnings
 ERROR   event  57c1561e-378c-4124-9aa2-88c0db96a037
└─ slug .......................... ⚠ Required
  └─ current ..................... ✖ Required

 ERROR   event  drafts.57c1561e-378c-4124-9aa2-88c0db96a037
└─ slug .......................... ⚠ Required
  └─ current ..................... ✖ Required
```

This means there is content in your dataset which is not aligned with the validation rules in your Studio schema. In the instance of our error above, there's one document that doesn't have a `slug` field value.



> [!NOTE]
> You can copy a document ID from the terminal into the search box of your studio to find and fix each document manually.



If you had validation errors, and have now fixed them, re-run:



```sh
# in apps/studio
pnpm dlx sanity@latest documents validate -y
```

And you should see output like:



```sh
Validation results:
✔ Valid:    408 documents
✖ Errors:   0 documents, 0 errors
⚠ Warnings: 0 documents, 0 warnings
```

Great! You now have a dataset full of valid content and the skills to ensure validation in bulk as part of future content operations. 



You are now ready to do the opposite, namely, changing the schema and updating the content accordingly.



---

## Lesson 3: Changing a field name
https://www.sanity.io/learn/course/handling-schema-changes-confidently/changing-a-field-name

Rename fields by first deprecating existing ones and marking them read only

> [Video: Changing a field name](https://www.sanity.io/learn/course/handling-schema-changes-confidently/changing-a-field-name)

Open an event document. There you will find the “Event Type” field that describes if the event is “In-person” or “Virtual.” When you go to the “Inspect” menu (`ctrl+opt+i/ctrl+alt+i`), you see that it’s encoded 



```json
{
  // ...other fields
  eventType: "in-person"
}
```

Let’s pretend that you have received feedback from your developer colleagues that it’s a bit confusing to have both a **document type** called `event` and a **field type** called `eventType` (they're correct by the way, who modeled this schema?)



When implementing this data in an application, you also get a somewhat inelegant pattern: `event.eventType`. 



Naming is hard.



We want to change this field to be called “Event Format” instead, that is a `format` attribute in the JSON data so that you can get the value with `event.format` where this field content is used.



## Change management



Now, you will not only change this field as data for developers, but you are changing it for content creators as well. Usually, if it’s early in the project, and there aren’t a lot of documents yet, you can get away with just changing the `name` property of the field and manually backfilling the content. 



But let’s pretend that this is a more mature project, where you have an application in production that consumes and depends on this data, and you (or someone on your team) don’t want to spend time manually going through every document to copy-paste content between the old and the new field.



In this case, changing the field name requires the following steps:



- Adding a new field with the new name

- Deprecating the old field

- Adapting a query to support the new field name and fallback on the old

- Create a content migration to move the content


## Add the new field



Begin with adding the new field; you can copy-paste the `eventFormat` field definition, and change the name.



- [ ] **Update **the `event` document schema type to include the new `format` field


```typescript:apps/studio/schemaTypes/eventType.ts
defineField({
  name: 'format',
  type: 'string',
  options: {
    list: ['in-person', 'virtual'],
    layout: 'radio',
  },
}),
```

You should now have two identical fields with different names and labels:



![Image](https://cdn.sanity.io/images/3do82whm/next/552d1c2f4eb5d256cee08b74263c0bf284363619-602x294.png)

## Deprecate the current field



Now, you can **deprecate** the “Event type” field and add a guide for the content creator. You can also set it to “read-only” to prevent content creators from using it.



- [ ] **Update** the `eventType` field to include config for `deprecated` and set to `readOnly`


```typescript:schemaTypes/eventType.ts
defineField({
  name: 'eventType',
  type: 'string',
  deprecated: {
    reason: 'Use the "Event format" field instead.'
  },
  readOnly: true,
  options: {
    list: ['in-person', 'virtual'],
    layout: 'radio',
  },
}),
```

Save the change and confirm that the document form now looks like this:



![Image](https://cdn.sanity.io/images/3do82whm/next/4904d5820b47e3521a92b414f6cba4e5d1acd2d4-2000x1068.png)

This prevents authors from making any further changes to this field in the Studio, or any new documents with this value.



Let's update our front end to consume the new field, once it has values.



---

## Lesson 4: Adapting a front end without downtime
https://www.sanity.io/learn/course/handling-schema-changes-confidently/adapting-a-frontend-without-downtime

Prepare a front end for down-time-less content migration by adapting a GROQ query with fallbacks

> [Video: Adapting a front end without downtime](https://www.sanity.io/learn/course/handling-schema-changes-confidently/adapting-a-frontend-without-downtime)

Let’s say that we want to do this schema and content migration without any downtime and without disrupting the work of content creators. Before deploying the change to the Studio, you can deploy a small change to the front end that makes it ready to use the new field and fall back on the old field if there is no content. In other words, this is how the workflow could look like:



- Make the content query work with content for both fields

- Deploy the frontend change

- Deploy the Studio with the deprecated and new fields

- Run a content migration to move legacy content over to the new field

- Adapt the content query to remove the assumption of the old field


This may seem like a lot, but it should be fairly straightforward in this case.



In this exercise, we will use the front end code from the [Day one content operations](https://www.sanity.io/learn/course/day-one-with-sanity-studio) course.



### Adapting the GROQ query



You must only change the query to make the front end work with content from *both* fields: `eventType` and `format`.



- [ ] Locate the GROQ query for the single event route.


```typescript:apps/web/src/app/events/[slug]/page.tsx
const EVENT_QUERY = `*[
  _type == "event" &&
  slug.current == $slug
][0]{
	...,
	headline->,
	venue->
}`;
```

In this query, you use the ellipsis (`…`) as a quick way to return *all* the fields on the document, and then below, you’re “overriding” the `headline` and `venue` reference fields with the join operator (`→`) to access all the fields of these documents. This is just a quick way to get all the data into the application and start experimenting with it. For projects in production, you would usually specify all the fields you use to prevent overfetching.



Now you want to override the `eventType` data to ensure we always get data for it, regardless if the deprecated or new field is used. To do this, you can use a GROQ function called `coalesce()`



- [ ] Update the query with the `coalesce()` function


```typescript:apps/web/src/app/events/[slug]/page.tsx
const EVENT_QUERY = `*[
  _type == "event" &&
  slug.current == $slug
][0]{
	...,
    "eventType": coalesce(format, eventType),
	headline->,
	venue->
}`;
```

What this query does now is to first look for a value for `format` and set that to the `eventType` key, if it doesn’t find a value for it, it will fallback on the value for `eventType`.



- [ ] Confirm that the query works by setting a value for “Event format” on an event that’s different from the “Event type”


Now, your front end and your new Studio schema changes can be deployed (you don’t have to for this course) without any downtime. Your content creators can start working with the new **Event format** field, while they cannot change the old **Event type** field.



---

## Lesson 5: Writing a content migration
https://www.sanity.io/learn/course/handling-schema-changes-confidently/writing-a-content-migration

Use Sanity CLI to create a new content migration. Adapt the migration script for your use case. 

> [Video: Writing a content migration](https://www.sanity.io/learn/course/handling-schema-changes-confidently/writing-a-content-migration)

While the current solution with the adapted GROQ query and the deprecated field works, having two field names for the same type of content can be confusing. Imagine you want to make a new filter on the home page. You would have to take both field names into account. For changes like these, it might be a good idea to move the legacy content over to the new field and, in this case, remove the old field completely so we can also keep the document form nice and tidy.



Since the front end query now works with both fields, you can even migrate content against the production dataset without any downtime or disruption.



## Using Sanity CLI to create a new migration



The Sanity CLI contains helpful guidance and templates to create content migration scripts.



- [ ] Inside your Sanity Studio, create a new content migration


```sh
# in apps/studio
pnpm dlx sanity@latest migration create "Replace event type with event format"
```

The CLI will prompt with some questions; answer the following (you can always change this in code after):



- [ ] Type of documents to migrate: **event**

- [ ] Select a template: **Rename a field**


The CLI should now have made a new folder in your Studio project called `migrations` with a content migration script inside `replace-event-type-with-event-format/index.ts`.



- [ ] Open the content migration script in your code editor


## Writing a content migration script



In fact, the CLI has done most of the job already. But let’s unpack what’s going on.



```typescript:apps/studio/migrations/replace-event-type-with-event-format/index.ts
import {defineMigration, at, setIfMissing, unset} from 'sanity/migrate'

const from = 'oldFieldName'
const to = 'newFieldName'

export default defineMigration({
  title: 'Replace event type with event format',
  documentTypes: ["event"],

  migrate: {
    document(doc, context) {
      return [
        at(to, setIfMissing(doc[from])),
        at(from, unset())
      ]
    }
  }
})
```

You can see that the script imports a couple of functions from `sanity/migrate` and has a default export called `defineMigration`.



Change the `from` and `to` variables to reflect the schema change you want to do.



- [ ] Set `from` to the value `eventType`

- [ ] Set `to` to the value `format`


```typescript:apps/studio/migrations/replace-event-type-with-event-format/index.ts
import {defineMigration, at, setIfMissing, unset} from 'sanity/migrate'

const from = 'eventType'
const to = 'format'

export default defineMigration({
  title: 'Replace event type with event format',
  documentTypes: ["event"],

  migrate: {
    document(doc, context) {
      return [
        at(to, setIfMissing(doc[from])),
        at(from, unset())
      ]
    }
  }
})
```

What happens when you run this, is that it will fetch all the documents of the type `event` and then export two **patches** for that document:



- the `at(to, setIfMissing(doc[from]))` patch sets a new field called `format` with the value of the `eventType` , `setIfMissing` ensures that the value is set *only* if `format` doesn’t exist from before

- `at(from, unset())` removes the `eventType` value, and thus the field from the document


Save your changes. If you run `npx sanity@latest migration list`. You should get a table with the migration ID and title:



- [ ] **Run** the following command to get the content migration ID


```sh
# in apps/studio
pnpm dlx sanity@latest migration list 
```

---

## Lesson 6: Running a content migration
https://www.sanity.io/learn/course/handling-schema-changes-confidently/running-a-content-migration

Use field validation to validate if the migration was successful. Back up the dataset for safe measure. Run the content migration and verify that it went well.

> [Video: Running a content migration](https://www.sanity.io/learn/course/handling-schema-changes-confidently/running-a-content-migration)

## Using validation to validate a migration



Before diving into the migration, we want to do one more thing: make the **Event format** field **required**. The primary reason is that we always want to have information for this with an event, but it will also let us validate that the content migration has been successful later on.



- [ ] Add the `rule.required()` validation rule to the **Event format** field


```typescript:apps/studio/schemaTypes/eventType.ts
defineField({
  name: 'format',
  type: 'string',
  title: 'Event format',
  options: {
    list: ['in-person', 'virtual'],
    layout: 'radio',
  },
  validation: (rule) => rule.required(),
}),
```

If you save this change and run the `sanity documents validate -y` command again, you should get a lot of errors like this on the `event` document type:



- [ ] **Run** the following to validate all documents


```sh
# in apps/studio
pnpm dlx sanity@latest documents validate -y
```

You should get errors on all event documents like:



```sh:Terminal output
ERROR   event  AUoLUkEDo6CVeRx5svBpXH
└─ format ........................ ✖ Required

```

When you finish the content migration, this validation error should disappear. You are now ready to prepare the content migration to move the values over from the old to the new field in all the existing documents.



## Dry-running a content migration



You are now ready to do a dry run to check how the migration script will affect your content in the dataset. You won't need to add a flag to dry run this, migrations are dry run by default.



- [ ] **Run** the following to "dry run" the migration


```sh
# in apps/studio
pnpm dlx sanity@latest migration run replace-event-type-with-event-format
```

If run successfully, this command should output a list of patches for each event document, looking like this:



```
patch   event  AUoLUkEDo6CVeRx5svBpBB 
└─ eventType ..................... unset()

 patch   event  AUoLUkEDo6CVeRx5svBiyh 
└─ format ........................ setIfMissing("in-person")
```

### Backing up the dataset



It’s a good habit to export the dataset before running a content migration, especially if you do it on production content. This way, you have a backup that you can import if something goes wrong or you make an error (it happens to us all).



- [ ] **Run** the following to export your production dataset with the Sanity CLI


```sh
# in apps/studio
pnpm dlx sanity@latest dataset export production
```

This will export all your documents into a newline-delineated JSON file (`.ndjson`) and assets in their folder as a `production.tar.gz` file. If you want to “reset” your dataset to its state before the content migration, you can run the following command:



```sh
# in apps/studio
pnpm dlx sanity@latest dataset import production.tar.gz production --replace
```

Note that the `--replace` will *overwrite* documents with the same `_id`.



With this backup, you can execute the content migration knowing that you have a way to restore the dataset if something goes wrong.



## Executing a content migration



With the dataset backup in place and having verified that the content migration script outputs the changes we want, you are now ready to move a lot of data in one swoop.



- [ ] **Run **the content migration with the `--no-dry-run` flag


```sh
# in apps/studio
pnpm dlx sanity@latest migration run replace-event-type-with-event-format --no-dry-run
```

If everything went well, then your terminal should have an output like this:



```sh:Terminal output
❯ sanity migration run replace-event-type-with-event-format --no-dry-run
? This migration will run on the production dataset in your-project-id project. Are you sure? Yes
✔ Migration "replace-event-type-with-event-format" completed.

  Project id:  uxqfcg2d
  Dataset:     production

  179 documents processed.
  179 mutations generated.
  1 transactions committed.
```

This tells you that you have migrated 179 documents with 179 mutations, and it was all done with one transaction (that is one API request). In other words, it happened almost instantly for all these documents and anyone who had been in the Studio while you ran this.



![Image](https://cdn.sanity.io/images/3do82whm/next/1b2b8585295d5e7ad4b117492854094678ba3b00-2056x1300.png)

## Verifying the content migration



Run the validation command again to see if there is content for the **Event format** field.



- [ ] **Run** the following to confirm that the migration was successful


```sh
# in apps/studio
pnpm dlx sanity@latest documents validate -y 
```

You should see no more validation errors:



```sh:Terminal output
❯ sanity documents validate -y    
✔ Loaded workspace 'default' using project 'uxqfcg2d' and dataset 'production' (1.8s)
✔ Downloaded 457 documents (0.3s)
✔ Checked all references (0.0s)
✔ Validated 444 documents (3.4s)

Validation results:
✔ Valid:    444 documents
✖ Errors:   0 documents, 0 errors
⚠ Warnings: 0 documents, 0 warnings
```

---

## Lesson 7: Tidy up the schema and front end code
https://www.sanity.io/learn/course/handling-schema-changes-confidently/tidy-up-the-schema-and-front-end-code

Tidy up the document form by hiding the old field. Update your front end to only query and access the new field name.

> [Video: Tidy up the schema and front end code](https://www.sanity.io/learn/course/handling-schema-changes-confidently/tidy-up-the-schema-and-front-end-code)

With the successful content migration, you can now clean up the event document type and the front end query.



For real projects, you can either remove the field completely or add `hidden: true` to hide it from content creators, keep its presence in code in cases where legacy data might enter the dataset, and you want to prevent the “unknown field” warning.



- [ ] Remove the `eventType` field in `eventType.ts` or add `hidden: true` to hide it visually for content creators.


```typescript:apps/studio/schemaTypes/eventType.ts
defineField({
  name: 'eventType',
  type: 'string',
  title: 'Event type',
  deprecated: {
    reason: 'Use the "Event format" field instead.',
  },
  readOnly: true,
  hidden: true, // hide from content creators, but keep it in code
  options: {
    list: ['in-person', 'virtual'],
    layout: 'radio',
  },
}),
```

- [ ] Remove the `"eventType": coalesce(format, eventType)` line in the GROQ query

- [ ] Replace the `event.eventType` variable in your front end with the new `event.format` variable.


Note that if you have used this same front end code for [Day one content operations](https://www.sanity.io/learn/course/day-one-with-sanity-studio) your query and components may look a little different. Adapt as required!



```tsx:apps/web/src/app/events/[slug]/page.tsx
import { defineQuery, PortableText } from "next-sanity";
import Link from "next/link";
import { notFound } from "next/navigation";
import Image from "next/image";

import { sanityFetch } from "@/sanity/live";
import { urlFor } from "@/sanity/image";

const EVENT_QUERY = defineQuery(`*[
    _type == "event" &&
    slug.current == $slug
  ][0]{
  ...,
  "date": coalesce(date, now()),
  "doorsOpen": coalesce(doorsOpen, 0),
  headline->,
  venue->
}`);

export default async function EventPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { data: event } = await sanityFetch({
    query: EVENT_QUERY,
    params: await params,
  });
  if (!event) {
    notFound();
  }
  const { name, date, headline, details, format, doorsOpen, venue, tickets } =
    event;

  const eventDate = new Date(date).toDateString();
  const eventTime = new Date(date).toLocaleTimeString();
  const doorsOpenTime = new Date(
    new Date(date).getTime() - doorsOpen * 60000
  ).toLocaleTimeString();

  const imageUrl = headline?.photo
    ? urlFor(headline.photo)
        .height(310)
        .width(550)
        .quality(80)
        .auto("format")
        .url()
    : "https://placehold.co/550x310/png";

  return (
    <main className="container mx-auto grid gap-12 p-12">
      <div className="mb-4">
        <Link
          href="/"
          className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
        >
          ← Back to events
        </Link>
      </div>
      <div className="grid items-top gap-12 sm:grid-cols-2">
        <Image
          src={imageUrl}
          alt={name || "Event"}
          className="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full"
          height="310"
          width="550"
        />
        <div className="flex flex-col justify-center space-y-4">
          <div className="space-y-4">
            {format ? (
              <div className="inline-block rounded-lg bg-gray-100 dark:bg-gray-800 px-3 py-1 text-sm text-gray-700 dark:text-gray-300 capitalize">
                {format.replace("-", " ")}
              </div>
            ) : null}
            {name ? (
              <h1 className="text-4xl font-bold tracking-tighter mb-8 text-gray-900 dark:text-white">
                {name}
              </h1>
            ) : null}
            {headline?.name ? (
              <dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base text-gray-700 dark:text-gray-300">
                <dd className="font-semibold text-gray-900 dark:text-white">
                  Artist
                </dd>
                <dt>{headline?.name}</dt>
              </dl>
            ) : null}
            <dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base text-gray-700 dark:text-gray-300">
              <dd className="font-semibold text-gray-900 dark:text-white">
                Date
              </dd>
              <div>
                {eventDate && <dt>{eventDate}</dt>}
                {eventTime && <dt>{eventTime}</dt>}
              </div>
            </dl>
            {doorsOpenTime ? (
              <dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base text-gray-700 dark:text-gray-300">
                <dd className="font-semibold text-gray-900 dark:text-white">
                  Doors Open
                </dd>
                <div className="grid gap-1">
                  <dt>Doors Open</dt>
                  <dt>{doorsOpenTime}</dt>
                </div>
              </dl>
            ) : null}
            {venue?.name ? (
              <dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base text-gray-700 dark:text-gray-300">
                <div className="flex items-start">
                  <dd className="font-semibold text-gray-900 dark:text-white">
                    Venue
                  </dd>
                </div>
                <div className="grid gap-1">
                  <dt>{venue.name}</dt>
                </div>
              </dl>
            ) : null}
          </div>
          {details && details.length > 0 && (
            <div className="prose max-w-none prose-gray dark:prose-invert">
              <PortableText value={details} />
            </div>
          )}
          {tickets && (
            <a
              className="flex items-center justify-center rounded-md bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 p-4 text-white transition-colors"
              href={tickets}
            >
              Buy Tickets
            </a>
          )}
        </div>
      </div>
    </main>
  );
}
```





---

## Lesson 8: Making the content migration (more) idempotent
https://www.sanity.io/learn/course/handling-schema-changes-confidently/making-the-content-migration-more-idempotent

Make it safe(r) to run the content migration multiple times using GROQ filters and idempotency keys.  

> [Video: Making the content migration (more) idempotent](https://www.sanity.io/learn/course/handling-schema-changes-confidently/making-the-content-migration-more-idempotent)

Idempotent or idempotency in the context of data and content migration means that you should be able to run a migration multiple times with the same result. In this case, the script is already fairly idempotent because `format` is never overwritten if it’s already set. However, learning more about how to prevent a content migration script from incurring unintentional changes can be useful.



You will learn two approaches:



- **Filtering** only the documents that should be migrated

- Adding an **idempotence key** to skip documents that have already been migrated once


### Using a GROQ filter



In the content migration example in this course module, you filtered on the document type `event` and added transactions for all the documents. You can replace this with the `filter` property that accepts a simple GROQ filter ([GROQ joins](https://www.sanity.io/learn/specifications/groq-joins) are not supported). Often, you can express the conditional state of documents that need to be migrated.



- [ ] Adapt the content migration script with a filter to only target documents with a value for `eventType` and no value for `format`


```typescript:migrations/replace-event-type-with-event-format/index.ts
import {defineMigration, at, setIfMissing, unset} from 'sanity/migrate'

const from = 'eventType'
const to = 'format'

export default defineMigration({
  title: 'Replace event type with event format',
  // documentTypes: ['event'],
  filter: '_type == "event" && defined(eventType) && !defined(format)',
  migrate: {
    document(doc, context) {
      return [at(to, setIfMissing(doc[from])), at(from, unset())]
    },
  },
})
```

Note: If you run this content migration script, you shouldn’t get any output because all your documents have been migrated.



### Using an idempotence** **key



Some content migrations can’t be made idempotent with filters. In this case, you can add an idempotency key to mark it as migrated. If you store the keys in an array, you can even keep track of all the migrations a document has been subjected to.



Here's an example of what that might look like:



```typescript:migrations/replace-event-type-with-event-format/index.ts
import {defineMigration, at, setIfMissing, unset, insert} from 'sanity/migrate'

// should be unique for the migration but never change
const idempotenceKey = 'xyz' 

const from = 'eventType'
const to = 'format'

export default defineMigration({
  title: 'Replace event type with event format',
  // documentTypes: ['event'],
  filter: '_type == "event" && defined(eventType) && !defined(format)',
  migrate: {
    document(doc, context) {
      if ((doc?._migrations as string[] || []).includes(idempotenceKey)) {
        // Document already migrated, so we can skip
        return
      }
      return [
        at(to, setIfMissing(doc[from])),
        at(from, unset()),
        //… add idempotence key
        at('_migrations', setIfMissing([])),
        at('_migrations', insert(idempotenceKey, 'after', 0)),
      ]
    },
  },
})
```

In this example, you also see the pattern for inserting into an array field.



---

## Lesson 9: Schema Change Management Quiz
https://www.sanity.io/learn/course/handling-schema-changes-confidently/schema-change-management-quiz

> [Video: Schema Change Management Quiz](https://www.sanity.io/learn/course/handling-schema-changes-confidently/schema-change-management-quiz)

> **Question:** What happens when you run sanity documents validate -y in your Sanity project?
>
> 1. It validates the schema against the content in your dataset, and outputs warnings for missing fields and references **[correct]**
> 2. It validates the JavaScript syntax in your schema files
> 3. It validates the HTML output of your front end
> 4. It validates the accessibility of your front end

> **Question:** What is the purpose of the deprecated property on a field in Sanity Studio?
>
> 1. It prevents you from publishing changes to the field
> 2. It removes the field from the document model
> 3. It prevents the field from being queried
> 4. It shows a warning to the content editor that the field is deprecated **[correct]**

> **Question:** What does it mean that a function or operation is idempotent in the context of data and content migration?
>
> 1. It means that you can run the function or operation multiple times and get the same result **[correct]**
> 2. It means that you can run the function or operation in parallel with other operations
> 3. It means that the function or operation can be undone
> 4. It means that the function or operation can be paused and resumed

> **Question:** What does the migration create command in the Sanity CLI do?
>
> 1. It creates a new dataset
> 2. It creates a new Sanity project
> 3. It creates a new migration script **[correct]**
> 4. It creates a new document in the dataset

> **Question:** What does the migration run command in the Sanity CLI do?
>
> 1. It runs the migration script in a new Docker container
> 2. It runs a migration script against your dataset **[correct]**
> 3. It runs the migration script in the cloud
> 4. It runs the migration script in the browser

> **Question:** What happens when you add readOnly: true to a field in Sanity Studio?
>
> 1. It prevents the field from being queried in the GraphQL API
> 2. It prevents the field from being displayed in the document form
> 3. It prevents the content editor from making changes to the field **[correct]**
> 4. It prevents the field from being indexed in the search

> **Question:** What is the purpose of the coalesce() function in GROQ?
>
> 1. It returns the first non-null argument **[correct]**
> 2. It concatenates two strings
> 3. It calculates the sum of two numbers
> 4. It returns the last non-null argument

> **Question:** What is the purpose of the filter property in a migration script?
>
> 1. It removes filtered documents from the dataset
> 2. It reduces the number of webhooks used in a migration
> 3. It refines the documents effected in the migration **[correct]**
> 4. It modifies which users can run migrations

---

## Related Resources

- [Track overview](https://www.sanity.io/learn/track/sanity-developer-essentials.md)
- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)
