Skip to content

Commit

Permalink
feat: reserve ::, -> syntax in attribute names (#14181)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `::` & `->` cannot be used in attribute names anymore (see out upgrade-to-v7 guide for more information)
  • Loading branch information
ephys committed Mar 11, 2022
1 parent c41c5b9 commit ed7b03b
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 21 deletions.
54 changes: 37 additions & 17 deletions docs/manual/other-topics/upgrade-to-v7.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,32 @@ await sequelize.authenticate();

### Support for Node 12 and up

Sequelize v7 will only support those versions of Node.js that are compatible with the ES module specification,
Sequelize v7 will only support the versions of Node.js that are compatible with the ES module specification,
namingly version 12 and upwards [#5](https://github.com/sequelize/meetings/issues/5).

### TypeScript conversion

One of the major foundational code changes of v7 is the migration to TypeScript.
One of the major foundational code changes of v7 is the migration to TypeScript.\
As a result, the manual typings that were formerly best-effort guesses on top of the JavaScript code base,
have been removed and all typings are now directly retrieved from the actual TypeScript code.

You'll likely find many tiny differences which however should be easy to fix.

### Attributes cannot start and end with `$`, or include `.`
### Attribute names cannot use syntax reserved by Sequelize

*Attributes cannot start or end with `$`, include `.`, include `::`, or include `->`. Column names are not impacted.*

`$attribute$` & `$nested.attribute$` is a special syntax used to reference nested attributes in Queries.
`$attribute$` & `$nested.attribute$` is a special syntax used to reference nested attributes in Queries.\
The `.` character also has special meaning, being used to reference nested JSON object keys,
the `$nested.attribute$` syntax, and in output names of eager-loaded associations in SQL queries.

In Sequelize 6, it was possible to create an attribute that matched this special syntax, leading to subtle bugs.
The `->` character sequence is [used internally to reference nested associations](https://github.com/sequelize/sequelize/pull/14181#issuecomment-1053591214).

Finally, the `::` character sequence has special meaning in queries as it allows you to tell sequelize to cast an attribute.

In Sequelize 6, it was possible to create an attribute that matched these special syntaxes, leading to subtle bugs.\
Starting with Sequelize 7, this is now considered reserved syntax, and it is no longer possible to
use a string that both starts and ends with a `$` as the attribute name, or includes the `.` character.
use a string that both starts or ends with a `$` as the attribute name, includes the `.` character, or includes `::`.

This only affects the attribute name, it is still possible to do this for the column name.

Expand All @@ -51,21 +58,27 @@ import { DataTypes, Model } from '@sequelize/core';
class User extends Model {
$myAttribute$: string;
'another.attribute': string;
'other::attribute': string;
}

User.init({
// this key sets the JavaScript name.
// It's not allowed to start & end with $ anymore.
// It's not allowed to start or end with $ anymore.
'$myAttribute$': {
type: DataTypes.STRING,
// 'field' sets the column name
field: '$myAttribute$'
field: '$myAttribute$',
},
// The JavaScript name is not allowed to include a dot anymore.
'another.attribute': {
type: DataTypes.STRING,
field: 'another.attribute'
}
field: 'another.attribute',
},
// The JavaScript name is not allowed to include '::' anymore.
'other::attribute': {
type: DataTypes.STRING,
field: 'other::attribute',
},
}, { sequelize });
```

Expand All @@ -75,21 +88,27 @@ Do this:
import { DataTypes, Model } from '@sequelize/core';

class User extends Model {
$myAttribute$: string;
'another.attribute': string;
myAttribute: string;
anotherAttribute: string;
otherAttribute: string;
}

User.init({
'myAttribute': {
myAttribute: {
type: DataTypes.STRING,
// Column names are still allowed to start & end with $
field: '$myAttribute$'
field: '$myAttribute$', // this sets the column name
},
'anotherAttribute': {
anotherAttribute: {
type: DataTypes.STRING,
// Column names are still allowed to include dots
field: 'another.attribute' // this sets the column name
}
field: 'another.attribute',
},
otherAttribute: {
type: DataTypes.STRING,
// Column names are still allowed to include ::
field: 'other::attribute',
},
}, { sequelize });
```

Expand Down Expand Up @@ -117,6 +136,7 @@ https://docs.microsoft.com/en-us/sql/sql-server/end-of-support/sql-server-end-of
### Overridden Model methods won't be called internally

`Model.findOne` and `Model.findAll` are used respectively by `Model.findByPk` and `Model.findOne`.

This is considered an implementation detail and as such, starting with Sequelize v7,
overrides of either of these methods will not be called internally by `Model.findByPk` or `Model.findOne`.

Expand Down
19 changes: 17 additions & 2 deletions src/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1063,14 +1063,29 @@ class Model {
this.rawAttributes = _.mapValues(attributes, (attribute, name) => {
attribute = this.sequelize.normalizeAttribute(attribute);

if (Utils.isColString(name)) {
throw new Error(`Name of attribute "${name}" in model "${this.name}" cannot start and end with "$" as "$attribute$" is reserved syntax used to reference nested columns in queries.`);
// Checks whether the name is ambiguous with Utils.isColString
// we check whether the attribute starts *or* ends because the following query:
// { '$json.key$' }
// could be interpreted as both
// "json"."key" (accessible attribute 'key' on model 'json')
// or
// "$json" #>> {key$} (accessing key 'key$' on attribute '$json')
if (name.startsWith('$') || name.endsWith('$')) {
throw new Error(`Name of attribute "${name}" in model "${this.name}" cannot start or end with "$" as "$attribute$" is reserved syntax used to reference nested columns in queries.`);
}

if (name.includes('.')) {
throw new Error(`Name of attribute "${name}" in model "${this.name}" cannot include the character "." as it would be ambiguous with the syntax used to reference nested columns, and nested json keys, in queries.`);
}

if (name.includes('::')) {
throw new Error(`Name of attribute "${name}" in model "${this.name}" cannot include the character sequence "::" as it is reserved syntax used to cast attributes in queries.`);
}

if (name.includes('->')) {
throw new Error(`Name of attribute "${name}" in model "${this.name}" cannot include the character sequence "->" as it is reserved syntax used in SQL generated by Sequelize to target nested associations.`);
}

if (attribute.type === undefined) {
throw new Error(`Unrecognized datatype for attribute "${this.name}.${name}"`);
}
Expand Down
26 changes: 24 additions & 2 deletions test/unit/model/define.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,15 @@ describe(Support.getTestDialectTeaser('Model'), () => {
it('should throw when the attribute name is ambiguous with $nested.attribute$ syntax', () => {
expect(() => {
current.define('foo', {
$id$: DataTypes.INTEGER,
$id: DataTypes.INTEGER,
});
}).to.throw('Name of attribute "$id$" in model "foo" cannot start and end with "$" as "$attribute$" is reserved syntax used to reference nested columns in queries.');
}).to.throw('Name of attribute "$id" in model "foo" cannot start or end with "$" as "$attribute$" is reserved syntax used to reference nested columns in queries.');

expect(() => {
current.define('foo', {
id$: DataTypes.INTEGER,
});
}).to.throw('Name of attribute "id$" in model "foo" cannot start or end with "$" as "$attribute$" is reserved syntax used to reference nested columns in queries.');
});

it('should throw when the attribute name is ambiguous with json.path syntax', () => {
Expand All @@ -62,6 +68,22 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}).to.throw('Name of attribute "my.attribute" in model "foo" cannot include the character "." as it would be ambiguous with the syntax used to reference nested columns, and nested json keys, in queries.');
});

it('should throw when the attribute name is ambiguous with casting syntax', () => {
expect(() => {
current.define('foo', {
'id::int': DataTypes.INTEGER,
});
}).to.throw('Name of attribute "id::int" in model "foo" cannot include the character sequence "::" as it is reserved syntax used to cast attributes in queries.');
});

it('should throw when the attribute name is ambiguous with nested-association syntax', () => {
expect(() => {
current.define('foo', {
'my->attribute': DataTypes.INTEGER,
});
}).to.throw('Name of attribute "my->attribute" in model "foo" cannot include the character sequence "->" as it is reserved syntax used in SQL generated by Sequelize to target nested associations.');
});

it('should defend against null or undefined "unique" attributes', () => {
expect(() => {
current.define('baz', {
Expand Down

0 comments on commit ed7b03b

Please sign in to comment.