Skip to content

fix(twenty-server): preserve input order in createMany response#17412

Merged
FelixMalfait merged 2 commits intomainfrom
fix/create-many-preserve-order
Jan 24, 2026
Merged

fix(twenty-server): preserve input order in createMany response#17412
FelixMalfait merged 2 commits intomainfrom
fix/create-many-preserve-order

Conversation

@FelixMalfait
Copy link
Copy Markdown
Member

Summary

  • The createMany API was returning records in arbitrary order because fetchUpsertedRecords used WHERE id IN (...) without ORDER BY
  • This caused flaky tests that assumed input order was preserved (e.g., people-merge-many.integration-spec.ts)
  • Fixed by reordering the fetched records to match the original input order from generatedMaps

Root Cause

// Before: No ORDER BY, so SQL returns in arbitrary order
const upsertedRecords = await queryBuilder
  .where({ id: In(objectRecords.generatedMaps.map((record) => record.id)) })
  .getMany();  // Returns in arbitrary order!

Fix

// After: Reorder results to match original input order
const orderedIds = objectRecords.generatedMaps.map((record) => record.id);
const upsertedRecords = await queryBuilder
  .where({ id: In(orderedIds) })
  .getMany();

// Preserve original input order
const recordsById = new Map(upsertedRecords.map((record) => [record.id, record]));
return orderedIds
  .map((id) => recordsById.get(id))
  .filter((record) => record !== undefined);

Test plan

  • Run people-merge-many.integration-spec.ts multiple times - passes consistently
  • Lint passes

@FelixMalfait FelixMalfait force-pushed the fix/create-many-preserve-order branch from 04b407a to 3c065e5 Compare January 24, 2026 10:57
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 24, 2026

Greptile Overview

Greptile Summary

This PR fixes a critical ordering bug in the createMany API where records were returned in arbitrary database order instead of preserving the original input order. The fix implements a Map-based reordering strategy in fetchUpsertedRecords that matches the returned records to the input order from generatedMaps.

Key Changes:

  • Extracted orderedIds from generatedMaps before the database query to preserve the original insertion order
  • Created a Map to enable O(1) lookup of records by ID after the unordered database fetch
  • Used .map() and .filter() with a type predicate to reorder results and filter out any missing records

Impact:

  • Resolves flaky test failures in people-merge-many.integration-spec.ts that relied on deterministic ordering
  • Ensures API consumers can depend on response order matching input order, which is critical for merge operations with conflictPriorityIndex

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The fix is well-structured, uses standard TypeScript patterns (Map for O(1) lookups, type predicates for filtering), and directly addresses the root cause identified in flaky tests. The logic is straightforward and adds minimal computational overhead while providing critical correctness guarantees for API consumers.
  • No files require special attention

Important Files Changed

Filename Overview
packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts Fixed fetchUpsertedRecords to preserve original input order by reordering results to match generatedMaps IDs using a Map-based lookup

Sequence Diagram

sequenceDiagram
    participant Client
    participant CreateManyRunner
    participant Repository
    participant Database

    Client->>CreateManyRunner: createMany(data: [record1, record2, record3])
    CreateManyRunner->>Repository: insert/upsert records
    Repository->>Database: INSERT INTO table VALUES (...)
    Database-->>Repository: generatedMaps: [{id: "uuid-1"}, {id: "uuid-2"}, {id: "uuid-3"}]
    Repository-->>CreateManyRunner: InsertResult with ordered generatedMaps
    
    Note over CreateManyRunner: Extract orderedIds from generatedMaps<br/>to preserve input order
    
    CreateManyRunner->>Repository: WHERE id IN (uuid-1, uuid-2, uuid-3)
    Repository->>Database: SELECT * FROM table WHERE id IN (...)
    Database-->>Repository: Records in arbitrary order
    Repository-->>CreateManyRunner: upsertedRecords (unordered)
    
    Note over CreateManyRunner: Map records by ID<br/>recordsById.set(id, record)
    
    Note over CreateManyRunner: Reorder using orderedIds<br/>orderedIds.map(id => recordsById.get(id))
    
    CreateManyRunner-->>Client: [record1, record2, record3] (preserves input order)
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

