Skip to main content

migrate / db push (Syncing Sheets)

npx gassma migrate and npx gassma db push are commands that generate a GAS function that syncs your spreadsheet's sheets and columns with the Prisma schema (the counterparts of Prisma's prisma migrate dev / prisma migrate deploy / prisma db push).

$ npx gassma migrate dev                  # generate and record a trail (migrations/)
$ npx gassma migrate dev --name add_tags # name the trail entry
$ npx gassma migrate deploy # generate the latest recorded trail entry as it is
$ npx gassma db push # generate without recording a trail
CommandBehavior
migrate devGenerates the runnable stub from the schema and records a trail entry (migrations/). Asks for confirmation when something is deleted
migrate deployWrites the latest recorded trail entry out as the runnable stub, as it is. It does not read the schema, does not record a trail, and does not ask anything
db pushGenerates the runnable stub from the schema. No trail is recorded

npx gassma migrate with no arguments prints the help.

note

The commands themselves do not access the spreadsheet. The sheets are synced the moment you run the generated gassmaMigrate function once on the Apps Script side. clasp push is not run automatically either (see "After Generating" below).

What Gets Generated​

A runnable stub gassma-migration.js (plain JS) is generated in the output directory. Its content is a single gassmaMigrate function that calls the GASsma library's migrateSheets, with the sheet and column names extracted from the schema embedded in it.

model User {
id Int @id
name String
posts Post[]
}

model Post {
id Int @id
title String
author User @relation(fields: [authorId], references: [id])
authorId Int
}

The schema above generates the following.

function gassmaMigrate() {
Gassma.migrateSheets({
spreadsheetId: "XXXXX",
models: [
{ name: "User", columns: ["id", "name"] },
{ name: "Post", columns: ["id", "title", "authorId"] }
]
});
}
  • The spreadsheet ID is resolved from the datasource block in the schema first, then from datasource.url in gassma.config.ts. If neither is set, it is not embedded and the spreadsheet bound to the script is targeted at runtime.
  • Running the function requires the GASsma library to be registered in the GAS project (an environment set up with bootstrap works as-is). The call symbol (the Gassma. part in the example) is automatically resolved from the userSymbol of the library registered in appsscript.json in the output directory (falling back to Gassma when not found).

Output Directory Resolution​

  1. --output <dir> (highest priority)
  2. rootDir in .clasp.json in the current directory

If neither is available, a MigrateOutputDirError is raised. This is the same for all three commands.

Sheet and Column Extraction Rules​

These are the rules migrate dev / db push follow when reading the schema (migrate deploy does not read the schema).

  • One sheet per model. When @@map / @map are used, the mapped physical names are used.
  • Only scalar fields become columns. Relation fields (posts / author in the example above) do not become columns, while foreign key columns (authorId) do.
  • Fields and models with @ignore / @@ignore are also included as creation targets. As in Prisma, they are only excluded from the client and still exist physically in the spreadsheet.
  • Junction sheets for implicit Many-to-Many relations are also created (e.g. _PostToTag, with columns postId, tagId in alphabetical order of the model names).

After Generating​

Run the two Next steps shown on success, and the sheets get synced.

✅ Migration generated

Next steps:
1. Run "clasp push" (or "npm run push") to upload gassma-migration.js
2. In the Apps Script editor, run the "gassmaMigrate" function once
note

Push with clasp push directly (or npm run push, which only calls clasp push). A full clean build (such as npm run deploy generated by bootstrap) may wipe the output directory and delete gassma-migration.js before it is pushed.

Sync Rules​

The sync performed by gassmaMigrate (Gassma.migrateSheets) is idempotent and safe to run any number of times.

  • Sheets that are in the schema but not in the spreadsheet are created, with the headers written to row 1.
  • On existing sheets, only the missing columns are appended to the right end of the header row. Existing columns are never reordered, and data rows are never written to.
  • Columns and sheets that are not in the schema are not deleted by default; a warning is logged and they are left untouched.
Gassma.migrateSheets: column "legacy" on sheet "User" is not in the schema. It is left untouched.

Deleting Data (--accept-data-loss)​

When acceptDataLoss: true is embedded in the stub, running gassmaMigrate also deletes the columns and sheets that are not in the schema.

Columns and sheets that still contain data are deleted after a warning that reports how much is left (the number of non-empty cells for a column, the number of data rows for a sheet). Empty ones are deleted without a warning.

Gassma.migrateSheets: You are about to drop the column "legacy" on the sheet "User", which still contains 12 non-empty values.

How the flag is set differs per command.

CommandHow it is set
db pushPass --accept-data-loss
migrate devAnswer y to the drop confirmation
migrate deployThe value recorded in the trail entry is used as it is

Protecting Sheets You Do Not Want Deleted​

To keep a sheet, leave its model in the schema and mark it @@ignore instead of removing it. As described in Sheet and Column Extraction Rules, a model with @@ignore stays on the list of targets, so it is not deleted even with --accept-data-loss. It stays invisible to the client while the sheet itself is protected.

model Memo {
id Int @id
content String

@@map("メモ")
@@ignore
}

Columns work the same way: a field marked @ignore stays on the list of targets. A model name cannot contain non-ASCII characters, so map a non-ASCII sheet name with @@map as in the example above (see map).

