Skip to content

fix: correct attributeToSQL across all dialects - #18367

Draft
wikirik-agent wants to merge 7 commits into
sequelize:mainfrom
wikirik-agent:attributes-to-sql-fixes
Draft

fix: correct attributeToSQL across all dialects#18367
wikirik-agent wants to merge 7 commits into
sequelize:mainfrom
wikirik-agent:attributes-to-sql-fixes

Conversation

@wikirik-agent

@wikirik-agent wikirik-agent commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Description of Changes

Second of the stack re-implementing #17121. Depends on #18366 — its commit is the first of the four here, since GitHub cannot base a cross-fork PR on a branch that only exists on the fork. Review #18366 first; only the last three commits belong to this PR.

#18366 pinned current behaviour exactly. This PR fixes it. Every expectation that changes in attribute-to-sql.test.ts / attributes-to-sql.test.ts is a behaviour delta, so the test diff is the review surface.

All nine dialect unit suites pass (2,296–2,906 tests each), clean eslint and clean tsc on both --noEmit and test/tsconfig.json.

Commits

fix: make the "this type cannot have a default" guards work again

Every dialect that refuses a DEFAULT for certain types was checking conditions that can never be true on v7 data types:

  • attribute.type._binary was a v6 property of STRING/CHAR set by .BINARY. It does not exist in v7 — I checked the v6 source in git history to confirm what it meant. It is now attribute.type.options?.binary.
  • db2, mssql and oracle compared attribute.type !== 'TEXT', i.e. a DataType instance against a string.
  • mariadb, mysql, snowflake and ibmi looked the rendered SQL string up in a set of type names, so DataTypes.TEXT('tiny')TINYTEXT missed, and ibmi never matched at all since its BLOB renders as BLOB(1M).

Lookups now use a new attributeTypeToDataTypeId core utility built on the existing getDataTypeId(), which returns a type's dialect-independent identity. Each dialect keeps exactly the list of types it already named — I did not re-decide which types reject defaults.

A second commit then removes most of those lists again. Once the guards were live I checked them against real servers, and the restrictions largely do not exist:

  • db2, mssql and oracle all accept a DEFAULT on their LOB types. CLOB DEFAULT 'abc', NVARCHAR(MAX) DEFAULT N'abc' and BLOB DEFAULT BLOB('abc') all work, and the value round-trips. The SQL Server folklore appears to come from CREATE TABLE's rule that CHECK constraints cannot be defined on text/ntext/image — a different clause. Those three lists are removed.
  • .BINARY on a string type is a collation, not a storage class, and every server tested accepts a default on such a column. That guard is removed everywhere rather than repaired.