No files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 1 file

@FelixMalfait FelixMalfait force-pushed the fix/create-many-preserve-order branch from 3c065e5 to 929b390 Compare January 24, 2026 11:00
Bulk APIs (createMany, mergeMany, findDuplicates) were returning records
in arbitrary order because they use WHERE id IN (...) without ORDER BY.

Fixed by reordering fetched records to match the original input order.

Affected endpoints:
- createMany: fetchUpsertedRecords
- mergeMany: getRecordsToMerge (for dry-run and actual merge)
- findDuplicates: when fetching by IDs
@FelixMalfait FelixMalfait force-pushed the fix/create-many-preserve-order branch from 929b390 to 5e59949 Compare January 24, 2026 11:01
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 24, 2026

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:12634

This environment will automatically shut down when the PR is closed or after 5 hours.


return upsertedRecords as ObjectRecord[];
// Preserve original input order by sorting results to match orderedIds
const recordsById = new Map<string, ObjectRecord>(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why not use sort method?

flatFieldMetadataMaps,
});

const orderedIds = objectRecords.generatedMaps.map((record) => record.id);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i'm confused on how this will preserver the order in the input: objectRecords does not seem to be the input. I wonder if the sort method should be done in a parent method that stil has access to the input

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For simple inserts, generatedMaps does preserve input order (TypeORM guarantee), so the current fix works. For upserts, you're right - the order is scrambled because updates are processed before inserts. Fixing that properly would require a bigger change, I don't think it's worth it. I will look into it but will likely merge like that if I find out it's too much code changes just for this upsert edge-case

);

objectRecords = args.ids
.map((id) => recordsById.get(id))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

here it makes sense

})
.getMany()) as ObjectRecord[];

// Preserve original input order
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not useful :)

Copy link
Copy Markdown
Member

@charlesBochet charlesBochet left a comment

Choose a reason for hiding this comment

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

left comments :)

@FelixMalfait FelixMalfait force-pushed the fix/create-many-preserve-order branch from f0b995d to f574c6f Compare January 24, 2026 12:40
@FelixMalfait FelixMalfait disabled auto-merge January 24, 2026 12:44
@FelixMalfait FelixMalfait added this pull request to the merge queue Jan 24, 2026
Merged via the queue into main with commit 389b6ce Jan 24, 2026
58 checks passed
@FelixMalfait FelixMalfait deleted the fix/create-many-preserve-order branch January 24, 2026 13:01
camilo-agudelo-uma pushed a commit to innovation-grupo-uma/twenty-uma that referenced this pull request Feb 2, 2026
…tyhq#17412)

## Summary
- The `createMany` API was returning records in arbitrary order because
`fetchUpsertedRecords` used `WHERE id IN (...)` without `ORDER BY`
- This caused flaky tests that assumed input order was preserved (e.g.,
`people-merge-many.integration-spec.ts`)
- Fixed by reordering the fetched records to match the original input
order from `generatedMaps`

## Root Cause
```typescript
// Before: No ORDER BY, so SQL returns in arbitrary order
const upsertedRecords = await queryBuilder
  .where({ id: In(objectRecords.generatedMaps.map((record) => record.id)) })
  .getMany();  // Returns in arbitrary order!
```

## Fix
```typescript
// After: Reorder results to match original input order
const orderedIds = objectRecords.generatedMaps.map((record) => record.id);
const upsertedRecords = await queryBuilder
  .where({ id: In(orderedIds) })
  .getMany();

// Preserve original input order
const recordsById = new Map(upsertedRecords.map((record) => [record.id, record]));
return orderedIds
  .map((id) => recordsById.get(id))
  .filter((record) => record !== undefined);
```

## Test plan
- [x] Run `people-merge-many.integration-spec.ts` multiple times -
passes consistently
- [x] Lint passes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants