Skip to content

feat(core): add native UNION support via sequelize.union#18110

Open
agamennone-siae wants to merge 4 commits intosequelize:mainfrom
agamennone-siae:main
Open

feat(core): add native UNION support via sequelize.union#18110
agamennone-siae wants to merge 4 commits intosequelize:mainfrom
agamennone-siae:main

Conversation

@agamennone-siae
Copy link

@agamennone-siae agamennone-siae commented Jan 26, 2026

Pull Request Checklist

Description of Changes

Implemented Sequelize#union (feat #18107) to combine results from multiple queries.
This adds support for:

  • UNION (default) and UNION ALL (via { unionAll: true } option).
  • Global ORDER BY clause for the combined result.
  • Global LIMIT and OFFSET clauses.
    The implementation relies on a new unionQuery method in AbstractQueryGenerator which handles the SQL generation across dialects, properly quoting identifiers and handling dialect-specific syntax (e.g. LIMIT/OFFSET handling).

List of Breaking Changes

  • None

Summary by CodeRabbit

Release Notes

  • New Features
    • Added union query functionality to combine results from multiple models.
    • Supports UNION and UNION ALL operations with optional ordering, limit, and offset capabilities.
    • Includes automatic deduplication by primary key for standard UNION operations.

✏️ Tip: You can customize this high-level summary in your review settings.

…eries with support for `UNION ALL`, `ORDER BY`, `LIMIT`, and `OFFSET`.
@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

This PR adds UNION query support across multiple Sequelize layers. A new UnionOptions type is introduced, along with unionQuery() in AbstractQueryGenerator to build combined SQL, union() in AbstractQueryInterface to execute union queries, and union() in the Sequelize class to orchestrate multi-model union operations with optional deduplication.

Changes

Cohort / File(s) Summary
Type Definitions
packages/core/src/abstract-dialect/query-generator.types.ts
Introduces UnionOptions interface extending AddLimitOffsetOptions with optional unionAll boolean and order property for union-specific configuration.
Query Generator
packages/core/src/abstract-dialect/query-generator.d.ts, packages/core/src/abstract-dialect/query-generator.js
Adds unionQuery(sqls, options?) method that constructs combined SQL by joining queries with UNION or UNION ALL, applying optional ORDER BY and LIMIT/OFFSET clauses.
Query Interface
packages/core/src/abstract-dialect/query-interface.d.ts, packages/core/src/abstract-dialect/query-interface.js
Adds union(sqls, options?) async method that defaults query type to SELECT, delegates SQL building to unionQuery(), and executes via queryRaw().
Sequelize Class
packages/core/src/sequelize.d.ts, packages/core/src/sequelize.js
Adds union(queries, options?) async method that normalizes per-query options, injects model-specific scopes/includes/attributes, handles deduplication via primary key injection, generates SELECT statements, and executes final UNION through query interface.
Integration Tests
packages/core/test/integration/sequelize/union.test.ts
Comprehensive test suite validating union of multiple models, UNION ALL behavior, ORDER BY/LIMIT/OFFSET support, and per-model attribute specification.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Sequelize
    participant QueryInterface
    participant QueryGenerator
    participant Database

    Client->>Sequelize: union(queries, options)
    activate Sequelize
    
    Sequelize->>Sequelize: Normalize & prepare each query<br/>(inject scopes, includes, attributes)
    Sequelize->>Sequelize: Handle deduplication<br/>(inject primary key if needed)
    
    loop For each query
        Sequelize->>QueryGenerator: buildSelectQuery(query)
        QueryGenerator-->>Sequelize: SELECT SQL
    end
    
    Sequelize->>QueryInterface: union(sqls, options)
    deactivate Sequelize
    activate QueryInterface
    
    QueryInterface->>QueryGenerator: unionQuery(sqls, options)
    activate QueryGenerator
    QueryGenerator->>QueryGenerator: Join SQLs with UNION/UNION ALL
    QueryGenerator->>QueryGenerator: Append ORDER BY, LIMIT, OFFSET
    QueryGenerator-->>QueryInterface: Combined SQL
    deactivate QueryGenerator
    
    QueryInterface->>Database: Execute combined SQL
    activate Database
    Database-->>QueryInterface: Result set
    deactivate Database
    
    QueryInterface-->>Client: Results
    deactivate QueryInterface
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Feature Request: Native UNION Query Support #18107: Native UNION support implementation — The changes directly implement the requested UNION query feature by adding Sequelize.prototype.union(), AbstractQueryGenerator.unionQuery(), and comprehensive UnionOptions configuration across all query execution layers.

