-
Notifications
You must be signed in to change notification settings - Fork 0
Embedded DB Push
Embedded DB Push lets a production binary apply a trusted .gcorm schema
without installing the gco CLI on the target machine. Embed schema files with
Go's embed package and call dbpush.Push from an explicit migrator command,
job, or administrative startup path.
Good fit:
- First-time initialization of an empty production database.
- Self-hosted deployments where shipping one binary is simpler than installing a separate CLI.
- Internal tools or controlled environments where direct schema synchronization is acceptable.
- Tenant/database bootstrap workflows.
Use reviewed SQL migrations instead when:
- Every production change must be reviewed as a stable SQL file.
- You need custom data backfills.
- Multiple application versions run at the same time during deploys.
- Destructive schema changes need a manual rollout plan.
Keep production credentials out of .gcorm files. Prefer env() in the
datasource block:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Embed the schema directory in the binary:
package main
import (
"context"
"database/sql"
"embed"
"fmt"
"os"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/arsfy/gcorm/pkg/tooling/dbpush"
)
//go:embed schema/*.gcorm
var schemaFS embed.FS
func main() {
ctx := context.Background()
dsn := os.Getenv("DATABASE_URL")
db, err := sql.Open("pgx", dsn)
if err != nil {
panic(err)
}
defer db.Close()
result, err := dbpush.Push(ctx, db, dbpush.Options{
SchemaFS: schemaFS,
SchemaRoot: "schema",
DatabaseURL: dsn,
AllowDestructive: false,
})
if err != nil {
panic(err)
}
fmt.Printf("db push: models=%d changes=%d hash=%s noop=%v\n",
result.ModelCount, result.ChangeCount, result.SchemaHash, result.Noop)
}For MySQL or SQLite, open the database with the matching database/sql driver
and import that driver in the migrator binary.
When the database has no user tables, dbpush.Push introspects an empty schema,
diffs it against the embedded .gcorm schema, and applies the full set of
CREATE TABLE, index, unique constraint, and foreign key statements that the
DDL generator supports.
This is equivalent to synchronizing from an empty database to the target schema. It does not require a migration directory.
On every run, dbpush.Push:
- Compiles the embedded
.gcormfiles. - Resolves the connection URL from
Options.DatabaseURLor the schema datasource. - Introspects the live database.
- Computes a diff from the live database to the target schema.
- Refuses destructive or review-required changes unless
AllowDestructiveis true. - Executes supported SQL. PostgreSQL and SQLite use a transaction.
- Records a row in
__gco_schema_pushes.
For MySQL, DDL statements are not atomic. MySQL implicitly commits most DDL, so
a multi-statement push can partially apply if a later statement fails. In that
case, inspect the database state and rerun Push or repair it manually before
continuing the deployment. On MySQL execution failures, Push may return both
an error and a non-nil Result; check Result.AppliedStatements to see which
user DDL statements completed before the error.
If the live database already matches the embedded schema, the result has
Noop=true, no user schema SQL is executed, and a noop metadata row is still
recorded.
Successful pushes create and write to __gco_schema_pushes. The table stores:
- Schema hash.
- Provider.
- Model count.
- Change count.
- Applied time.
- GCORM tool version.
GCORM ignores __gco_schema_pushes and __gco_migrations during introspection,
so metadata tables do not appear as drift from the .gcorm schema.
The metadata table is for audit and visibility. Live database introspection is still the source of truth for deciding what SQL to apply.
Use DryRun to inspect the generated SQL statements without applying them:
result, err := dbpush.Push(ctx, db, dbpush.Options{
SchemaFS: schemaFS,
SchemaRoot: "schema",
DatabaseURL: os.Getenv("DATABASE_URL"),
DryRun: true,
})DryRun does not create user tables and does not write __gco_schema_pushes.
By default it still introspects the live database so the plan reflects current
database state. On large databases, this can run catalog queries over tables,
columns, indexes, and foreign keys.
For a pure local initialization plan that does not touch the database, combine
DryRun with SkipIntrospection:
result, err := dbpush.Push(ctx, nil, dbpush.Options{
SchemaFS: schemaFS,
SchemaRoot: "schema",
DryRun: true,
SkipIntrospection: true,
})SkipIntrospection is only valid with DryRun; it plans SQL from an empty
database to the embedded schema.
- Run embedded DB Push from one explicit migrator job or admin command, not from every web worker automatically.
- Locks are enabled by default. Use
DisableLock=trueonly for single-process test workflows where external serialization is already guaranteed. - Keep
AllowDestructive=falseby default. - Treat MySQL push failures as potentially partial because MySQL DDL is not transactional.
- Use a separate database credential with DDL permissions for the migrator.
- Keep application runtime credentials more restrictive when possible.
- Back up important databases before applying schema changes.
- Use
gco migrate init-sqlor reviewed migrations when you need stable SQL artifacts for approval.