Skip to content

feat: add ForeignID, ForeignUlid, and ForeignUuid methods to Blueprint for migration #604#1187

Merged
hwbrzzl merged 3 commits intogoravel:masterfrom
alfanzain:feat/add-methods-to-create-foreign-keys-migration
Sep 16, 2025
Merged

feat: add ForeignID, ForeignUlid, and ForeignUuid methods to Blueprint for migration #604#1187
hwbrzzl merged 3 commits intogoravel:masterfrom
alfanzain:feat/add-methods-to-create-foreign-keys-migration

Conversation

@alfanzain
Copy link
Contributor

@alfanzain alfanzain commented Sep 6, 2025

📑 Description

Closes goravel/goravel#604

Implemented ForeignID, ForeignUlid, and ForeignUuid methods in the Blueprint class to facilitate the creation of foreign key unsigned big integer, unsigned ULID, and unsigned UUID columns, respectively, in database migrations.

These methods offer a more efficient way to define foreign key columns with appropriate data types or constraints, improving both the flexibility and clarity of migration scripts.

New foreign keys methods:

ForeignID()

This method creates an unsigned big integer equivalent column.

table.ForeignID("role_id")

ForeignUlid()

This method creates a ULID equivalent column.

table.ForeignUlid("role_id")

ForeignUuid()

This method creates a UUID equivalent column.

table.ForeignUuid("role_id")

Extended methods for constraints:

Constrained()

These new methods support creating foreign key constraints, like Foreign():

table.ForeignID("role_id").References("id").On("roles")

With this new Constrained(), the example above can be written more shorter like this:

table.ForeignID("role_id").Constrained("roles", "id")

Evidences:

I ran migrations on goravel/examples to test that this feature works as expected. Here's the migration file:

package migrations

import (
	"github.com/goravel/framework/contracts/database/schema"
	"github.com/goravel/framework/facades"
)

type M20210101000002CreateUsersTable struct {
}

// Signature The unique signature for the migration.
func (r *M20210101000002CreateUsersTable) Signature() string {
	return "20210101000002_create_users_table"
}

// Up Run the migrations.
// Up Run the migrations.
func (r *M20210101000002CreateUsersTable) Up() error {
	tableRole1 := "roles_1"
	tableRole2 := "roles_2"
	tableRole3 := "roles_3"

	if err := facades.Schema().Create(tableRole1, func(table schema.Blueprint) {
		table.BigIncrements("id")
	}); err != nil {
		return err
	}

	if err := facades.Schema().Create(tableRole2, func(table schema.Blueprint) {
		table.Ulid("id")
		table.Primary("id")
	}); err != nil {
		return err
	}

	if err := facades.Schema().Create(tableRole3, func(table schema.Blueprint) {
		table.Uuid("id")
		table.Primary("id")
	}); err != nil {
		return err
	}

	if !facades.Schema().HasTable("users") {
		if err := facades.Schema().Create("users", func(table schema.Blueprint) {
			// Primary key
			table.BigIncrements("id")

			// Simple unsigned bigint
			table.UnsignedBigInteger("simple_bigint")

			// ForeignID variations
			table.ForeignID("foreign_id_no_constraint")
			table.ForeignID("foreign_id_constrained").Constrained(tableRole1, "id")
			table.ForeignID("foreign_id_custom").References("id").On(tableRole1)

			// ForeignUlid variations
			table.ForeignUlid("foreign_ulid1") // default length
			table.ForeignUlid("foreign_ulid2").Constrained(tableRole2, "id")
			table.ForeignUlid("foreign_ulid3", 32) // custom length
			table.ForeignUlid("foreign_ulid4", 32).Constrained(tableRole2, "id")
			table.ForeignUlid("foreign_ulid5", 32).References("id").On(tableRole2)

			// ForeignUuid variations
			table.ForeignUuid("foreign_uuid1") // basic
			table.ForeignUuid("foreign_uuid2").Constrained(tableRole3, "id")
			table.ForeignUuid("foreign_uuid3").References("id").On(tableRole3)

			// ULID and UUID (regular columns)
			table.Ulid("ulid_column")
			table.Uuid("uuid_column")

			// Char with custom length
			table.Char("char_column", 50)

			// Timestamps & soft deletes
			table.Timestamps()
			table.SoftDeletes()

			// Table comment
			table.Comment("Test table for all ForeignID, ForeignUlid, ForeignUuid, and related column types")
		}); err != nil {
			return err
		}
	}

	return nil
}