Poem

🐰 ✨ Multiple queries hop and play,
United under one UNION way,
Deduplication keeps things neat,
With ORDER BY to arrange the sweet,
LIMIT hops last—now the dance is complete! 🎪

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding native UNION support via a sequelize.union method, which is the primary feature introduced across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@packages/core/src/abstract-dialect/query-generator.js`:
- Around line 1495-1506: The ORDER BY direction from options.order is used
directly when building orderClauses; validate the direction against
VALID_ORDER_OPTIONS before interpolating to avoid unexpected SQL. In the order
mapping logic (where orders, orderClauses, and the tuple [col, dir] are
handled), normalize dir to a canonical form (e.g., toUpperCase()), check
inclusion in VALID_ORDER_OPTIONS, and if invalid either throw a clear error or
fall back to a safe default like 'ASC'; then use the validated value when
calling quoteIdentifier(col) and building the clause. Ensure VALID_ORDER_OPTIONS
is referenced and used for validation so behavior matches the rest of the
codebase.
- Around line 1509-1514: The conditional that decides whether to call
this.#internals.addLimitAndOffset uses a truthy check (if (options.limit ||
options.offset)) — change it to use explicit null/undefined checks to match the
file's pattern by testing options.limit != null || options.offset != null so
LIMIT/OFFSET with zero values are handled consistently; update the conditional
near the query assembly where query and limitOrder are used (the block calling
this.#internals.addLimitAndOffset with options.limit/options.offset).

In `@packages/core/src/abstract-dialect/query-interface.js`:
- Around line 585-588: Before calling this.sequelize.queryRaw in the union flow,
remove options.replacements the same way select() does to avoid double
replacements: obtain or clone the options passed to unionQuery (referencing
this.queryGenerator.unionQuery and the local variable options), delete the
replacements property (options.replacements) on that cloned options object, then
pass the sanitized options into this.sequelize.queryRaw(sql, sanitizedOptions).
Ensure you do not mutate the original options object if other callers rely on
it.
- Around line 583-584: The merge currently allows callers to override the query
type; in the async union(sqls, options) function ensure QueryTypes.SELECT is
applied after merging so callers cannot override it. Modify the options
assignment to first merge user options (e.g., options = { ...options }) and then
enforce options.type = QueryTypes.SELECT (or recreate options = { ...options,
type: QueryTypes.SELECT }) so the function always uses SELECT; update references
to options in union accordingly.

In `@packages/core/src/sequelize.js`:
- Around line 247-252: In union, add upfront validation to ensure the queries
array is not empty and each entry has a valid model: check that queries is an
array with length > 0 and iterate queries to verify q && q.model is present (and
optionally that q.options exists); throw a clear TypeError or return a rejected
Promise with a descriptive message if validation fails. Place this check at the
start of the async union(queries, options = {}) method before mapping queries so
that later uses of q.model and q.options (e.g., the mapping that references
q.model and q.options) are safe and produce deterministic errors.
- Line 287: Update the UnionOptions type so it includes transaction support by
extending QueryRawOptions instead of only AddLimitOffsetOptions: find the
UnionOptions type (used by the union method) and change its extends clause to
include QueryRawOptions (which includes Transactionable) so callers can pass a
transaction through the union() call that ultimately delegates to queryRaw;
ensure any existing properties (limit, offset, replacements) remain present and
compatible with QueryRawOptions.

In `@packages/core/test/integration/sequelize/union.test.ts`:
- Line 41: The tests cast sequelize to any because the custom union method is
missing from the Sequelize type definitions; add a TypeScript declaration for
the union method (its signature used in tests) and surface it via module
augmentation or by extending the Sequelize interface so the test instance has
the correct type instead of casting. Concretely, add a declaration file (or
update the package's exported types) that augments the 'sequelize' module /
Sequelize class with union(params: Array<{ model: typeof Model }>):
Promise<any[]> (match the real return type), export that types file from the
package (or include it in the package's types entry), then remove the (sequelize
as any) casts in union.test.ts so the test uses the updated typed
sequelize.union method.
- Around line 96-109: Test currently asserts that 'age' is absent but doesn't
assert whether the primary key was injected when attributes were specified;
update the test for sequelize.union to also assert presence (or absence, as
appropriate) of the injected primary key field (id) when calling (sequelize as
any).union with options { attributes: ['name'] } and raw mode not set, e.g.,
after creating vars.User and vars.Guest check results include the primary key
property 'id' (or verify deduplication behavior relies on id) so the behavior of
attribute injection in the union implementation (union, attributes, raw, id,
vars.User, vars.Guest) is covered.
🧹 Nitpick comments (4)
packages/core/src/abstract-dialect/query-generator.types.ts (1)

15-18: Consider adding JSDoc documentation for the new public interface.

The UnionOptions interface is part of the public API but lacks documentation. Adding JSDoc comments would improve developer experience.

📝 Suggested documentation
+/**
+ * Options for union queries.
+ */
 export interface UnionOptions extends AddLimitOffsetOptions {
+  /** If true, use UNION ALL (keeps duplicates). Defaults to false (UNION with deduplication). */
   unionAll?: boolean;
+  /** Order clause for the combined result. Can be a column name, [column, direction] tuple, or array of either. */
   order?: string | [string, string] | Array<string | [string, string]>;
 }
packages/core/src/sequelize.d.ts (1)

611-617: Consider using a more specific type for the queries parameter.

The queries parameter is typed as object[], which is quite loose. Based on the implementation in sequelize.js, each query object should have model and optional options properties.

💡 Suggested type improvement
+export interface UnionQueryEntry {
+  model: ModelStatic<any>;
+  options?: FindOptions<any>;
+}
+
 /**
  * Run a union query
  *
  * `@param` queries The queries to union. Can be a set of options or model instances.
  * `@param` options The options for the union query.
  */
-union(queries: object[], options?: UnionOptions): Promise<any>;
+union(queries: UnionQueryEntry[], options?: UnionOptions): Promise<any>;
packages/core/test/integration/sequelize/union.test.ts (1)

5-5: Add dialect teaser for test description.

Per coding guidelines, use Support.getTestDialectTeaser() for dialect-specific test descriptions.

📝 Suggested fix
+import { sequelize, getTestDialectTeaser } from '../support';
-import { sequelize } from '../support';
 
-describe('Sequelize#union', () => {
+describe(getTestDialectTeaser('Sequelize#union'), () => {
packages/core/src/sequelize.js (1)

264-274: Add comments explaining the primary key injection logic.

The condition for primary key injection is complex. Adding a comment would help future maintainers understand when and why the primary key is injected.

📝 Suggested comment
+        // Inject primary key into attributes if:
+        // - Custom attributes are specified (not selecting all columns)
+        // - Not a raw query (raw queries bypass model mapping)
+        // - Model has a primary key
+        // - Primary key is not already in the attributes
+        // - Either no grouping, or has multi-association (to ensure proper deduplication)
         if (
           queryOptions.attributes &&
           !queryOptions.raw &&
           model.primaryKeyAttribute &&
           !queryOptions.attributes.includes(model.primaryKeyAttribute) &&
           (!queryOptions.group ||
             !queryOptions.hasSingleAssociation ||
             queryOptions.hasMultiAssociation)
         ) {
           queryOptions.attributes = [model.primaryKeyAttribute].concat(queryOptions.attributes);
         }

Comment on lines +1495 to +1506
if (options.order) {
const orders = Array.isArray(options.order) ? options.order : [options.order];
const orderClauses = orders.map(t => {
if (Array.isArray(t)) {
const [col, dir] = t;

return `${this.quoteIdentifier(col)} ${dir}`;
}

return this.quoteIdentifier(t);
});
query += ` ORDER BY ${orderClauses.join(', ')}`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Order direction should be validated to prevent unexpected SQL.

The dir value from the order tuple is interpolated directly into SQL without validation. While this may not be a direct SQL injection risk (as it's typically controlled by developers), validating against VALID_ORDER_OPTIONS would be consistent with the rest of the codebase.

🛡️ Suggested validation
 const orderClauses = orders.map(t => {
   if (Array.isArray(t)) {
     const [col, dir] = t;
+    const upperDir = dir?.toUpperCase();
+    if (dir && !['ASC', 'DESC', 'ASC NULLS FIRST', 'ASC NULLS LAST', 'DESC NULLS FIRST', 'DESC NULLS LAST', 'NULLS FIRST', 'NULLS LAST'].includes(upperDir)) {
+      throw new Error(`Invalid order direction: ${dir}`);
+    }
-    return `${this.quoteIdentifier(col)} ${dir}`;
+    return `${this.quoteIdentifier(col)}${dir ? ` ${upperDir}` : ''}`;
   }

   return this.quoteIdentifier(t);
 });
🤖 Prompt for AI Agents
In `@packages/core/src/abstract-dialect/query-generator.js` around lines 1495 -
1506, The ORDER BY direction from options.order is used directly when building
orderClauses; validate the direction against VALID_ORDER_OPTIONS before
interpolating to avoid unexpected SQL. In the order mapping logic (where orders,
orderClauses, and the tuple [col, dir] are handled), normalize dir to a
canonical form (e.g., toUpperCase()), check inclusion in VALID_ORDER_OPTIONS,
and if invalid either throw a clear error or fall back to a safe default like
'ASC'; then use the validated value when calling quoteIdentifier(col) and
building the clause. Ensure VALID_ORDER_OPTIONS is referenced and used for
validation so behavior matches the rest of the codebase.

Comment on lines +1509 to +1514
if (options.limit || options.offset) {
const limitOrder = this.#internals.addLimitAndOffset(options);
if (limitOrder) {
query += ` ${limitOrder}`;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find packages -name "query-generator.js" -o -name "query-generator.ts" | head -20

Repository: sequelize/sequelize

Length of output: 467


🏁 Script executed:

cat -n packages/core/src/abstract-dialect/query-generator.js | sed -n '1500,1530p'

Repository: sequelize/sequelize

Length of output: 981


🏁 Script executed:

ast-grep --pattern 'addLimitAndOffset($_) {
  $$$
}'

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

rg -A 5 "class.*extends.*QueryGenerator" packages/

Repository: sequelize/sequelize

Length of output: 11640


🏁 Script executed:

rg -A 10 "addLimitAndOffset" packages/core/src/abstract-dialect/ -t js

Repository: sequelize/sequelize

Length of output: 1855


🏁 Script executed:

rg -A 10 "addLimitAndOffset" packages/ -t ts -t js | head -100

Repository: sequelize/sequelize

Length of output: 7631


🏁 Script executed:

rg "limit.*offset|offset.*limit" packages/core/src/abstract-dialect/query-generator.js -B 3 -A 3

Repository: sequelize/sequelize

Length of output: 2384


🏁 Script executed:

rg "offset.*0|limit.*10" packages/core/tests -A 3 -B 1 | head -60

Repository: sequelize/sequelize

Length of output: 122


🏁 Script executed:

rg "limit.*!=|limit.*null" packages/core/src/abstract-dialect/query-generator.js | head -20

Repository: sequelize/sequelize

Length of output: 549


Condition uses truthy/falsy check; align with codebase pattern using explicit null checks.

The specific example ({ limit: 10, offset: 0 }) will not skip the LIMIT clause—10 || 0 evaluates to 10 (truthy), so the condition passes. However, there is an inconsistency: this condition uses implicit truthiness evaluation while elsewhere in the same file it uses explicit checks like options.limit != null || options.offset (lines 1440, 1452, 1464). For consistency and clarity, consider aligning this condition to use options.limit != null || options.offset instead of relying on truthy/falsy behavior.

🤖 Prompt for AI Agents
In `@packages/core/src/abstract-dialect/query-generator.js` around lines 1509 -
1514, The conditional that decides whether to call
this.#internals.addLimitAndOffset uses a truthy check (if (options.limit ||
options.offset)) — change it to use explicit null/undefined checks to match the
file's pattern by testing options.limit != null || options.offset != null so
LIMIT/OFFSET with zero values are handled consistently; update the conditional
near the query assembly where query and limitOrder are used (the block calling
this.#internals.addLimitAndOffset with options.limit/options.offset).

Comment on lines +583 to +584
async union(sqls, options) {
options = { type: QueryTypes.SELECT, ...options };
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Force type to SELECT after merging options.

The current merge order lets callers override type, which can change result parsing vs select. Set type last to keep behavior consistent.

Proposed fix
-    options = { type: QueryTypes.SELECT, ...options };
+    options = { ...options, type: QueryTypes.SELECT };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async union(sqls, options) {
options = { type: QueryTypes.SELECT, ...options };
async union(sqls, options) {
options = { ...options, type: QueryTypes.SELECT };
🤖 Prompt for AI Agents
In `@packages/core/src/abstract-dialect/query-interface.js` around lines 583 -
584, The merge currently allows callers to override the query type; in the async
union(sqls, options) function ensure QueryTypes.SELECT is applied after merging
so callers cannot override it. Modify the options assignment to first merge user
options (e.g., options = { ...options }) and then enforce options.type =
QueryTypes.SELECT (or recreate options = { ...options, type: QueryTypes.SELECT
}) so the function always uses SELECT; update references to options in union
accordingly.

Comment on lines +585 to +588
const sql = this.queryGenerator.unionQuery(sqls, options);

return await this.sequelize.queryRaw(sql, options);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Drop options.replacements before queryRaw, like select().

This keeps replacement handling consistent and avoids double/inconsistent replacement application.

Proposed fix
     const sql = this.queryGenerator.unionQuery(sqls, options);

+    // unlike bind, replacements are handled by QueryGenerator, not QueryRaw
+    delete options.replacements;
     return await this.sequelize.queryRaw(sql, options);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sql = this.queryGenerator.unionQuery(sqls, options);
return await this.sequelize.queryRaw(sql, options);
}
const sql = this.queryGenerator.unionQuery(sqls, options);
// unlike bind, replacements are handled by QueryGenerator, not QueryRaw
delete options.replacements;
return await this.sequelize.queryRaw(sql, options);
}
🤖 Prompt for AI Agents
In `@packages/core/src/abstract-dialect/query-interface.js` around lines 585 -
588, Before calling this.sequelize.queryRaw in the union flow, remove
options.replacements the same way select() does to avoid double replacements:
obtain or clone the options passed to unionQuery (referencing
this.queryGenerator.unionQuery and the local variable options), delete the
replacements property (options.replacements) on that cloned options object, then
pass the sanitized options into this.sequelize.queryRaw(sql, sanitizedOptions).
Ensure you do not mutate the original options object if other callers rely on
it.

Comment on lines +247 to +252
async union(queries, options = {}) {
// Basic preparations similar to Model.findAll, ensuring options are normalized.
const rawSqls = await Promise.all(
queries.map(async q => {
const model = q.model;
const queryOptions = { ...q.options };
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add validation for empty queries array and model presence.

The method doesn't validate that the queries array is non-empty or that each query has a valid model. This could lead to confusing errors downstream.

🛡️ Suggested validation
 async union(queries, options = {}) {
+  if (!Array.isArray(queries) || queries.length === 0) {
+    throw new Error('union() requires at least one query');
+  }
+
   // Basic preparations similar to Model.findAll, ensuring options are normalized.
   const rawSqls = await Promise.all(
     queries.map(async q => {
       const model = q.model;
+      if (!model) {
+        throw new Error('Each union query must specify a model');
+      }
       const queryOptions = { ...q.options };
🤖 Prompt for AI Agents
In `@packages/core/src/sequelize.js` around lines 247 - 252, In union, add upfront
validation to ensure the queries array is not empty and each entry has a valid
model: check that queries is an array with length > 0 and iterate queries to
verify q && q.model is present (and optionally that q.options exists); throw a
clear TypeError or return a rejected Promise with a descriptive message if
validation fails. Place this check at the start of the async union(queries,
options = {}) method before mapping queries so that later uses of q.model and
q.options (e.g., the mapping that references q.model and q.options) are safe and
produce deterministic errors.

}),
);

return this.queryInterface.union(rawSqls, options);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if queryInterface.union supports transaction options
rg -A 20 "^  union\(" packages/core/src/abstract-dialect/query-interface.js

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Also check the TypeScript version
rg -A 20 "union\(" packages/core/src/abstract-dialect/query-interface.ts

Repository: sequelize/sequelize

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Check the sequelize.js file to see the full context of the union method
rg -B 5 -A 5 "union\(" packages/core/src/sequelize.js

Repository: sequelize/sequelize

Length of output: 752


🏁 Script executed:

#!/bin/bash
# Check how other query methods handle transactions
rg -A 10 "transaction" packages/core/src/abstract-dialect/query-interface.ts | head -50

Repository: sequelize/sequelize

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Find all query-interface files
fd "query-interface" packages/

Repository: sequelize/sequelize

Length of output: 1690


🏁 Script executed:

#!/bin/bash
# Search for union method definition across the codebase
rg "union\(" packages/ --type js --type ts -B 2 -A 15 | head -100

Repository: sequelize/sequelize

Length of output: 6272


🏁 Script executed:

#!/bin/bash
# Check the directory structure of abstract-dialect
ls -la packages/core/src/abstract-dialect/ | head -30

Repository: sequelize/sequelize

Length of output: 1572


🏁 Script executed:

#!/bin/bash
# Check queryRaw implementation to see if it handles transaction options
rg -A 15 "async queryRaw" packages/core/src/abstract-dialect/query-interface.js

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check the TypeScript version of union method
rg -A 10 "union\(" packages/core/src/abstract-dialect/query-interface-typescript.ts

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if there are tests for union with transactions
rg -B 2 -A 10 "union.*transaction\|transaction.*union" packages/core/test/ --type js --type ts

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check UnionOptions type to see what options are supported
rg -B 5 -A 10 "UnionOptions" packages/core/src/

Repository: sequelize/sequelize

Length of output: 8725


🏁 Script executed:

#!/bin/bash
# Check AddLimitOffsetOptions to see what options are included
rg -B 2 -A 15 "interface AddLimitOffsetOptions" packages/core/src/

Repository: sequelize/sequelize

Length of output: 1436


🏁 Script executed:

#!/bin/bash
# Check the sequelize.queryRaw method to see if it handles transaction
rg -B 5 -A 25 "async queryRaw" packages/core/src/sequelize.js

Repository: sequelize/sequelize

Length of output: 1185


🏁 Script executed:

#!/bin/bash
# Look for transaction option handling in query execution
rg "transaction" packages/core/src/abstract-dialect/query-interface.js -A 2 -B 2 | head -60

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check how select or other methods handle transaction options
rg -B 5 -A 15 "transaction" packages/core/src/abstract-dialect/query-interface-typescript.ts | head -80

Repository: sequelize/sequelize

Length of output: 2772


🏁 Script executed:

#!/bin/bash
# Look for base QueryOptions or similar that includes transaction
rg "interface.*Options" packages/core/src/abstract-dialect/query-interface.types.ts -A 8 | head -100

Repository: sequelize/sequelize

Length of output: 4616


🏁 Script executed:

#!/bin/bash
# Check how queryRaw actually executes the query and if transaction is used
rg -A 40 "async queryRaw" packages/core/src/sequelize.js | tail -50

Repository: sequelize/sequelize

Length of output: 1890


🏁 Script executed:

#!/bin/bash
# Find QueryRawOptions definition
rg -B 2 -A 20 "interface QueryRawOptions\|type QueryRawOptions" packages/core/src/

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if other union-like methods extend QueryRawOptions
rg -B 3 -A 10 "interface.*Options.*extends.*QueryRawOptions" packages/core/src/abstract-dialect/query-interface.types.ts | head -60

Repository: sequelize/sequelize

Length of output: 2699


🏁 Script executed:

#!/bin/bash
# Search in sequelize.ts for QueryRawOptions
rg -B 5 -A 25 "interface QueryRawOptions\|type QueryRawOptions" packages/core/src/sequelize.ts

Repository: sequelize/sequelize

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Check how sequelize.query handles transactions vs sequelize.queryRaw
rg -B 5 -A 15 "^\s+query\(" packages/core/src/sequelize.js | head -50

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Confirm UnionOptions is used in union method signature
cat packages/core/src/abstract-dialect/query-interface.d.ts | grep -A 5 "union("

Repository: sequelize/sequelize

Length of output: 196


🏁 Script executed:

#!/bin/bash
# Search for QueryRawOptions in all files
rg "QueryRawOptions" packages/core/src/ -l

Repository: sequelize/sequelize

Length of output: 445


🏁 Script executed:

#!/bin/bash
# Check the sequelize.d.ts for QueryRawOptions
rg -B 5 -A 20 "QueryRawOptions" packages/core/src/sequelize.d.ts | head -80

Repository: sequelize/sequelize

Length of output: 2635


🏁 Script executed:

#!/bin/bash
# Check if there are any tests that show union with transaction working or failing
fd ".*test.*" packages/core/test/ -type f | xargs grep -l "union" | head -5

Repository: sequelize/sequelize

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Search for Transactionable interface
rg -B 2 -A 10 "interface Transactionable" packages/core/src/

Repository: sequelize/sequelize

Length of output: 916


🏁 Script executed:

#!/bin/bash
# Check query tests to see if union with transaction is tested
find packages/core/test -name "*.js" -o -name "*.ts" | xargs grep -l "\.union" | head -3

Repository: sequelize/sequelize

Length of output: 117


🏁 Script executed:

#!/bin/bash
# Read the union test file to see what's tested
cat packages/core/test/integration/sequelize/union.test.ts

Repository: sequelize/sequelize

Length of output: 3296


🏁 Script executed:

#!/bin/bash
# Check how the union method is implemented in query-interface-typescript to be thorough
rg -B 5 -A 15 "union\(" packages/core/src/abstract-dialect/query-interface-typescript.ts

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if union is defined in query-interface-typescript at all
rg "union" packages/core/src/abstract-dialect/query-interface-typescript.ts

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Let's verify the complete flow: sequelize.union -> queryInterface.union -> queryRaw
# Check the actual implementation in query-interface-typescript.ts to see union method
cat packages/core/src/abstract-dialect/query-interface-typescript.ts | grep -A 10 "union"

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Let me verify the UnionOptions definition one more time and see what it really extends
cat packages/core/src/abstract-dialect/query-generator.types.ts | grep -B 2 -A 10 "export interface UnionOptions"

Repository: sequelize/sequelize

Length of output: 476


🏁 Script executed:

#!/bin/bash
# Check how select or other QueryInterface methods define their options
rg "selectQuery\|findAll" packages/core/src/abstract-dialect/query-interface.d.ts -A 3 | head -30

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if there's any guidance in the codebase about why UnionOptions doesn't extend QueryRawOptions
git log --all --oneline --grep="UnionOptions\|union" -- packages/core/src/abstract-dialect/query-generator.types.ts 2>/dev/null | head -5

Repository: sequelize/sequelize

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Check the commit that added union to understand intent
git show 434ec30 --stat 2>/dev/null | head -20

Repository: sequelize/sequelize

Length of output: 1099


🏁 Script executed:

#!/bin/bash
# Let me also verify what Logging and Poolable interfaces contain in QueryRawOptions
rg -B 2 -A 8 "interface Logging\|interface Poolable" packages/core/src/model.d.ts

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Let me check what the actual transaction behavior would be
# by looking at how other methods handle transactions with QueryRawOptions
rg -B 3 -A 10 "interface.*Options extends.*QueryRawOptions" packages/core/src/abstract-dialect/query-interface.types.ts | grep -A 10 "Select\|Find" | head -30

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Final check: confirm UnionOptions is the intended type for union method
rg "union.*UnionOptions" packages/core/src/

Repository: sequelize/sequelize

Length of output: 398


Extend UnionOptions to include transaction support via QueryRawOptions.

The union method's UnionOptions type definition only extends AddLimitOffsetOptions (limit, offset, replacements), but the underlying queryRaw call accepts QueryRawOptions, which includes Transactionable (transaction property). Update UnionOptions to extend QueryRawOptions so users can pass transaction options without TypeScript type violations. Currently, the type signature prevents valid transactions even though the runtime implementation would support them.

🤖 Prompt for AI Agents
In `@packages/core/src/sequelize.js` at line 287, Update the UnionOptions type so
it includes transaction support by extending QueryRawOptions instead of only
AddLimitOffsetOptions: find the UnionOptions type (used by the union method) and
change its extends clause to include QueryRawOptions (which includes
Transactionable) so callers can pass a transaction through the union() call that
ultimately delegates to queryRaw; ensure any existing properties (limit, offset,
replacements) remain present and compatible with QueryRawOptions.

await vars.User.create({ name: 'Alice', age: 20 });
await vars.Guest.create({ name: 'Bob', age: 30 });

const results = await (sequelize as any).union([{ model: vars.User }, { model: vars.Guest }]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type cast indicates missing type definition.

The (sequelize as any).union cast is used throughout the tests, indicating that the union method type isn't properly exported or the test's sequelize instance doesn't have the updated types. This should be addressed to ensure type safety in tests.

🤖 Prompt for AI Agents
In `@packages/core/test/integration/sequelize/union.test.ts` at line 41, The tests
cast sequelize to any because the custom union method is missing from the
Sequelize type definitions; add a TypeScript declaration for the union method
(its signature used in tests) and surface it via module augmentation or by
extending the Sequelize interface so the test instance has the correct type
instead of casting. Concretely, add a declaration file (or update the package's
exported types) that augments the 'sequelize' module / Sequelize class with
union(params: Array<{ model: typeof Model }>): Promise<any[]> (match the real
return type), export that types file from the package (or include it in the
package's types entry), then remove the (sequelize as any) casts in
union.test.ts so the test uses the updated typed sequelize.union method.

Comment on lines +96 to +109
it('allows specifying attributes in query options', async () => {
await vars.User.create({ name: 'Alice', age: 20 });
await vars.Guest.create({ name: 'Bob', age: 30 });

// Only select name
const results = await (sequelize as any).union([
{ model: vars.User, options: { attributes: ['name'] } },
{ model: vars.Guest, options: { attributes: ['name'] } },
]);

expect(results).to.have.lengthOf(2);
expect(results[0]).to.have.property('name');
expect(results[0]).to.not.have.property('age');
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find packages/core/src -name "*union*" -o -name "*sequelize*" | head -20

Repository: sequelize/sequelize

Length of output: 296


🏁 Script executed:

rg -l "union" packages/core/src --type ts --type js | head -10

Repository: sequelize/sequelize

Length of output: 433


🏁 Script executed:

rg -A 30 "^\s*(union|\.union)\s*\(" packages/core/src/sequelize.js | head -100

Repository: sequelize/sequelize

Length of output: 45


🏁 Script executed:

wc -l packages/core/src/sequelize.js

Repository: sequelize/sequelize

Length of output: 97


🏁 Script executed:

rg "union" packages/core/src/sequelize.js -A 20

Repository: sequelize/sequelize

Length of output: 1605


🏁 Script executed:

cat packages/core/test/integration/sequelize/union.test.ts | head -120

Repository: sequelize/sequelize

Length of output: 3296


🏁 Script executed:

rg "Inject primary key if needed for deduplication" packages/core/src/sequelize.js -A 40

Repository: sequelize/sequelize

Length of output: 1564


Test incompletely verifies attribute filtering—does not check for injected primary key.

The union implementation injects the primary key when attributes is specified and !raw is true. In this test, since attributes: ['name'] is specified and the primary key is not included, the implementation will inject the id column into the query. The test verifies that age is absent but does not verify whether id appears in the results, leaving a gap in coverage of the deduplication behavior that relies on the primary key.

🤖 Prompt for AI Agents
In `@packages/core/test/integration/sequelize/union.test.ts` around lines 96 -
109, Test currently asserts that 'age' is absent but doesn't assert whether the
primary key was injected when attributes were specified; update the test for
sequelize.union to also assert presence (or absence, as appropriate) of the
injected primary key field (id) when calling (sequelize as any).union with
options { attributes: ['name'] } and raw mode not set, e.g., after creating
vars.User and vars.Guest check results include the primary key property 'id' (or
verify deduplication behavior relies on id) so the behavior of attribute
injection in the union implementation (union, attributes, raw, id, vars.User,
vars.Guest) is covered.

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.

1 participant