When you write fields as in the example above, the columns of that sheet are synced too. Columns that are not in the schema are deleted with --accept-data-loss, so what is protected is the sheet plus the columns you wrote in the schema.

To protect every column as well, write a model with no fields at all. It is a valid Prisma schema, and GASsma then manages none of that sheet's columns: no column is added or deleted even with --accept-data-loss, and no "a column that is not in the schema" warning is logged.

model Memo {
@@map("メモ")
@@ignore
}
Gassma.migrateSheets: model "メモ" declares no columns. The columns of sheet "メモ" are left untouched.

If the sheet does not exist yet, it is created just as it is for a model with fields (with no header row, since there are zero columns).

note

Removing the model from the schema instead makes the sheet "a sheet that is not in the schema". Without --accept-data-loss it survives with a warning, but with it the sheet is deleted.

Drop Confirmation (migrate dev)​

migrate dev compares the latest trail entry with the current schema, and asks for confirmation before generating anything when a sheet or a column has disappeared.


⚠️ The following are recorded in gassma/migrations but are no longer in your schema:
• sheet "Legacy"
• column "nickname" in sheet "User"
Generating this migration deletes them together with every value they hold.
This is based on the recorded migrations, not on the spreadsheet itself:
sheets and columns changed by "gassma db push" or by hand are not reflected here.

Continue? (y/N)
  • Answering y / yes (case-insensitive) generates a stub that deletes (acceptDataLoss: true) and records the trail entry.
  • Anything else (including a bare Enter) aborts. Neither the stub nor the trail entry is written.
Aborted. gassma-migration.js and the migration were not written.
  • When no sheet or column has disappeared, nothing is asked and the stub does not delete. Nothing is asked on the very first run either, when no trail entry exists yet.
  • When something is deleted in a non-interactive environment (CI and the like), there is no way to confirm, so the command aborts with a MigrateConfirmationRequiredError. Use migrate deploy when you want to run the recorded trail entry as it is.
caution

This confirmation compares against the trail entries recorded in migrations/, not against the spreadsheet itself. Running db push in between, or editing sheets by hand, makes the trail and the actual sheets drift apart. Read the list as "what disappeared from the schema since the last migrate dev".

note

When the latest trail entry cannot be read, the command warns that deletions could not be checked and generates a stub that does not delete.

Migration Trail (migrations/)​

migrate dev creates <UTC timestamp>[_name]/migration.js under the migrations/ directory next to the schema. Its content is identical to the runnable stub.

gassma/
├── schema.prisma
└── migrations/
├── 20260801120000_init/
│ └── migration.js
└── 20260802093000_add_tags/
└── migration.js
  • If the content is the same as the latest trail entry, no new entry is created (Already in sync, no schema change or pending migration was found. is shown). The runnable stub itself is rewritten every time.
  • Re-running migrate dev on an unchanged schema keeps the acceptDataLoss recorded in the trail entry. A deletion you once answered y to is not taken back by a re-run.
  • --name names the trail entry. The name is sanitized by splitting camelCase, lowercasing, and replacing runs of non-alphanumeric characters with _ (e.g. --name "Add UserRole!" → 20260801120000_add_user_role).

migrate deploy​

migrate deploy writes the latest trail entry in migrations/ out as the runnable stub, as it is. It does not read the schema, so the output does not change even if you have edited the schema.

$ npx gassma migrate deploy
📄 Using gassma/migrations/20260802093000_add_tags/migration.js
📄 Wrote dist/gassma-migration.js

✅ Latest recorded migration prepared

The acceptDataLoss embedded in the trail entry is replayed as well, so the deletion decision you made in dev is what runs. CI never decides a deletion on its own.

When no trail entry exists, a NoMigrationTrailError is raised. Run migrate dev first.

Options​

migrate dev​

OptionDescription
--name <name>Name for the new migration
--output <dir>Directory to write gassma-migration.js (defaults to rootDir in .clasp.json)
--schema <path>Path to a specific .prisma file to migrate from
--config <path>Custom path to your GASsma config file

migrate deploy​

OptionDescription
--output <dir>Directory to write gassma-migration.js (defaults to rootDir in .clasp.json)
--config <path>Custom path to your GASsma config file

db push​

OptionDescription
--output <dir>Directory to write gassma-migration.js (defaults to rootDir in .clasp.json)
--schema <path>Path to a specific .prisma file to push from
--config <path>Custom path to your GASsma config file
--accept-data-lossDelete sheets and columns that are not in the schema

Limitations​

  • The header row is assumed to be row 1 starting at column A on every sheet. Header positions moved via changeSettings are not supported.
  • Since a spreadsheet must contain at least one sheet, the last remaining sheet is never deleted even with acceptDataLoss: true; only a warning is logged.

Gassma.migrateSheets (Library API)​

Gassma.migrateSheets, which the stub calls, is a public GASsma API. You can also call it directly with the sheets and columns you want to sync, without using the CLI.

Gassma.migrateSheets({
spreadsheetId: "SPREAD_SHEET_ID", // defaults to the bound spreadsheet
models: [{ name: "User", columns: ["id", "name"] }],
acceptDataLoss: false,
});

models is required; omitting it raises a GassmaMissingArgumentError. The sync rules and limitations are the same as when going through the CLI.