// Down Reverse the migrations.
func (r *M20210101000002CreateUsersTable) Down() error {
	return facades.Schema().DropIfExists("users")
}

This is the query result on the command line.

  INFO    Running: 20210101000002_create_users_table
[2025-09-08 15:08:06.388] local.info: [112.960ms] [rows:0] create table "roles_1" ("id" bigserial primary key not null)
[2025-09-08 15:08:06.389] local.info: [0.644ms] [rows:0] create table "roles_2" ("id" char(26) not null)
[2025-09-08 15:08:06.483] local.info: [94.365ms] [rows:0] alter table "roles_2" add primary key ("id")
[2025-09-08 15:08:06.484] local.info: [0.712ms] [rows:0] create table "roles_3" ("id" uuid not null)
[2025-09-08 15:08:06.569] local.info: [84.884ms] [rows:0] alter table "roles_3" add primary key ("id")
[2025-09-08 15:08:06.57] local.info: [0.906ms] [rows:4] select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') order by c.relname
[2025-09-08 15:08:06.664] local.info: [92.963ms] [rows:0] create table "users" ("id" bigserial primary key not null, "simple_bigint" bigint not null, "foreign_id_no_constraint" bigint not null, "foreign_id_constrained" bigint not null, "foreign_id_custom" bigint not null, "foreign_ulid1" char(26) not null, "foreign_ulid2" char(26) not null, "foreign_ulid3" char(32) not null, "foreign_ulid4" char(32) not null, "foreign_ulid5" char(32) not null, "foreign_uuid1" uuid not null, "foreign_uuid2" uuid not null, "foreign_uuid3" uuid not null, "ulid_column" char(26) not null, "uuid_column" uuid not null, "char_column" char(50) not null, "created_at" timestamp(0) without time zone null, "updated_at" timestamp(0) without time zone null, "deleted_at" timestamp(0) without time zone null)
[2025-09-08 15:08:06.667] local.info: [3.210ms] [rows:0] alter table "users" add constraint "users_foreign_id_constrained_foreign" foreign key ("foreign_id_constrained") references "roles_1" ("id")
[2025-09-08 15:08:06.668] local.info: [0.679ms] [rows:0] alter table "users" add constraint "users_foreign_id_custom_foreign" foreign key ("foreign_id_custom") references "roles_1" ("id")
[2025-09-08 15:08:06.669] local.info: [0.896ms] [rows:0] alter table "users" add constraint "users_foreign_ulid2_foreign" foreign key ("foreign_ulid2") references "roles_2" ("id")
[2025-09-08 15:08:06.669] local.info: [0.400ms] [rows:0] alter table "users" add constraint "users_foreign_ulid4_foreign" foreign key ("foreign_ulid4") references "roles_2" ("id")
[2025-09-08 15:08:06.67] local.info: [0.474ms] [rows:0] alter table "users" add constraint "users_foreign_ulid5_foreign" foreign key ("foreign_ulid5") references "roles_2" ("id")
[2025-09-08 15:08:06.671] local.info: [0.849ms] [rows:0] alter table "users" add constraint "users_foreign_uuid2_foreign" foreign key ("foreign_uuid2") references "roles_3" ("id")
[2025-09-08 15:08:06.672] local.info: [0.858ms] [rows:0] alter table "users" add constraint "users_foreign_uuid3_foreign" foreign key ("foreign_uuid3") references "roles_3" ("id")
[2025-09-08 15:08:06.673] local.info: [0.493ms] [rows:0] comment on table "users" is 'Test table for all ForeignID, ForeignUlid, ForeignUuid, and related column types'
[2025-09-08 15:08:06.789] local.info: [38.946ms] [rows:1] INSERT INTO "migrations" ("batch","migration") VALUES (1,'20210101000002_create_users_table')

