Database: Schema Builder
- Introduction
- Configuration
- Builder Entry Points
- Creating Tables
- Updating Tables
- Renaming and Dropping Tables
- Available Column Types
- Column Modifiers
- Modifying Columns
- Dropping Columns
- Indexes
- Foreign Key Constraints
- Metadata Inspection
- Conditional Execution
- Foreign Key Constraint Toggles
- Database & Extension Management
- SyncModels Transition
- Dialect Compatibility
- Laravel Schema Mapping
Introduction
PrismGo's database/schema package provides a Laravel-style Schema Builder for declaring database table structures in Go migrations. It is built on top of GORM, with optional dialect capabilities carried by the GORM Dialector:
- MySQL — the production-primary dialect built into the framework core
- SQLite — a test/development dialect available after installing
github.com/prismgo/sqlite
When the SQLite extension is not installed, or a Dialector does not provide the required capability, the operation returns schema.ErrUnsupportedFeature to prevent generating unverified SQL. See Database: SQLite for installation. Once installed, schema.New(db), schema.Bind(db), and migration calls remain unchanged.
SQLite schema migrations must run serially. SQLite foreign-key PRAGMAs and attached databases are physical-connection scoped, and concurrent schema changes are not supported. Use db.Connection and create the Schema builder from that pinned connection when calling constraint toggles or inspecting an attached schema.
The Schema Builder is ideal for:
- Creating tables with a declarative API
- Altering existing tables (adding, modifying, renaming, dropping columns)
- Managing indexes and foreign key constraints
- Inspecting database metadata (tables, columns, indexes, views, schemas, foreign keys)
New migrations should prefer
schema.Create/schema.Tableover model-driven approaches. TheSyncModelsmethod exists only for transitioning legacy GORM model-based migrations.
Configuration
The Schema Builder does not require its own configuration file. Global defaults that affect column type behaviour are set programmatically via package-level functions in your AppServiceProvider or bootstrap:
Configuration Parameters
| Function | Default | Description |
|---|---|---|
schema.DefaultStringLength(length) | 255 | Default length for String / Char columns when no explicit length is given. Set to 191 for MySQL < 5.7 or InnoDB large prefix compatibility. |
schema.DefaultTimePrecision(precision) | nil (no precision) | Default fractional seconds precision for DateTime, Time, and Timestamp columns. Pass nil or a negative value to disable. |
schema.DefaultMorphKeyType(kind) | "int" | Data type for morph ID columns ({name}_id). Accepts "int", "uuid", or "ulid". |
schema.MorphUsingUuids() | — | Shortcut for DefaultMorphKeyType("uuid"). Morph ID columns become char(36). |
schema.MorphUsingUlids() | — | Shortcut for DefaultMorphKeyType("ulid"). Morph ID columns become char(26). |
Usage example in your application service provider:
import "github.com/prismgo/framework/database/schema"
func Boot() {
schema.DefaultStringLength(191)
precision := 3
schema.DefaultTimePrecision(&precision)
schema.MorphUsingUuids()
}These defaults affect:
String/Charcolumns without an explicit lengthDateTime/Time/Timestampcolumns without an explicit precisionMorphs/NullableMorphsID column typesSyncModelswhen parsing model struct fields
Explicit gorm tags (size, type, precision) on model fields are never overridden.
Builder Entry Points
The Builder is the central object that executes schema operations. There are four ways to obtain one:
Binding a Migration Connection
Migration functions receive a *gorm.DB from the migrator. Use Bind(db) to attach the Schema Builder to that specific connection or transaction:
func up(db *gorm.DB) error {
builder := schema.Bind(db)
if err := builder.Create("customers", func(table *schema.Blueprint) {
table.Id()
table.String("name", 100).Comment("Customer name")
}); err != nil {
return err
}
return builder.Table("customers", func(table *schema.Blueprint) {
table.String("phone", 32).Nullable().Comment("Phone number")
})
}Bind returns the same Builder instance for chaining. In migrations, always prefer Bind(db) to prevent accidentally using the unbound default connection.
The New Constructor
Create a standalone Builder bound to a specific *gorm.DB:
builder := schema.New(db)
err := builder.Create("demo_tables", func(table *schema.Blueprint) {
table.Id()
table.String("code", 32).Unique()
})Specifying a Named Connection
Use a connection defined in config/database.go under database.connections.{name}:
err := schema.Connection("mysql").Create("external_logs", func(table *schema.Blueprint) {
table.Id()
table.Text("message")
table.Timestamps()
})If the connection fails to open, the error is stored in the Builder and returned when a schema operation is executed.
Package-Level Facade
All builder methods are exposed as package-level functions that delegate to the default builder resolved from the application container:
err := schema.Create("demo_tables", func(table *schema.Blueprint) {
table.Id()
})
exists := schema.HasTable("demo_tables")This is convenient for standalone scripts and tests, but migrations should still use schema.Bind(db) to stay within the migration transaction.
Creating Tables
Create(table, fn) creates a new table. If the table already exists, it is skipped (idempotent):
err := schema.Bind(db).Create("customers", func(table *schema.Blueprint) {
table.Id()
table.UnsignedBigInteger("tenant_id").Comment("Tenant ID")
table.String("name", 100).Comment("Customer name")
table.String("phone", 32).Nullable().Comment("Phone number")
table.TinyInteger("status").Default(1).Comment("Status: 1=active 2=disabled")
table.Decimal("balance", 12, 2).Default(0).Comment("Account balance")
table.Json("profile").Nullable().Comment("Extended profile")
table.Timestamps()
table.SoftDeletes()
table.IndexNamed("idx_customers_tenant_status", "tenant_id", "status")
table.UniqueNamed("uix_customers_tenant_phone", "tenant_id", "phone")
})Notes:
- At least one column is required, or the builder returns an error.
- MySQL automatically appends
database.TableOptions(ENGINE=InnoDB DEFAULT CHARSET=utf8mb4). - SQLite ignores MySQL-specific syntax (comments, character sets, foreign key constraints).
Updating Tables
Table(table, fn) modifies an existing table. It supports adding columns, modifying columns, renaming columns, dropping columns, and managing indexes and foreign keys.
Adding Columns
New columns are skipped if they already exist (idempotent):
err := schema.Bind(db).Table("customers", func(table *schema.Blueprint) {
table.String("email", 128).Nullable().After("phone").Comment("Email")
table.Timestamp("email_verified_at").Nullable().Comment("Email verified at")
table.IndexNamed("idx_customers_email", "email")
})Modifying Columns
Use Change() to modify an existing column's type, nullable state, or default value:
err := schema.Bind(db).Table("customers", func(table *schema.Blueprint) {
table.String("phone", 64).Nullable().Comment("Phone number").Change()
})Change() compiles to ALTER TABLE ... MODIFY COLUMN on MySQL. SQLite does not support safe column modification and returns schema.ErrUnsupportedFeature.
Renaming Columns
err := schema.Bind(db).Table("customers", func(table *schema.Blueprint) {
table.RenameColumn("mobile", "phone")
})The operation is skipped if the source column does not exist or the target column already exists.
Dropping Columns
err := schema.Bind(db).Table("customers", func(table *schema.Blueprint) {
table.DropColumn("legacy_code", "legacy_level")
table.DropColumns([]string{"old_name", "old_phone"})
})Non-existent columns are silently ignored.
Raw SQL Commands
For one-off structural fixes that cannot be expressed through the DSL:
err := schema.Bind(db).Table("customers", func(table *schema.Blueprint) {
table.Raw("ALTER TABLE `customers` ADD COLUMN `search_name` varchar(255) GENERATED ALWAYS AS (`name`) STORED")
})Regular column, index, and foreign key operations should use Blueprint methods for testability and dialect compatibility.
Renaming and Dropping Tables
Renaming / Dropping
Idempotent table-level operations:
builder := schema.Bind(db)
// Rename: skips if source is missing or target already exists
err := builder.Rename("old_customers", "customers")
// Drop: skips if table does not exist
err = builder.Drop("temp_customers")
err = builder.DropIfExists("temp_customers")
// Drop specified columns from a table
err = builder.DropColumns("customers", "legacy_code", "legacy_level")Destructive Operations
Use with extreme caution — these are intended for test databases and explicit cleanup commands only:
err := schema.Bind(db).DropAllTables()
err = schema.Bind(db).DropAllViews()
err = schema.Bind(db).DropAllTypes()DropAllTables temporarily disables foreign key checks before dropping.
Available Column Types
Primary Keys & Increments
| Method | MySQL Type | Description |
|---|---|---|
Id(name ...string) | bigint unsigned auto_increment primary key | Default primary key; falls back to "id" if no name given |
Increments(name) | int unsigned auto_increment primary key | Auto-incrementing int primary key |
BigIncrements(name) | bigint unsigned auto_increment primary key | Auto-incrementing bigint primary key |
TinyIncrements(name) | tinyint unsigned auto_increment primary key | Auto-incrementing tinyint primary key |
SmallIncrements(name) | smallint unsigned auto_increment primary key | Auto-incrementing smallint primary key |
MediumIncrements(name) | mediumint unsigned auto_increment primary key | Auto-incrementing mediumint primary key |
Integer(name) | int | Signed integer |
BigInteger(name) | bigint | Signed big integer |
MediumInteger(name) | mediumint | Signed medium integer |
SmallInteger(name) | smallint | Signed small integer |
TinyInteger(name) | tinyint | Signed tiny integer |
UnsignedInteger(name) | int unsigned | Unsigned integer |
UnsignedBigInteger(name) | bigint unsigned | Unsigned big integer |
UnsignedMediumInteger(name) | mediumint unsigned | Unsigned medium integer |
UnsignedSmallInteger(name) | smallint unsigned | Unsigned small integer |
UnsignedTinyInteger(name) | tinyint unsigned | Unsigned tiny integer |
table.Id()
table.UnsignedBigInteger("tenant_id").Comment("Tenant ID")
table.TinyInteger("status").Default(1).Comment("Status: 1=active 2=disabled")String & Text Types
| Method | MySQL Type | Description |
|---|---|---|
String(name, length ...int) | varchar(length) | Variable-length string; defaults to DefaultStringLength (255) |
Char(name, length ...int) | char(length) | Fixed-length string |
Text(name) | text | Long text |
TinyText(name) | tinytext | Tiny text |
MediumText(name) | mediumtext | Medium text |
LongText(name) | longtext | Long text |
Uuid(name) | char(36) | UUID |
Ulid(name) | char(26) | ULID |
IpAddress(name) | varchar(45) | IP address (supports both IPv4 and IPv6) |
MacAddress(name) | varchar(17) | MAC address |
RememberToken() | varchar(100) null | Laravel-style remember_token field |
table.String("name", 100).Comment("Name")
table.Text("description").Nullable().Comment("Description")
table.Uuid("uuid").Unique()
table.RememberToken()Numeric Types
| Method | MySQL Type | Description |
|---|---|---|
Boolean(name) | tinyint(1) | Boolean value |
Float(name, totalAndPlaces ...int) | float(total, places) | Floating-point number; defaults to (8,2) |
Double(name, totalAndPlaces ...int) | double(total, places) | Double-precision float; defaults to (8,2) |
Decimal(name, totalAndPlaces ...int) | decimal(total, places) | Exact decimal; defaults to (8,2) |
UnsignedDecimal(name, totalAndPlaces ...int) | decimal(total, places) unsigned | Unsigned exact decimal |
Do not use
Float/Doublefor monetary values. Always useDecimalor store amounts as the smallest currency unit (integer).
table.Boolean("is_enabled").Default(true).Comment("Is enabled")
table.Decimal("amount", 12, 2).Default(0).Comment("Amount")Date & Time Types
| Method | MySQL Type | Description |
|---|---|---|
Date(name) | date | Date value |
DateTime(name) | datetime | Date and time |
DateTimeTz(name) | datetime | Date and time (timezone-aware alias, same SQL) |
Time(name) | time | Time value |
TimeTz(name) | time | Time (timezone-aware alias, same SQL) |
Timestamp(name) | timestamp | Timestamp |
TimestampTz(name) | timestamp | Timestamp (timezone-aware alias, same SQL) |
Year(name) | year | Year value |
Timestamps() | created_at, updated_at nullable timestamp | Two nullable timestamp fields |
TimestampsTz() | Same as Timestamps() | Alias (same SQL) |
SoftDeletes() | deleted_at nullable timestamp + index | Soft delete field with default index |
SoftDeletesTz() | Same as SoftDeletes() | Alias (same SQL) |
table.Timestamp("published_at").Nullable()
table.Timestamp("created_at").UseCurrent()
table.Timestamp("updated_at").UseCurrent().UseCurrentOnUpdate()
table.Timestamps()
table.SoftDeletes()Boolean, JSON, Binary, Enum & Spatial Types
| Method | MySQL Type | Description |
|---|---|---|
Binary(name) | blob | Binary data |
Json(name) | json | JSON data |
Jsonb(name) | json | JSONB alias; compiles to json on MySQL/SQLite |
Enum(name, allowed) | enum(...) | Enumeration |
Set(name, allowed) | set(...) | MySQL set type |
Geometry(name) | geometry | Spatial geometry |
Geography(name) | geography | Spatial geography |
Point(name) | point | Spatial point |
LineString(name) | linestring | Spatial linestring |
Polygon(name) | polygon | Spatial polygon |
Vector(name, dimensions) | vector(dimensions) | Vector field (e.g., for embeddings) |
SQLite downgrades most MySQL-specific types to compatible equivalents during testing.
table.Json("payload").Nullable()
table.Enum("source", []string{"manual", "import"}).Default("manual")
table.Vector("embedding", 1536).Nullable()Foreign ID & Morph Types
table.ForeignId("customer_id") // bigint unsigned
table.ForeignIdFor("owner_id") // bigint unsigned
table.Morphs("owner") // {name}_id bigint unsigned, {name}_type varchar(255), joint index
table.NullableMorphs("source") // Same as Morphs but nullableBehaviour:
ForeignId(name)creates abigint unsignedcolumn.ForeignIdFor(name)is a direct alias; pass the final column name.Morphs(name)creates{name}_id,{name}_type, and a composite index.NullableMorphs(name)is the same but both columns allowNULL.- The
{name}_idtype is controlled byDefaultMorphKeyType(int,uuid, orulid).
Convenience Methods
| Method | Description |
|---|---|
NullableTimestamps() | Alias for Timestamps() |
NullableMorphs(name) | Nullable variant of Morphs |
Column Modifiers
Each column method returns a *ColumnDefinition, enabling chainable modifiers:
table.String("name", 100).
Nullable().
Default("unknown").
Comment("Name").
After("tenant_id").
Index()| Modifier | Description |
|---|---|
Nullable() | Allow NULL values |
NotNull() | Disallow NULL values (default for non-primary columns) |
Unsigned() | Mark numeric column as unsigned |
AutoIncrement() | Set auto-increment |
Primary() | Set as primary key |
Unique(enabled ...bool) | Add a unique index on the column; pass false to drop by default name |
Index(enabled ...bool) | Add a regular index on the column; pass false to drop by default name |
Default(value any) | Set a default value |
Comment(value string) | Set column comment (MySQL only) |
First() | Place column first in table (MySQL only) |
After(column string) | Place column after another (MySQL only) |
Charset(value string) | Set character set (MySQL string columns only) |
Collation(value string) | Set collation (MySQL string columns only) |
UseCurrent() | Set default to CURRENT_TIMESTAMP |
UseCurrentOnUpdate() | Set ON UPDATE CURRENT_TIMESTAMP |
Invisible() | Make column invisible (MySQL only) |
Change() | Mark as modification of existing column (MySQL MODIFY COLUMN; SQLite returns error) |
StoredAs(expr string) | Declare stored generated column (API placeholder, not compiled) |
VirtualAs(expr string) | Declare virtual generated column (API placeholder, not compiled) |
From(value int) | Laravel compatibility placeholder; no effect |
Instant() | Laravel compatibility placeholder; no effect |
Lock(value string) | Laravel compatibility placeholder; no effect |
Default value SQL literals:
table.String("name").Default("guest") // DEFAULT 'guest'
table.Boolean("is_enabled").Default(true) // DEFAULT 1
table.Integer("sort").Default(0) // DEFAULT 0
table.Timestamp("created_at").Default("CURRENT_TIMESTAMP")
table.Json("payload").Default("NULL")CURRENT_TIMESTAMP, NULL, and parenthesised expressions are preserved as-is.
Modifying Columns
Use the Change() modifier to update an existing column's type, nullable state, or default value:
err := schema.Bind(db).Table("customers", func(table *schema.Blueprint) {
table.String("phone", 64).Nullable().Comment("Phone number").Change()
})Behaviour:
- MySQL: Compiles to
ALTER TABLE ... MODIFY COLUMN. - SQLite: Returns
schema.ErrUnsupportedFeature— SQLite cannot safely modify column types, nullability, or defaults without table rebuild. - Idempotent: The
Change()modifier does not check whether the column already matches the desired definition. Callers must ensure the operation is meaningful.
Dropping Columns
err := schema.Bind(db).Table("customers", func(table *schema.Blueprint) {
table.DropColumn("legacy_code")
table.DropColumns([]string{"old_name", "old_phone"})
})Non-existent columns are silently ignored.
Dropping Convention Columns
table.DropRememberToken() // Drops remember_token
table.DropTimestamps() // Drops created_at, updated_at
table.DropTimestampsTz() // Same as DropTimestamps
table.DropSoftDeletes() // Drops deleted_at + default index
table.DropSoftDeletesTz() // Same as DropSoftDeletes
table.DropMorphs("owner") // Drops owner_id, owner_type + default index
table.DropConstrainedForeignId("customer_id") // Drops foreign key constraint + column
table.DropForeignIdFor("owner_id") // Drops column only, keeps constraintIndexes
Creating Indexes
table.Primary("id")
table.Unique("tenant_id", "code")
table.Index("tenant_id", "status")
table.FullText("title", "content")
table.SpatialIndex("location")
// Custom index names
table.UniqueNamed("uix_customers_tenant_phone", "tenant_id", "phone")
table.IndexNamed("idx_customers_tenant_status", "tenant_id", "status")Column-level indexes:
table.String("email", 128).Unique()
table.String("phone", 32).Index()Default index naming convention (Laravel-style):
{table}_{column1}_{column2}_{kind}Examples:
customers_email_uniquecustomers_tenant_id_status_index
For composite indexes or business-critical indexes, use explicit naming to guarantee stable names for rollbacks:
- Prefix:
idx_(plain),uix_(unique),ftx_(fulltext),spx_(spatial) - Names exceeding 64 characters are auto-trimmed with a SHA1 hash suffix for MySQL compatibility.
Renaming Indexes
table.RenameIndex("idx_old_name", "idx_new_name")- MySQL: Compiles to
ALTER TABLE ... RENAME INDEX .... - SQLite: Skipped (SQLite's native rename index support is limited).
Dropping Indexes
table.DropIndex("idx_customers_email")
table.DropUnique("uix_customers_tenant_phone")
table.DropPrimary()
table.DropFullText("ftx_articles_title")
table.DropSpatialIndex("spx_locations_point")The Index(true/false) and Unique(true/false) column-level modifiers can also remove indexes:
// Removes the unique index on "phone" by default name
table.String("phone", 32).Unique(false).Change()Foreign Key Constraints
Creating Foreign Keys
Constrained foreign ID (Laravel-style):
table.ForeignId("customer_id").
Constrained().
CascadeOnDelete().
CascadeOnUpdate()Constrained() resolves the referenced table and column:
- Strips
_idsuffix and pluralises:customer_id→customers - Default referenced column:
id - Explicit override:
Constrained("users", "id")
Manual foreign key:
table.Foreign("tenant_id").
References("id").
On("tenants").
CascadeOnDelete().
CascadeOnUpdate()Foreign key actions:
| Method | SQL Action |
|---|---|
OnUpdate(action string) | Custom ON UPDATE action |
OnDelete(action string) | Custom ON DELETE action |
CascadeOnUpdate() | ON UPDATE CASCADE |
CascadeOnDelete() | ON DELETE CASCADE |
RestrictOnUpdate() | ON UPDATE RESTRICT |
RestrictOnDelete() | ON DELETE RESTRICT |
NullOnUpdate() | ON UPDATE SET NULL |
NullOnDelete() | ON DELETE SET NULL |
NoActionOnUpdate() | ON UPDATE NO ACTION |
NoActionOnDelete() | ON DELETE NO ACTION |
Custom constraint name:
table.ForeignId("owner_id").
Constrained("users", "id").
Name("fk_customers_owner").
RestrictOnDelete()Foreign key SQL is only generated for MySQL. SQLite skips foreign key constraint statements in CREATE TABLE.
Dropping Foreign Keys
table.DropForeign("fk_customers_owner") // Drop constraint by name
table.DropConstrainedForeignId("customer_id") // Drop constraint by convention name + column
table.DropForeignIdFor("owner_id") // Drop column only, keep constraintDifferences:
DropForeign(name)— removes only the foreign key constraint.DropConstrainedForeignId(column)— removes the constraint (by convention name) and the column.DropForeignIdFor(column)— removes only the column.
Metadata Inspection
The Builder provides read-only inspection methods for database metadata.
Table / View / Schema Inspection
builder := schema.Bind(db)
hasTable := builder.HasTable("customers")
hasView := builder.HasView("active_customers")
tables, err := builder.GetTables(nil) // []TableInfo
tableNames, err := builder.GetTableListing(nil) // []string (schema-qualified by default)
views, err := builder.GetViews(nil) // []ViewInfo
schemas, err := builder.GetSchemas() // []SchemaInfo
types, err := builder.GetTypes(nil) // []TypeInfo (empty on MySQL/SQLite)schemaFilteracceptsnil, astring(single schema), or[]string(multiple schemas).GetTableListingtakes an optionalschemaQualifiedvariadic boolean (defaults totrue).
Column Inspection
hasColumn := builder.HasColumn("customers", "phone")
hasColumns := builder.HasColumns("customers", []string{"tenant_id", "phone"})
columns, err := builder.GetColumns("customers") // []ColumnInfo
columnNames, err := builder.GetColumnListing("customers") // []string
columnType, err := builder.GetColumnType("customers", "phone", true) // stringGetColumnType accepts an optional fullDefinition boolean:
false(default): returns the short type name (e.g.,varchar)true: returns the full type definition (e.g.,varchar(32) unsigned)
Index Inspection
hasIndex := builder.HasIndex("customers", "idx_customers_email")
hasIndexCol := builder.HasIndex("customers", []string{"tenant_id", "status"}) // by columns
hasIndexTyp := builder.HasIndex("customers", "idx_customers_email", "unique") // by type filter
indexes, err := builder.GetIndexes("customers") // []IndexInfo
indexNames, err := builder.GetIndexListing("customers") // []stringForeign Key Inspection
foreignKeys, err := builder.GetForeignKeys("customers") // []ForeignKeyInfoMetadata Structure Types
| Type | Key Fields |
|---|---|
SchemaInfo | Name |
TableInfo | Name, Schema, Type |
ViewInfo | Name, Schema, Definition |
TypeInfo | Name, Schema, Type |
ColumnInfo | Name, Type, FullType, Nullable, Default, Comment, Primary, AutoIncrement, Unique, Length, Precision, Scale |
IndexInfo | Name, Columns, Type, Unique, Primary |
ForeignKeyInfo | Name, Columns, ForeignTable, ForeignColumns, OnUpdate, OnDelete |
Conditional Execution
Conditional helpers keep migrations idempotent across environments with different schema states.
Column Conditionals
builder := schema.Bind(db)
// Execute only if the column exists
err := builder.WhenTableHasColumn("customers", "mobile", func() error {
return builder.Table("customers", func(table *schema.Blueprint) {
table.RenameColumn("mobile", "phone")
})
})
// Execute only if the column is missing
err = builder.WhenTableDoesntHaveColumn("customers", "email", func() error {
return builder.Table("customers", func(table *schema.Blueprint) {
table.String("email", 128).Nullable()
})
})Index Conditionals
// By index name
err := builder.WhenTableDoesntHaveIndex("customers", "idx_customers_email", func() error {
return builder.Table("customers", func(table *schema.Blueprint) {
table.IndexNamed("idx_customers_email", "email")
})
})
// By column list (with optional type filter)
err = builder.WhenTableDoesntHaveIndex("customers", []string{"tenant_id", "phone"}, func() error {
return builder.Table("customers", func(table *schema.Blueprint) {
table.UniqueNamed("uix_customers_tenant_phone", "tenant_id", "phone")
})
}, "unique")Foreign Key Constraint Toggles
Temporarily disable and re-enable foreign key constraint checks:
builder := schema.Bind(db)
err := builder.DisableForeignKeyConstraints()
err = builder.EnableForeignKeyConstraints()Execute a callback with foreign key checks disabled (always attempts re-enable):
err := schema.Bind(db).WithoutForeignKeyConstraints(func() error {
if err := schema.Bind(db).DropIfExists("legacy_items"); err != nil {
return err
}
return schema.Bind(db).DropIfExists("legacy_orders")
})Dialect behaviour:
| Dialect | Mechanism |
|---|---|
| MySQL | SET FOREIGN_KEY_CHECKS=0/1 |
| SQLite | PRAGMA foreign_keys = OFF/ON |
| Other | Returns schema.ErrUnsupportedFeature |
WithoutForeignKeyConstraints always attempts to re-enable constraints after the callback, even if the callback returns an error. The callback error takes precedence.
Database & Extension Management
created, err := schema.Bind(db).CreateDatabase("archive") // MySQL only
dropped, err := schema.Bind(db).DropDatabaseIfExists("archive") // MySQL only
err = schema.Bind(db).EnsureExtensionExists("vector") // Unsupported on MySQL/SQLite
err = schema.Bind(db).EnsureVectorExtensionExists() // Unsupported on MySQL/SQLiteNotes:
CreateDatabaseandDropDatabaseIfExistsexecute only on MySQL; other dialects returnErrUnsupportedFeature.- MySQL and SQLite do not support extension management, so
EnsureExtensionExistsandEnsureVectorExtensionExistsreturnErrUnsupportedFeature.
SyncModels Transition
SyncModels(models ...any) provides a migration path for legacy GORM model-driven migrations:
err := schema.Bind(db).SyncModels(
&model.User{},
&model.Customer{},
)Behaviour:
- If the model's table does not exist, it is created via the GORM migrator.
- If the table exists, only columns present in the model struct but missing from the database are added.
- MySQL table options (
InnoDB,utf8mb4) are automatically applied. - Schema defaults (
DefaultStringLength,DefaultTimePrecision,DefaultMorphKeyType) are applied to model field parsing. - Does not call GORM
AutoMigratedirectly.
Recommendations:
- Existing total migrations can continue using
SyncModelsfor legacy models. - New tables, columns, indexes, and foreign keys should use
Create/Tableexplicitly. - Indexes, foreign keys, and complex column changes should not rely on model inference.
Dialect Compatibility
| Feature | MySQL | SQLite |
|---|---|---|
| Create table | Supported | Supported |
| Add column | Supported | Supported |
Modify column (Change()) | Supported | Returns ErrUnsupportedFeature |
| Rename column | Supported | Supported |
| Drop column | Supported | Supported (limited) |
| Column comments | Supported | Ignored |
First / After positioning | Supported | Ignored |
| Charset / Collation | Supported | Ignored |
| Invisible column | Supported | Ignored |
| Plain / Unique indexes | Supported | Supported |
| Fulltext / Spatial indexes | Supported | Downgraded to plain index |
| Rename index | Supported | Skipped |
| Foreign key constraints SQL | Supported | Skipped |
| Foreign key constraint toggles | Supported | Supported |
| Create / Drop database | Supported | Unsupported |
| Extension management | Unsupported | Unsupported |
Other database dialects return schema.ErrUnsupportedFeature to prevent generating untested SQL.
Laravel Schema Mapping
| Laravel Concept | PrismGo Equivalent |
|---|---|
Schema::create($table, $callback) | schema.Create(table, fn) |
Schema::table($table, $callback) | schema.Table(table, fn) |
Schema::drop($table) / dropIfExists | schema.Drop / schema.DropIfExists |
Schema::rename($from, $to) | schema.Rename(from, to) |
Schema::hasTable / hasColumn | schema.HasTable / schema.HasColumn |
Blueprint::id() | table.Id() |
Blueprint::string($name, $len) | table.String(name, len) |
Blueprint::text() / json() / boolean() | table.Text() / table.Json() / table.Boolean() |
Blueprint->nullable() | .Nullable() |
Blueprint->default($value) | .Default(value) |
Blueprint->unique() | .Unique() |
Blueprint->index() | .Index() |
Blueprint->change() | .Change() |
Blueprint->foreignId()->constrained() | table.ForeignId(name).Constrained() |
Blueprint->timestamps() | table.Timestamps() |
Blueprint->softDeletes() | table.SoftDeletes() |
Blueprint->morphs($name) | table.Morphs(name) |
Schema::defaultStringLength($len) | schema.DefaultStringLength(len) |
Schema::morphUsingUuids() | schema.MorphUsingUuids() |