So the net effect of the two commits is: the guards no longer silently discard a defaultValue the user asked for. mariadb, mysql, snowflake and ibmi keep the lists they had, now matched by data type identity rather than by rendered SQL string. (#18369 revisits mariadb's and mysql's, which turned out to be wrong too.)

fix: give attributeToSQL one way to name the table and the column

The table arrived under two names and the column under three, and no caller passed all of them:

  • Core passed table; mysql/mariadb/snowflake/db2/ibmi read tableName. Passing the name declared in AttributeToSqlOptions threw on mariadb/mysql and produced undefined_..._foreign_idx on snowflake/db2/ibmi.
  • The column was key on postgres, attributeName on oracle, foreignKey elsewhere — all duplicating attribute.field.

Now: one tableOrModel option, and the column always read from attribute.field, which attributesToSQL and every addColumnQuery set. schema is gone — core already stamps the multi-tenancy schema onto the table it passes, so an explicitly-qualified reference target now keeps its own schema rather than being overridden.

Also fixed here:

  • snowflake/ibmi built FK constraint names from an already-quoted column ("myTable_""myColumn""_foreign_idx"); db2 left the name unquoted entirely.
  • sqlite3, snowflake and ibmi ignored withoutForeignKeyConstraints.
  • postgres dereferenced three options unguarded, so attributeToSQL threw for any attribute with references and no options — which also made deferrable references unreachable.
  • mssql and ibmi threw on plain string attributes ({ id: 'INTEGER' }) that the other seven accept.
  • snowflake and sqlite3 did not normalize the data type in addColumnQuery.
  • columnName is now honoured by every dialect, not only sqlite3.

fix(db2,ibmi): emit the nullability change as its own ALTER COLUMN clause

Neither can change a column's type and nullability in one ALTER COLUMN clause.

db2 returned an array of fragments in the changeColumn context so changeColumnQuery could split them — one method returning string or string[] depending on its options. The COMMENT branch then appended to that array, coercing it to DATA TYPE INTEGER,DROP NOT NULL COMMENT Test and collapsing both clauses back into one invalid fragment.

ibmi had the same bug invisibly, emitting ALTER COLUMN "c" SET DATA TYPE INTEGER DROP NOT NULL, and joined multiple columns with a comma instead of repeating ALTER COLUMN.

attributeToSQL now always returns a string; changeColumnQuery receives the attribute definitions alongside the generated SQL and emits the nullability clause itself.

Notes for review

  • Integration suites now run against real servers: mssql 411 passing, oracle 388 passing (needs TZ=UTC), db2 19 passing on the query-interface suite. ibmi is still unverified — there is no way to run it locally, so its ALTER COLUMN change rests on code reading alone.
  • Two items from fix: overhaul test suite of attribute(s)ToSQL #17121 turned out not to need fixing: ARRAY(ENUM) on postgres was already corrected upstream, and comment escaping is already done by every consumer that extracts the in-band COMMENT marker. Escaping at the source (as fix: overhaul test suite of attribute(s)ToSQL #17121 does) would double-escape and is what caused that PR to leak N'FooBar' into describeTable.
  • db2's attributeToSQL still has an unreachable addColumn FK branch — its addColumnQuery never triggers it. I fixed the quoting rather than deleting it, since whether db2 needs the , CONSTRAINT form is a call I can't verify.

Remaining follow-up

  • Convert both methods to TypeScript and rename to attributeToSql / attributesToSql

List of Breaking Changes

AttributeToSqlOptions changes shape (table/tableName/schema/key/foreignKey/attributeNametableOrModel plus attribute.field). It is exported only under _non-semver-use-at-your-own-risk_.

🤖 Generated with Claude Code

wikirik-agent and others added 5 commits September 11, 2026 22:54
Replaces the per-dialect `attributesToSQL` suites with two dialect-agnostic
suites that cover all nine dialects, including ibmi and mssql which had no
coverage at all.

The expectations pin current behaviour exactly, so this commit changes no SQL.
Behaviour that is wrong today is pinned as-is and marked with a TODO.

`sqlite3` gains an `attributeToSQL` method, extracted from `attributesToSQL`
without changing its output, so the shared suite can cover it like every other
dialect.

`inlineErrorCause` now inspects non-error values instead of stringifying them:
mssql and ibmi return null-prototype maps from `attributesToSQL`, which threw
when the assertion message was built.

Closes sequelize#15533

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every dialect that refuses a DEFAULT for certain column types was checking
conditions that can never be true on v7 data types:

- `attribute.type._binary` was a v6 property of STRING/CHAR set by `.BINARY`.
  It does not exist in v7, so the check was always a no-op. It is now
  `attribute.type.options?.binary`, which is where that flag lives today.
- db2, mssql and oracle compared `attribute.type !== 'TEXT'`, comparing a
  DataType instance against a string, so it never matched.
- mariadb, mysql, snowflake and ibmi looked up the rendered SQL string in a set
  of type names. That only matched the default length variant, and never
  matched at all on ibmi, whose BLOB renders as `BLOB(1M)`.

The lookups now use `attributeTypeToDataTypeId`, added to the core data type
utilities, which returns the dialect-independent identity of a type. Each
dialect keeps exactly the list of types it already named.

As a result db2, mssql and oracle stop emitting a DEFAULT for BLOB and TEXT
columns, and ibmi stops emitting one for BLOB. A raw `'BLOB'` string type with
a default no longer throws while trying to escape the value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`attributeToSQL` received the table under two different names and the column
under three, and no caller passed all of them:

- Core passed `table`, while mysql, mariadb, snowflake, db2 and ibmi read
  `tableName`. Passing the name declared in `AttributeToSqlOptions` therefore
  threw on mariadb and mysql, and named the constraint `undefined_..._foreign_idx`
  on snowflake, db2 and ibmi.
- The column arrived as `key` on postgres, `attributeName` on oracle and
  `foreignKey` everywhere else, duplicating `attribute.field`.

There is now a single `tableOrModel` option, accepting anything `quoteTable`
does, and the column name is always read from `attribute.field`, which
`attributesToSQL` and every `addColumnQuery` now set. `schema` is gone: core
already stamps the multi-tenancy schema onto the table it passes, so an
explicitly qualified reference target keeps its own schema instead of being
overridden.

Fixed along the way:

- snowflake and ibmi built foreign key constraint names out of an
  already-quoted column, producing `"myTable_""myColumn""_foreign_idx"`, and db2
  left the constraint name unquoted entirely.
- sqlite3, snowflake and ibmi ignored `withoutForeignKeyConstraints`.
- postgres dereferenced `options.schema`, `options.table` and
  `options.withoutForeignKeyConstraints` without guarding, so `attributeToSQL`
  threw for any attribute with `references` and no options. Deferrable
  references were unreachable as a result.
- mssql and ibmi threw on plain string attributes such as `{ id: 'INTEGER' }`,
  which the other seven dialects accept.
- snowflake and sqlite3 did not normalize the data type in `addColumnQuery`.
- `columnName` is now honoured by every dialect, not only sqlite3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ause

Neither dialect can change a column's type and its nullability in a single
ALTER COLUMN clause, and both got it wrong in a different way.

db2's `attributeToSQL` returned an array of fragments in the changeColumn
context so that `changeColumnQuery` could split them into separate clauses,
which meant one method returned a string or an array depending on its options.
Worse, the COMMENT branch that follows appended to that array, coercing it into
`DATA TYPE INTEGER,DROP NOT NULL COMMENT Test` and collapsing the two clauses
back into one invalid fragment.

ibmi produced `ALTER COLUMN "c" SET DATA TYPE INTEGER DROP NOT NULL`, a single
clause the server rejects, and joined multiple columns with a comma rather than
repeating ALTER COLUMN.

`attributeToSQL` now always returns a string. `changeColumnQuery` receives the
attribute definitions alongside the generated SQL and emits the nullability as
its own clause, so both dialects produce the form db2 already produced for
`allowNull: false`.

ibmi also quotes the column through `quoteIdentifier` instead of interpolating
double quotes directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes the notes describing behaviour this branch fixed, and corrects two that
were wrong: db2 and mssql drop the referential actions of repeated references
to the same table on purpose, because both servers reject more than one
cascading constraint to a table. Only db2's extra rule for unique attributes
has no known justification, so that one keeps a TODO.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

Follow-up to the previous commit, which restored guards that turned out to be
describing restrictions that do not exist. Verified against db2 12.1.4, SQL
Server 2025, Oracle 23.26, MySQL 8.4 and MariaDB 11.6:

- db2, mssql and oracle accept a DEFAULT on their LOB types. `CLOB DEFAULT
  'abc'`, `NVARCHAR(MAX) DEFAULT N'abc'` and `BLOB DEFAULT BLOB('abc')` all
  work. The SQL Server folklore appears to come from CREATE TABLE's rule that
  CHECK constraints cannot be defined on text/ntext/image, which is a different
  clause. Those three lists are removed.
- `.BINARY` on a string type is a collation, not a storage class, and every
  server tested accepts a default on such a column. That guard is removed
  everywhere rather than repaired.

mariadb, mysql, snowflake and ibmi keep the lists they already had, now matched
by data type identity rather than by rendered SQL string.

Without this, a `defaultValue` the user asked for would silently vanish.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Giving `attributeToSQL` a single column option made db2's `addColumn` foreign
key branch reachable for the first time: it used to require `options.foreignKey`,
which db2's `addColumnQuery` never passed, and now reads `attribute.field`,
which it does set.

The branch separated the two clauses with a comma, which db2 rejects — ALTER
TABLE actions are not comma separated. It now uses ` ADD CONSTRAINT`, the same
form ibmi uses, which db2 accepts.

Caught by the removeColumn integration test against db2 12.1.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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