The foreign key columns created ForeignID, ForeignUlid, and ForeignUuid will be created as below:

image

Although the created columns are basic columns of unsigned big integer, UUID, and ULID (as the same as Laravel), the columns created with Constrained and References add constraints as below:

image

Limitation:

The Constrained can't be used without parameters for now because we need to 'magically' set the required parameter with some methods. I'm not familiar with these methods. Further guidance would be helpful to make this happen. As well as ForeignIdFor().

✅ Checks

  • Added test cases for my code

@alfanzain alfanzain requested a review from a team as a code owner September 6, 2025 19:32
@codecov
Copy link

codecov bot commented Sep 6, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.91%. Comparing base (4cd93b0) to head (fed615b).
⚠️ Report is 16 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1187      +/-   ##
==========================================
- Coverage   68.27%   67.91%   -0.37%     
==========================================
  Files         233      233              
  Lines       15071    14712     -359     
==========================================
- Hits        10290     9991     -299     
+ Misses       4401     4363      -38     
+ Partials      380      358      -22     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome 👍 Could you add some screenshots in the PR description to show that foreign columns are added as expected?

Name(name string) IndexDefinition
}

type ForeignIdColumnDefinition interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type ForeignIdColumnDefinition interface {
type ForeignIDColumnDefinition interface {


type ForeignIdColumnDefinition interface {
driver.ColumnDefinition
Constrained(table string, column string) ForeignKeyDefinition
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Constrained(table string, column string) ForeignKeyDefinition
Constrained(table, column, indexName string) ForeignKeyDefinition

…ename column into indexName on Constrained()
@alfanzain
Copy link
Contributor Author

Awesome 👍 Could you add some screenshots in the PR description to show that foreign columns are added as expected?

Done @hwbrzzl
Please kindly check it

blueprint *Blueprint
}

func (r *ForeignIDColumnDefinition) Constrained(table string, indexName string) schema.ForeignKeyDefinition {
Copy link
Contributor

@hwbrzzl hwbrzzl Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be implemented as below:

image image

https://github.com/laravel/framework/blob/bb00401b3979c507ec614d537ea7518cb065eac0/src/Illuminate/Database/Schema/ForeignIdColumnDefinition.php#L52

Suggested change
func (r *ForeignIDColumnDefinition) Constrained(table string, indexName string) schema.ForeignKeyDefinition {
func (r *ForeignIDColumnDefinition) Constrained(table, column, indexName string) schema.ForeignKeyDefinition {

Comment on lines +127 to +145
func (r *ForeignIDColumnDefinition) Constrained(table, column, indexName string) schema.ForeignKeyDefinition {
if column == "" {
column = "id"
}

if table == "" {
name := r.GetName()
if strings.HasSuffix(name, "_"+column) {
base := strings.TrimSuffix(name, "_"+column)
table = pluralizer.Plural(base)
}
}

return r.References(column, indexName).On(table)
}

func (r *ForeignIDColumnDefinition) References(column, indexName string) schema.ForeignKeyDefinition {
return r.blueprint.Foreign(r.GetName()).References(column).Name(indexName)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate it if you could add some test cases for these two functions.

@alfanzain alfanzain force-pushed the feat/add-methods-to-create-foreign-keys-migration branch 2 times, most recently from fe3f876 to b6a50fd Compare September 15, 2025 22:49
hwbrzzl
hwbrzzl previously approved these changes Sep 16, 2025
Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing, thanks!

@hwbrzzl
Copy link
Contributor

hwbrzzl commented Sep 16, 2025

Oh, sorry, just one more thing, please run go tool mockery locally to generate mock files.

@alfanzain alfanzain force-pushed the feat/add-methods-to-create-foreign-keys-migration branch from b6a50fd to fed615b Compare September 16, 2025 03:57
@alfanzain
Copy link
Contributor Author

Oh, sorry, just one more thing, please run go tool mockery locally to generate mock files.

Done. Kindly review when you have a moment @hwbrzzl

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@hwbrzzl hwbrzzl merged commit fa0d50c into goravel:master Sep 16, 2025
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add "sugar" to create foreign keys in migrations

2 participants