Limited Time Offer!
For Less Than the Cost of a Starbucks Coffee, Access All DevOpsSchool Videos on YouTube Unlimitedly.
Master DevOps, SRE, DevSecOps Skills!
Laravel migrations are one of the most useful features of the framework. They allow developers to manage database structure changes safely, version them with code, and apply them across different environments using simple Artisan commands.
But sometimes, even a normal migration can fail because Laravel’s internal migration tracking table is not correctly structured.
A common error looks like this:
SQLSTATE[HY000]: General error: 1364 Field 'id' doesn't have a default value
In this tutorial, we will understand why this error happens, how to diagnose it, and how to safely fix it without damaging your existing database changes.
This guide is based on a real Laravel migration issue where the migration itself was completed successfully, but Laravel failed while inserting the migration record into the migrations table.
The Real Error Scenario
Suppose you are inside a Laravel project directory:
cd /opt/lampp/htdocs/devopsschool/services/commands
Then you run these migrations manually:
php artisan migrate --path=database/migrations/2026_05_21_160000_add_keycloak_user_id_to_examples_table.php
php artisan migrate --path=database/migrations/2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables.php
Laravel starts running the first migration:
INFO Running migrations.
2026_05_21_160000_add_keycloak_user_id_to_examples_table ........ 87ms DONE
At first, everything looks fine. Laravel says the migration is DONE.
But immediately after that, this error appears:
In Connection.php line 829:
SQLSTATE[HY000]: General error: 1364 Field 'id' doesn't have a default value
(Connection: mysql, SQL: insert into `migrations` (`migration`, `batch`)
values (2026_05_21_160000_add_keycloak_user_id_to_examples_table, 2))
Then MySQL shows:
In MySqlConnection.php line 45:
SQLSTATE[HY000]: General error: 1364 Field 'id' doesn't have a default value
The second migration also behaves similarly:
INFO Running migrations.
2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables .... 718ms DONE
But again, Laravel fails while inserting the migration tracking record:
SQLSTATE[HY000]: General error: 1364 Field 'id' doesn't have a default value
This situation is confusing because Laravel says the migration is DONE, but then it throws an error.
So the main question is:
Did the migration fail, or did it succeed?
The answer is: the database changes most likely succeeded, but Laravel failed to record the migration in the migrations table.
Understanding How Laravel Tracks Migrations
Laravel uses a special table named migrations to track which migration files have already been executed.
A normal Laravel migrations table has three columns:
id
migration
batch
The id column should be an auto-incrementing primary key.
A healthy Laravel migrations table usually looks like this:
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
);
When Laravel runs a migration, it does two things:
First, it executes the database changes inside the migration file.
For example, it may add a column:
Schema::table('examples', function (Blueprint $table) {
$table->string('keycloak_user_id')->nullable()->index();
});
Second, after the migration finishes successfully, Laravel inserts a record into the migrations table:
INSERT INTO migrations (migration, batch)
VALUES ('2026_05_21_160000_add_keycloak_user_id_to_examples_table', 2);
Notice something important here.
Laravel does not insert the id manually. It only inserts migration and batch.
That means MySQL must automatically generate the id.
For this to work, the id column must be AUTO_INCREMENT.
If the id column is not auto-incrementing, MySQL does not know what value to put in the id column. That is exactly why this error happens:
Field 'id' doesn't have a default value
Why the Migration Shows DONE but Still Throws an Error
This is the most important part to understand.
When Laravel prints this:
2026_05_21_160000_add_keycloak_user_id_to_examples_table .... DONE
It usually means the migration file was executed successfully.
For example, the column may already have been added to the target table.
But after executing the migration, Laravel tries to write the migration name into the migrations table.
That insert fails because migrations.id is not auto-incrementing.
So the problem is not necessarily inside your migration file.
The actual problem is inside Laravel’s internal migration tracking table.
In simple words:
The migration changes were applied, but Laravel could not save the migration history.
This is dangerous because Laravel may try to run the same migration again later.
If the migration tries to add a column that already exists, the next run may fail with errors like:
Duplicate column name 'keycloak_user_id'
That is why you should not blindly rerun migrations before fixing the migrations table.
Step 1: Check the Migrations Table Structure
First, log in to MySQL:
mysql -u root -p
Select your Laravel database:
USE your_database_name;
Then check the table structure:
SHOW CREATE TABLE migrations\G
In the real case, the output was:
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL,
`migration` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
The problem is here:
`id` int unsigned NOT NULL,
It should be:
`id` int unsigned NOT NULL AUTO_INCREMENT,
The primary key is present, but AUTO_INCREMENT is missing.
That means Laravel cannot insert a new row without manually providing an id.
But Laravel does not provide an id because it expects MySQL to generate it automatically.
Step 2: Fix the Migrations Table
To fix the issue, modify the id column and add AUTO_INCREMENT:
ALTER TABLE migrations
MODIFY id INT UNSIGNED NOT NULL AUTO_INCREMENT;
After running this command, check the table again:
SHOW CREATE TABLE migrations\G
Now the output should look like this:
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
Now the important part is fixed:
`id` int unsigned NOT NULL AUTO_INCREMENT
This means MySQL can automatically generate the id value whenever Laravel inserts a migration record.
Step 3: Check the Latest Migration Batch
Now you need to check the latest batch number.
Run:
SELECT MAX(batch) AS latest_batch FROM migrations;
In the real case, the result was:
+--------------+
| latest_batch |
+--------------+
| 1 |
+--------------+
This means all previous migrations are in batch 1.
The two migrations that were just executed should now be added as batch 2.
Step 4: Check Whether the Failed Migration Records Already Exist
Before manually inserting records, always check whether they already exist.
Run:
SELECT * FROM migrations
WHERE migration IN (
'2026_05_21_160000_add_keycloak_user_id_to_examples_table',
'2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables'
);
In the real case, the result was:
Empty set
That means Laravel did not record these migrations.
So now we can safely insert them manually.
Step 5: Manually Insert the Migration Records
Since the latest batch is 1, we will insert these two migrations as batch 2.
Run:
INSERT INTO migrations (migration, batch)
VALUES
('2026_05_21_160000_add_keycloak_user_id_to_examples_table', 2),
('2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables', 2);
This manually tells Laravel:
These two migrations have already been executed.
Now Laravel will not try to run them again in future php artisan migrate commands.
Step 6: Verify the Inserted Migration Records
After inserting, verify the records:
SELECT * FROM migrations
WHERE migration IN (
'2026_05_21_160000_add_keycloak_user_id_to_examples_table',
'2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables'
);
You should see both migration records.
Example output:
+----+--------------------------------------------------------------------------+-------+
| id | migration | batch |
+----+--------------------------------------------------------------------------+-------+
| 15 | 2026_05_21_160000_add_keycloak_user_id_to_examples_table | 2 |
| 16 | 2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables | 2 |
+----+--------------------------------------------------------------------------+-------+
The actual id values may be different depending on your database.
That is fine.
The important thing is that both migration names are present and assigned to the correct batch.
Step 7: Exit MySQL and Check Laravel Migration Status
Exit MySQL:
exit;
Then run:
php artisan migrate:status
Laravel should now show both migrations as already run.
You should see something like:
Ran? Migration
Yes 2026_05_21_160000_add_keycloak_user_id_to_examples_table
Yes 2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables
If both migrations show Yes, the issue is fixed.
Step 8: Clear Laravel Cache
After database structure or migration fixes, it is a good practice to clear Laravel caches:
php artisan config:clear
php artisan cache:clear
Depending on your project, you may also run:
php artisan route:clear
php artisan view:clear
If you are working in a Laravel microservice or production-like environment, cache clearing helps prevent stale configuration or stale application state from causing confusion.
Complete Safe Command Flow
Here is the complete safe command flow.
First, log in to MySQL:
mysql -u root -p
Select the database:
USE your_database_name;
Check the current migration table:
SHOW CREATE TABLE migrations\G
Fix the id column:
ALTER TABLE migrations
MODIFY id INT UNSIGNED NOT NULL AUTO_INCREMENT;
Verify the fix:
SHOW CREATE TABLE migrations\G
Check latest batch:
SELECT MAX(batch) AS latest_batch FROM migrations;
Check whether the two migrations are already recorded:
SELECT * FROM migrations
WHERE migration IN (
'2026_05_21_160000_add_keycloak_user_id_to_examples_table',
'2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables'
);
If the result is empty, insert them manually:
INSERT INTO migrations (migration, batch)
VALUES
('2026_05_21_160000_add_keycloak_user_id_to_examples_table', 2),
('2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables', 2);
Verify again:
SELECT * FROM migrations
WHERE migration IN (
'2026_05_21_160000_add_keycloak_user_id_to_examples_table',
'2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables'
);
Exit MySQL:
exit;
Then run Laravel verification commands:
php artisan migrate:status
php artisan config:clear
php artisan cache:clear
Why You Should Not Immediately Rerun the Migration
When developers see migration errors, the first instinct is often to rerun:
php artisan migrate
But in this situation, rerunning immediately can create another problem.
Because Laravel already executed the database changes, but failed only while inserting into the migrations table.
That means the actual schema changes may already exist.
For example, if your first migration added a keycloak_user_id column to the examples table, the column may already be there.
If you rerun the same migration, Laravel may again try to add the same column:
$table->string('keycloak_user_id')->nullable();
MySQL may then throw:
SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'keycloak_user_id'
That creates a second issue.
So before rerunning migrations, always verify whether the schema changes were already applied.
For example:
SHOW COLUMNS FROM examples LIKE 'keycloak_user_id';
If the column exists, do not rerun that migration blindly.
Instead, fix the migrations table and manually insert the migration record.
How to Check Whether a Column Was Already Added
For the first migration:
2026_05_21_160000_add_keycloak_user_id_to_examples_table
You can check:
SHOW COLUMNS FROM examples LIKE 'keycloak_user_id';
If it returns a row, the column exists.
Example:
+------------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------------+--------------+------+-----+---------+-------+
| keycloak_user_id | varchar(255) | YES | MUL | NULL | |
+------------------+--------------+------+-----+---------+-------+
For the second migration:
2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables
You need to check all affected tables.
For example, if the migration modifies tables like commands, examples, bookmarks, or any user-owned tables, check each table:
SHOW COLUMNS FROM table_name LIKE 'keycloak_user_id';
SHOW COLUMNS FROM table_name LIKE 'email';
Replace table_name with the actual table names used in the migration.
A Safer Migration Style for Existing Databases
When working on old Laravel projects, production databases, or microservices, migrations should be defensive.
Instead of directly adding a column, use checks like:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('examples') && !Schema::hasColumn('examples', 'keycloak_user_id')) {
Schema::table('examples', function (Blueprint $table) {
$table->string('keycloak_user_id')->nullable()->index()->after('id');
});
}
}
public function down(): void
{
if (Schema::hasTable('examples') && Schema::hasColumn('examples', 'keycloak_user_id')) {
Schema::table('examples', function (Blueprint $table) {
$table->dropColumn('keycloak_user_id');
});
}
}
};
This prevents duplicate column errors if the migration is accidentally executed more than once or if the database already has partial changes.
For multi-table migrations, use the same pattern:
$tables = [
'examples',
'commands',
'bookmarks',
];
foreach ($tables as $tableName) {
if (Schema::hasTable($tableName) && !Schema::hasColumn($tableName, 'keycloak_user_id')) {
Schema::table($tableName, function (Blueprint $table) {
$table->string('keycloak_user_id')->nullable()->index();
});
}
}
This is especially useful in microservice projects where different environments may not always have the exact same database state.
Why This Error Happens in Existing Projects
This issue usually happens because the migrations table was created manually or modified incorrectly.
Possible reasons include:
The migrations table was manually created without AUTO_INCREMENT.
The database was imported from an old dump where the auto-increment property was lost.
Someone edited the table structure manually.
A migration tracking table was copied from another system incorrectly.
The project was moved between different MySQL or MariaDB versions.
A database restore process did not preserve the full table definition.
The Laravel project may be old and the migrations table was created differently.
Whatever the cause, the fix is the same: Laravel expects the id column in the migrations table to be auto-incrementing.
Correct Laravel Migrations Table Structure
A standard Laravel migrations table should look similar to this:
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
In newer Laravel versions, the id type may vary depending on the framework version, but the key point remains the same:
The id column must auto-increment.
Laravel inserts records like this:
INSERT INTO migrations (migration, batch) VALUES (...);
It does not insert id.
So id must generate automatically.
What the Batch Number Means in Laravel Migrations
The batch column tells Laravel which migrations were run together.
For example:
+----+--------------------------------------+-------+
| id | migration | batch |
+----+--------------------------------------+-------+
| 1 | 2014_10_12_000000_create_users_table | 1 |
| 2 | 2014_10_12_100000_create_password_reset_tokens_table | 1 |
| 3 | 2026_05_21_160000_add_keycloak_user_id_to_examples_table | 2 |
+----+--------------------------------------+-------+
If several migrations are run in one command, they usually share the same batch number.
Laravel uses batch numbers for rollback operations.
For example:
php artisan migrate:rollback
This rolls back the last batch.
If your latest batch is 2, Laravel will roll back all migrations in batch 2.
That is why we inserted the failed migration records as batch 2 after checking that the previous latest batch was 1.
If your latest batch was 5, then the new manually inserted migrations should usually be batch 6.
In this case, because the failed migration command itself attempted to insert with batch 2, using batch 2 was correct.
Should You Manually Insert Migration Records?
In general, manually editing the migrations table should be avoided unless you understand exactly what happened.
But in this specific case, manual insert is the correct fix because:
The migrations already showed DONE.
The database changes were likely applied.
Laravel failed only while recording the migration.
The migration records were missing.
Rerunning the migrations could create duplicate column errors.
The migrations table structure has now been fixed.
So the safe solution is:
Fix migrations.id.
Verify the actual schema changes.
Insert the missing migration records.
Run php artisan migrate:status.
What If the Migration Changes Were Not Actually Applied?
Sometimes a migration may show partial execution but not complete all changes.
Before manually inserting records, you should verify the schema.
For example:
SHOW COLUMNS FROM examples LIKE 'keycloak_user_id';
If the expected column does not exist, then the migration may not have applied correctly.
In that case, after fixing the migrations table, you can rerun the migration:
php artisan migrate --path=database/migrations/2026_05_21_160000_add_keycloak_user_id_to_examples_table.php
But only do this after checking the target table.
If the column exists, do not rerun unless the migration is written safely with Schema::hasColumn() checks.
How to Handle This in a Production Environment
If this issue happens in production, be more careful.
Before making any change, take a database backup:
mysqldump -u root -p database_name > backup_before_migration_fix.sql
Then check the migration table:
SHOW CREATE TABLE migrations\G
Fix only the migration table structure:
ALTER TABLE migrations
MODIFY id INT UNSIGNED NOT NULL AUTO_INCREMENT;
Do not modify application tables unless you have verified the exact problem.
Then check whether the target columns exist.
Then insert only the missing migration records.
Finally run:
php artisan migrate:status
Avoid running:
php artisan migrate:fresh
on production.
This command drops all tables and recreates them. It should never be used on production databases.
Also avoid running:
php artisan migrate:reset
or:
php artisan migrate:refresh
unless you fully understand the consequences.
Recommended Production Checklist
Before fixing:
pwd
php artisan --version
php artisan migrate:status
Backup database:
mysqldump -u root -p database_name > backup_before_fix.sql
Check current migrations table:
SHOW CREATE TABLE migrations\G
SELECT MAX(batch) AS latest_batch FROM migrations;
Check missing records:
SELECT * FROM migrations
WHERE migration IN (
'2026_05_21_160000_add_keycloak_user_id_to_examples_table',
'2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables'
);
Check target schema:
SHOW COLUMNS FROM examples LIKE 'keycloak_user_id';
Fix migration table:
ALTER TABLE migrations
MODIFY id INT UNSIGNED NOT NULL AUTO_INCREMENT;
Insert missing records only if they are absent:
INSERT INTO migrations (migration, batch)
VALUES
('2026_05_21_160000_add_keycloak_user_id_to_examples_table', 2),
('2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables', 2);
Verify:
php artisan migrate:status
php artisan config:clear
php artisan cache:clear
Why This Matters in Keycloak Migration Work
In this specific case, the migrations were related to adding keycloak_user_id to Laravel tables.
This type of migration is common when a project moves from local authentication to centralized identity management using Keycloak.
A migration like this may add keycloak_user_id to user-owned tables so records can be connected to the authenticated Keycloak user instead of relying only on legacy fields like:
email
user_id
student_id
For example, a table may previously have stored ownership like this:
email
user_id
student_id
After Keycloak integration, the preferred owner field becomes:
keycloak_user_id
This allows multiple microservices to identify the same user using a central identity provider.
But because these migrations affect ownership and access logic, they must be handled carefully.
If Laravel thinks the migration did not run, it may try to apply ownership-related schema changes again.
That can break deployment or create inconsistent environments.
So fixing the migration tracking issue is not just a database cleanup task. It is important for the stability of the full Keycloak integration.
Example: Adding keycloak_user_id Safely
Here is a safe example migration for adding keycloak_user_id to an existing table:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('examples') && !Schema::hasColumn('examples', 'keycloak_user_id')) {
Schema::table('examples', function (Blueprint $table) {
$table->string('keycloak_user_id')->nullable()->index()->after('id');
});
}
}
public function down(): void
{
if (Schema::hasTable('examples') && Schema::hasColumn('examples', 'keycloak_user_id')) {
Schema::table('examples', function (Blueprint $table) {
$table->dropIndex(['keycloak_user_id']);
$table->dropColumn('keycloak_user_id');
});
}
}
};
However, dropping indexes can be tricky if the index name differs. Laravel usually creates an index name like:
examples_keycloak_user_id_index
A more explicit version is:
$table->index('keycloak_user_id', 'examples_keycloak_user_id_index');
Then in rollback:
$table->dropIndex('examples_keycloak_user_id_index');
This makes the migration more predictable.
Example: Adding keycloak_user_id to Multiple Tables
For user-owned tables, you may need to add the same column to many tables.
Example:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private array $tables = [
'examples',
'commands',
'bookmarks',
];
public function up(): void
{
foreach ($this->tables as $tableName) {
if (Schema::hasTable($tableName) && !Schema::hasColumn($tableName, 'keycloak_user_id')) {
Schema::table($tableName, function (Blueprint $table) {
$table->string('keycloak_user_id')->nullable()->index();
});
}
if (Schema::hasTable($tableName) && Schema::hasColumn($tableName, 'email')) {
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
// Example only. Be careful when changing existing columns.
// Use doctrine/dbal if required by your Laravel version.
});
}
}
}
public function down(): void
{
foreach ($this->tables as $tableName) {
if (Schema::hasTable($tableName) && Schema::hasColumn($tableName, 'keycloak_user_id')) {
Schema::table($tableName, function (Blueprint $table) {
$table->dropColumn('keycloak_user_id');
});
}
}
}
};
When working with multiple tables, defensive checks are very important.
Some environments may have one table but not another.
Some tables may already have the column.
Some databases may have old schema versions.
A safe migration should handle these conditions gracefully.
Common Mistakes to Avoid
Rerunning the Migration Without Checking the Schema
Do not immediately rerun:
php artisan migrate
after seeing the error.
First check whether the column or table changes already happened.
Manually Inserting Records Before Fixing AUTO_INCREMENT
Do not insert records into migrations before fixing the id column.
If id is not auto-incrementing, this will also fail:
INSERT INTO migrations (migration, batch)
VALUES ('migration_name', 2);
Fix this first:
ALTER TABLE migrations
MODIFY id INT UNSIGNED NOT NULL AUTO_INCREMENT;
Using the Wrong Batch Number
Do not randomly choose a batch number.
Check:
SELECT MAX(batch) AS latest_batch FROM migrations;
Then use the next logical batch.
In the real case:
latest_batch = 1
So the failed migrations were inserted as:
batch = 2
Forgetting to Verify Migration Status
Always run:
php artisan migrate:status
This confirms whether Laravel now recognizes the migrations as executed.
Running Destructive Commands
Avoid destructive commands such as:
php artisan migrate:fresh
php artisan migrate:refresh
php artisan migrate:reset
on important databases.
These commands can drop or roll back tables.
Troubleshooting Additional Errors
Duplicate Column Error
If you rerun a migration and get:
Duplicate column name 'keycloak_user_id'
It means the column already exists.
Fix the migration tracking record instead of trying to add the column again.
Check:
SHOW COLUMNS FROM examples LIKE 'keycloak_user_id';
Then insert the missing migration record into migrations.
Duplicate Migration Record
If you manually insert and get a duplicate error, check:
SELECT * FROM migrations
WHERE migration = '2026_05_21_160000_add_keycloak_user_id_to_examples_table';
If the record already exists, do not insert it again.
AUTO_INCREMENT Already Exists
If your SHOW CREATE TABLE migrations\G already shows:
AUTO_INCREMENT
then this specific issue is not the cause.
In that case, check whether the table has triggers, strict SQL mode issues, or a custom migrations table structure.
No migrations Table Found
If the table does not exist, Laravel may not be installed properly or the migration table was deleted.
You can create it using:
php artisan migrate:install
But do not run this if the table already exists.
Final Verification Checklist
After fixing the issue, confirm all of these points:
The migrations.id column has AUTO_INCREMENT.
SHOW CREATE TABLE migrations\G
The latest batch is correct.
SELECT MAX(batch) AS latest_batch FROM migrations;
The failed migration records exist.
SELECT * FROM migrations
WHERE migration IN (
'2026_05_21_160000_add_keycloak_user_id_to_examples_table',
'2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables'
);
The target columns exist.
SHOW COLUMNS FROM examples LIKE 'keycloak_user_id';
Laravel recognizes the migrations.
php artisan migrate:status
Laravel cache is cleared.
php artisan config:clear
php artisan cache:clear
Final Summary
The error:
SQLSTATE[HY000]: General error: 1364 Field 'id' doesn't have a default value
during Laravel migration does not always mean your migration file is wrong.
In this case, the real issue was with the migrations table itself.
The table had:
`id` int unsigned NOT NULL
But Laravel needs:
`id` int unsigned NOT NULL AUTO_INCREMENT
Because Laravel inserts migration records like this:
INSERT INTO migrations (migration, batch) VALUES (...);
It does not manually provide the id.
The correct fix is:
ALTER TABLE migrations
MODIFY id INT UNSIGNED NOT NULL AUTO_INCREMENT;
Then, because the migrations already showed DONE but were not recorded, manually insert the missing migration records:
INSERT INTO migrations (migration, batch)
VALUES
('2026_05_21_160000_add_keycloak_user_id_to_examples_table', 2),
('2026_05_21_170000_add_keycloak_user_id_and_nullable_email_to_user_owned_tables', 2);
Finally, verify with:
php artisan migrate:status
This approach safely fixes Laravel’s migration tracking without rerunning already-applied migrations and without risking duplicate column errors.
For Laravel projects, especially microservices using Keycloak or other identity migration work, keeping the migrations table healthy is critical. It ensures every environment knows exactly which database changes have already been applied and prevents deployment issues in future releases.
