Repository: mikro-orm/mikro-orm (2026-04-01T11:40:33Z)

Issue:
Inline nested embeddable generates wrong column name when parent is null
### Describe the bug

When a parent inline embeddable is set to `null`, nested embeddables fail to generate a correct column name.

We were trying to upgrade from 6.4.15 to 6.6.x and our tests started failing.

### Reproduction

https://github.com/atachh/mikro-nested-embeddable-bug

### What driver are you using?

@mikro-orm/sqlite

### MikroORM version

next

### Node.js version

v24.11.0

### Operating system

MacOS

### Validations

- [x] Read the [Contributing Guidelines](https://github.com/mikro-orm/mikro-orm/blob/master/CONTRIBUTING.md).
- [x] Read the [docs](https://mikro-orm.io/docs).
- [x] Check that there isn't [already an issue](https://github.com/mikro-orm/mikro-orm/issues) that reports the same bug to avoid creating a duplicate.
- [x] Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/mikro-orm/mikro-orm/discussions) or join our [Discord](https://discord.gg/w8bjxFHS7X).
- [x] The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.


Discussion:

Patch:
diff --git a/packages/core/src/utils/EntityComparator.ts b/packages/core/src/utils/EntityComparator.ts
index 7b8120331838..ebb1919ba2e6 100644
--- a/packages/core/src/utils/EntityComparator.ts
+++ b/packages/core/src/utils/EntityComparator.ts
@@ -636,19 +636,9 @@ export class EntityComparator {
     } else {
       ret += `${padding}if (${nullCond}) {\n`;
       ret +=
-        meta.props
-          .filter(
-            p =>
-              p.embedded?.[0] === prop.name &&
-              // object for JSON embeddable
-              (p.object || p.persist !== false),
-          )
-          .map(childProp => {
-            const childDataKey =
-              meta.embeddable || prop.object ? dataKey + this.wrap(childProp.embedded![1]) : this.wrap(childProp.name);
-            return `${padding}  ret${childDataKey} = null;`;
-          })
-          .join('\n') + `\n`;
+        this.getInlineEmbeddedNullLines(meta, prop.name, padding, dataKey, !!(meta.embeddable || prop.object)).join(
+          '\n',
+        ) + `\n`;
       ret += `${padding}}\n`;
     }
 
@@ -737,6 +727,26 @@ export class EntityComparator {
     return `${ret}${padding}}`;
   }
 
+  private getInlineEmbeddedNullLines<T>(
+    meta: EntityMetadata<T>,
+    parentName: string,
+    padding: string,
+    dataKey: string,
+    useEmbeddedKey: boolean,
+  ): string[] {
+    return meta.props
+      .filter(p => p.embedded?.[0] === parentName && (p.object || p.persist !== false))
+      .flatMap(childProp => {
+        const childDataKey = useEmbeddedKey ? dataKey + this.wrap(childProp.embedded![1]) : this.wrap(childProp.name);
+
+        if (childProp.kind === ReferenceKind.EMBEDDED && !childProp.object) {
+          return this.getInlineEmbeddedNullLines(meta, childProp.name, padding, childDataKey, useEmbeddedKey);
+        }
+
+        return [`${padding}  ret${childDataKey} = null;`];
+      });
+  }
+
   private registerCustomType<T>(prop: EntityProperty<T>, context: Map<string, any>) {
     const convertorKey = this.safeKey(prop.name);
     context.set(`convertToDatabaseValue_${convertorKey}`, (val: any) => {


Test patch:
diff --git a/tests/features/embeddables/GH7463.test.ts b/tests/features/embeddables/GH7463.test.ts
new file mode 100644
index 000000000000..cd96a4faf4b5
--- /dev/null
+++ b/tests/features/embeddables/GH7463.test.ts
@@ -0,0 +1,102 @@
+import {
+  Embeddable,
+  Embedded,
+  Entity,
+  PrimaryKey,
+  Property,
+  ReflectMetadataProvider,
+} from '@mikro-orm/decorators/legacy';
+import { MikroORM } from '@mikro-orm/sqlite';
+
+@Embeddable()
+class InnerEmbeddable {
+  @Property()
+  name!: string;
+}
+
+@Embeddable()
+class OuterEmbeddable {
+  @Embedded(() => InnerEmbeddable, { nullable: true })
+  inner?: InnerEmbeddable;
+}
+
+@Entity()
+class User {
+  @PrimaryKey()
+  id!: number;
+
+  @Property()
+  name!: string;
+
+  @Embedded(() => OuterEmbeddable, { nullable: true })
+  outer?: OuterEmbeddable;
+}
+
+let orm: MikroORM;
+
+beforeAll(async () => {
+  orm = await MikroORM.init({
+    dbName: ':memory:',
+    entities: [User],
+    metadataProvider: ReflectMetadataProvider,
+  });
+  await orm.schema.refresh();
+});
+
+afterAll(async () => {
+  await orm.close(true);
+});
+
+test('GH #7463 - snapshot generator', async () => {
+  const comparator = orm.em.getComparator();
+  const snapshotGenerator = comparator.getSnapshotGenerator(User);
+  expect(snapshotGenerator.toString()).toMatchSnapshot();
+});
+
+test('GH #7463', async () => {
+  orm.em.create(User, {
+    name: 'Foo',
+    outer: {
+      inner: { name: 'inner' },
+    },
+  });
+  await orm.em.flush();
+  orm.em.clear();
+
+  const user = await orm.em.findOneOrFail(User, { name: 'Foo' });
+  expect(user.outer!.inner!.name).toBe('inner');
+  orm.em.assign(user, { outer: null });
+  await orm.em.flush();
+  orm.em.clear();
+
+  const userAfter = await orm.em.findOneOrFail(User, { name: 'Foo' });
+  expect(userAfter.outer).toBeNull();
+});
+
+test('GH #7463 - create after null', async () => {
+  orm.em.create(User, {
+    name: 'Bar',
+    outer: {
+      inner: { name: 'nested' },
+    },
+  });
+  await orm.em.flush();
+  orm.em.clear();
+
+  const user = await orm.em.findOneOrFail(User, { name: 'Bar' });
+  expect(user.outer!.inner!.name).toBe('nested');
+  orm.em.assign(user, { outer: null });
+  await orm.em.flush();
+  orm.em.clear();
+
+  const user2 = await orm.em.findOneOrFail(User, { name: 'Bar' });
+  expect(user2.outer).toBeNull();
+
+  // re-assign and verify it persists correctly
+  orm.em.assign(user2, { outer: { inner: { name: 'new' } } });
+  await orm.em.flush();
+  orm.em.clear();
+
+  const user3 = await orm.em.findOneOrFail(User, { name: 'Bar' });
+  expect(user3.outer!.inner!.name).toBe('new');
+});
diff --git a/tests/features/embeddables/__snapshots__/GH7463.test.ts.snap b/tests/features/embeddables/__snapshots__/GH7463.test.ts.snap
new file mode 100644
index 000000000000..07397588d9c0
--- /dev/null
+++ b/tests/features/embeddables/__snapshots__/GH7463.test.ts.snap
@@ -0,0 +1,30 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`GH #7463 - snapshot generator 1`] = `
+"function(entity) {
+  const ret = {};
+  if (typeof entity.id !== 'undefined') {
+    ret.id = entity.id;
+  }
+
+  if (typeof entity.name !== 'undefined') {
+    ret.name = entity.name;
+  }
+
+  if (entity.outer === null) {
+    ret.outer_inner_name = null;
+  }
+  if (entity.outer != null) {
+
+    if (entity.outer.inner === null) {
+      ret.outer_inner_name = null;
+    }
+    if (entity.outer.inner != null) {
+      if (typeof entity.outer.inner.name !== 'undefined') ret.outer_inner_name = clone(entity.outer.inner.name);
+    }
+
+  }
+
+  return ret;
+}"
+`;
diff --git a/tests/features/embeddables/__snapshots__/embeddable-custom-types.postgres.test.ts.snap b/tests/features/embeddables/__snapshots__/embeddable-custom-types.postgres.test.ts.snap
index 2775d1a02e46..a0623f35f9fa 100644
--- a/tests/features/embeddables/__snapshots__/embeddable-custom-types.postgres.test.ts.snap
+++ b/tests/features/embeddables/__snapshots__/embeddable-custom-types.postgres.test.ts.snap
@@ -24,7 +24,7 @@ exports[`embedded entities with custom types > snapshot generator 1`] = `
 
   if (entity.nested === null) {
     ret.nested_someValue = null;
-    ret.nested_deep = null;
+    ret.nested_deep_someValue = null;
   }
   if (entity.nested != null) {
     if (typeof entity.nested.someValue !== 'undefined') ret.nested_someValue = clone(convertToDatabaseValue_nested_someValue(entity.nested.someValue));
diff --git a/tests/features/embeddables/__snapshots__/entities-in-embeddables.mongo.test.ts.snap b/tests/features/embeddables/__snapshots__/entities-in-embeddables.mongo.test.ts.snap
index 784de9db62b2..22575528fb4e 100644
--- a/tests/features/embeddables/__snapshots__/entities-in-embeddables.mongo.test.ts.snap
+++ b/tests/features/embeddables/__snapshots__/entities-in-embeddables.mongo.test.ts.snap
@@ -13,7 +13,12 @@ exports[`embedded entities in mongo > diffing 1`] = `
 
   if (entity.profile1 === null) {
     ret.profile1_username = null;
-    ret.profile1_identity = null;
+    ret.profile1_identity_email = null;
+    ret.profile1_identity_meta_foo = null;
+    ret.profile1_identity_meta_bar = null;
+    ret.profile1_identity_meta_source = null;
+    ret.profile1_identity_links = null;
+    ret.profile1_identity_source = null;
     ret.profile1_source = null;
   }
   if (entity.profile1 != null) {
@@ -21,7 +26,9 @@ exports[`embedded entities in mongo > diffing 1`] = `
 
     if (entity.profile1.identity === null) {
       ret.profile1_identity_email = null;
-      ret.profile1_identity_meta = null;
+      ret.profile1_identity_meta_foo = null;
+      ret.profile1_identity_meta_bar = null;
+      ret.profile1_identity_meta_source = null;
       ret.profile1_identity_links = null;
       ret.profile1_identity_source = null;
     }
diff --git a/tests/features/embeddables/__snapshots__/entities-in-embeddables.postgres.test.ts.snap b/tests/features/embeddables/__snapshots__/entities-in-embeddables.postgres.test.ts.snap
index b3949e757ac6..7d7d576421de 100644
--- a/tests/features/embeddables/__snapshots__/entities-in-embeddables.postgres.test.ts.snap
+++ b/tests/features/embeddables/__snapshots__/entities-in-embeddables.postgres.test.ts.snap
@@ -13,7 +13,12 @@ exports[`embedded entities in postgres > diffing 1`] = `
 
   if (entity.profile1 === null) {
     ret.profile1_username = null;
-    ret.profile1_identity = null;
+    ret.profile1_identity_email = null;
+    ret.profile1_identity_meta_foo = null;
+    ret.profile1_identity_meta_bar = null;
+    ret.profile1_identity_meta_source = null;
+    ret.profile1_identity_links = null;
+    ret.profile1_identity_source = null;
     ret.profile1_source = null;
   }
   if (entity.profile1 != null) {
@@ -21,7 +26,9 @@ exports[`embedded entities in postgres > diffing 1`] = `
 
     if (entity.profile1.identity === null) {
       ret.profile1_identity_email = null;
-      ret.profile1_identity_meta = null;
+      ret.profile1_identity_meta_foo = null;
+      ret.profile1_identity_meta_bar = null;
+      ret.profile1_identity_meta_source = null;
       ret.profile1_identity_links = null;
       ret.profile1_identity_source = null;
     }
diff --git a/tests/features/embeddables/__snapshots__/nested-embeddables.mongo.test.ts.snap b/tests/features/embeddables/__snapshots__/nested-embeddables.mongo.test.ts.snap
index fcf5e806af61..523d1b11f07c 100644
--- a/tests/features/embeddables/__snapshots__/nested-embeddables.mongo.test.ts.snap
+++ b/tests/features/embeddables/__snapshots__/nested-embeddables.mongo.test.ts.snap
@@ -13,14 +13,17 @@ exports[`embedded entities in mongo > diffing 1`] = `
 
   if (entity.profile1 === null) {
     ret.profile1_username = null;
-    ret.profile1_identity = null;
+    ret.profile1_identity_email = null;
+    ret.profile1_identity_meta_foo = null;
+    ret.profile1_identity_meta_bar = null;
   }
   if (entity.profile1 != null) {
     if (typeof entity.profile1.username !== 'undefined') ret.profile1_username = clone(entity.profile1.username);
 
     if (entity.profile1.identity === null) {
       ret.profile1_identity_email = null;
-      ret.profile1_identity_meta = null;
+      ret.profile1_identity_meta_foo = null;
+      ret.profile1_identity_meta_bar = null;
     }
     if (entity.profile1.identity != null) {
       if (typeof entity.profile1.identity.email !== 'undefined') ret.profile1_identity_email = clone(entity.profile1.identity.email);
diff --git a/tests/features/embeddables/__snapshots__/nested-embeddables.postgres.test.ts.snap b/tests/features/embeddables/__snapshots__/nested-embeddables.postgres.test.ts.snap
index cf54eee1d08e..1cdcf2f380d7 100644
--- a/tests/features/embeddables/__snapshots__/nested-embeddables.postgres.test.ts.snap
+++ b/tests/features/embeddables/__snapshots__/nested-embeddables.postgres.test.ts.snap
@@ -13,14 +13,18 @@ exports[`embedded entities in postgres > diffing 1`] = `
 
   if (entity.profile1 === null) {
     ret.profile1_username = null;
-    ret.profile1_identity = null;
+    ret.profile1_identity_email = null;
+    ret.profile1_identity_meta_foo = null;
+    ret.profile1_identity_meta_bar = null;
+    ret.profile1_identity_links = null;
   }
   if (entity.profile1 != null) {
     if (typeof entity.profile1.username !== 'undefined') ret.profile1_username = clone(entity.profile1.username);
 
     if (entity.profile1.identity === null) {
       ret.profile1_identity_email = null;
-      ret.profile1_identity_meta = null;
+      ret.profile1_identity_meta_foo = null;
+      ret.profile1_identity_meta_bar = null;
       ret.profile1_identity_links = null;
     }
     if (entity.profile1.identity != null) {
diff --git a/tests/features/embeddables/__snapshots__/polymorphic-embedded-entities.sqlite.test.ts.snap b/tests/features/embeddables/__snapshots__/polymorphic-embedded-entities.sqlite.test.ts.snap
index 6001207426f9..63b03c792052 100644
--- a/tests/features/embeddables/__snapshots__/polymorphic-embedded-entities.sqlite.test.ts.snap
+++ b/tests/features/embeddables/__snapshots__/polymorphic-embedded-entities.sqlite.test.ts.snap
@@ -602,7 +602,8 @@ exports[`polymorphic embeddables in sqlite > diffing 2`] = `
     ret.pet_type = null;
     ret.pet_name = null;
     ret.pet_canMeow = null;
-    ret.pet_food = null;
+    ret.pet_food_cats = null;
+    ret.pet_food_mice = null;
   }
   if (entity.pet != null) {
     if (typeof entity.pet.canBark !== 'undefined') ret.pet_canBark = clone(entity.pet.canBark);