不要怂,就是干,撸起袖子干!

Commit 18e04a2b by Scott Rocha Committed by Felix Becker

feat(operators): Add $regexp operator (#7855)

Adds $regexp operator for MySQL and Postgres and its negated and case-insensitive versions.
1 parent 0528f39a
...@@ -135,6 +135,10 @@ $like: '%hat', // LIKE '%hat' ...@@ -135,6 +135,10 @@ $like: '%hat', // LIKE '%hat'
$notLike: '%hat' // NOT LIKE '%hat' $notLike: '%hat' // NOT LIKE '%hat'
$iLike: '%hat' // ILIKE '%hat' (case insensitive) (PG only) $iLike: '%hat' // ILIKE '%hat' (case insensitive) (PG only)
$notILike: '%hat' // NOT ILIKE '%hat' (PG only) $notILike: '%hat' // NOT ILIKE '%hat' (PG only)
$regexp: '^[h|a|t]' // REGEXP/~ '^[h|a|t]' (MySQL/PG only)
$notRegexp: '^[h|a|t]' // NOT REGEXP/!~ '^[h|a|t]' (MySQL/PG only)
$iRegexp: '^[h|a|t]' // ~* '^[h|a|t]' (PG only)
$notIRegexp: '^[h|a|t]' // !~* '^[h|a|t]' (PG only)
$like: { $any: ['cat', 'hat']} $like: { $any: ['cat', 'hat']}
// LIKE ANY ARRAY['cat', 'hat'] - also works for iLike and notLike // LIKE ANY ARRAY['cat', 'hat'] - also works for iLike and notLike
$overlap: [1, 2] // && [1, 2] (PG array overlap operator) $overlap: [1, 2] // && [1, 2] (PG array overlap operator)
......
...@@ -1977,6 +1977,10 @@ const QueryGenerator = { ...@@ -1977,6 +1977,10 @@ const QueryGenerator = {
$notLike: 'NOT LIKE', $notLike: 'NOT LIKE',
$iLike: 'ILIKE', $iLike: 'ILIKE',
$notILike: 'NOT ILIKE', $notILike: 'NOT ILIKE',
$regexp: '~',
$notRegexp: '!~',
$iRegexp: '~*',
$notIRegexp: '!~*',
$between: 'BETWEEN', $between: 'BETWEEN',
$notBetween: 'NOT BETWEEN', $notBetween: 'NOT BETWEEN',
$overlap: '&&', $overlap: '&&',
...@@ -2282,7 +2286,20 @@ const QueryGenerator = { ...@@ -2282,7 +2286,20 @@ const QueryGenerator = {
comparator = 'IS NOT'; comparator = 'IS NOT';
} }
if (comparator.indexOf('~') !== -1) {
escapeValue = false;
}
if (this._dialect.name === 'mysql') {
if (comparator === '~') {
comparator = 'REGEXP';
} else if (comparator === '!~') {
comparator = 'NOT REGEXP';
}
}
escapeOptions.acceptStrings = comparator.indexOf('LIKE') !== -1; escapeOptions.acceptStrings = comparator.indexOf('LIKE') !== -1;
escapeOptions.acceptRegExp = comparator.indexOf('~') !== -1 || comparator.indexOf('REGEXP') !== -1;
if (escapeValue) { if (escapeValue) {
value = this.escape(value, field, escapeOptions); value = this.escape(value, field, escapeOptions);
...@@ -2291,6 +2308,8 @@ const QueryGenerator = { ...@@ -2291,6 +2308,8 @@ const QueryGenerator = {
if (escapeOptions.acceptStrings && comparator.indexOf('ANY') > comparator.indexOf('LIKE')) { if (escapeOptions.acceptStrings && comparator.indexOf('ANY') > comparator.indexOf('LIKE')) {
value = '(' + value + ')'; value = '(' + value + ')';
} }
} else if (escapeOptions.acceptRegExp) {
value = '\'' + value + '\'';
} }
} }
......
...@@ -41,7 +41,8 @@ MysqlDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype. ...@@ -41,7 +41,8 @@ MysqlDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.
updateOnDuplicate: true, updateOnDuplicate: true,
indexViaAlter: true, indexViaAlter: true,
NUMERIC: true, NUMERIC: true,
GEOMETRY: true GEOMETRY: true,
REGEXP: true
}); });
ConnectionManager.prototype.defaultVersion = '5.6.0'; ConnectionManager.prototype.defaultVersion = '5.6.0';
......
...@@ -44,6 +44,7 @@ PostgresDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototy ...@@ -44,6 +44,7 @@ PostgresDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototy
ARRAY: true, ARRAY: true,
RANGE: true, RANGE: true,
GEOMETRY: true, GEOMETRY: true,
REGEXP: true,
GEOGRAPHY: true, GEOGRAPHY: true,
JSON: true, JSON: true,
JSONB: true, JSONB: true,
......
...@@ -1406,7 +1406,7 @@ class Model { ...@@ -1406,7 +1406,7 @@ class Model {
* ```sql * ```sql
* WHERE attr1 > 50 AND attr2 <= 45 AND attr3 IN (1,2,3) AND attr4 != 5 * WHERE attr1 > 50 AND attr2 <= 45 AND attr3 IN (1,2,3) AND attr4 != 5
* ``` * ```
* Possible options are: `$ne, $in, $not, $notIn, $gte, $gt, $lte, $lt, $like, $ilike/$iLike, $notLike, $notILike, '..'/$between, '!..'/$notBetween, '&&'/$overlap, '@>'/$contains, '<@'/$contained` * Possible options are: `$ne, $in, $not, $notIn, $gte, $gt, $lte, $lt, $like, $ilike/$iLike, $notLike, $notILike, $regexp, $notRegexp, '..'/$between, '!..'/$notBetween, '&&'/$overlap, '@>'/$contains, '<@'/$contained`
* *
* __Queries using OR__ * __Queries using OR__
* ```js * ```js
......
'use strict';
const chai = require('chai'),
Sequelize = require('../../../../index'),
Promise = Sequelize.Promise,
expect = chai.expect,
Support = require(__dirname + '/../../support'),
DataTypes = require(__dirname + '/../../../../lib/data-types'),
dialect = Support.getTestDialect();
describe(Support.getTestDialectTeaser('Model'), () => {
describe('attributes', () => {
describe('operators', () => {
describe('REGEXP', () => {
beforeEach(function() {
const queryInterface = this.sequelize.getQueryInterface();
this.User = this.sequelize.define('user', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
field: 'userId'
},
name: {
type: DataTypes.STRING,
field: 'full_name'
}
}, {
tableName: 'users',
timestamps: false
});
return Promise.all([
queryInterface.createTable('users', {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true
},
full_name: {
type: DataTypes.STRING
}
})
]);
});
if (dialect === 'mysql' || dialect === 'postgres') {
it('should work with a regexp where', function() {
const self = this;
return this.User.create({
name: 'Foobar'
}).then(() => {
return self.User.find({
where: {
name: {
$regexp: '^Foo'
}
}
});
}).then(user => {
expect(user).to.be.ok;
});
});
it('should work with a not regexp where', function() {
const self = this;
return this.User.create({
name: 'Foobar'
}).then(() => {
return self.User.find({
where: {
name: {
$notRegexp: '^Foo'
}
}
});
}).then(user => {
expect(user).to.not.be.ok;
});
});
if (dialect === 'postgres') {
it('should work with a case-insensitive regexp where', function() {
const self = this;
return this.User.create({
name: 'Foobar'
}).then(() => {
return self.User.find({
where: {
name: {
$iRegexp: '^foo'
}
}
});
}).then(user => {
expect(user).to.be.ok;
});
});
it('should work with a case-insensitive not regexp where', function() {
const self = this;
return this.User.create({
name: 'Foobar'
}).then(() => {
return self.User.find({
where: {
name: {
$notIRegexp: '^foo'
}
}
});
}).then(user => {
expect(user).to.not.be.ok;
});
});
}
}
});
});
});
});
...@@ -381,6 +381,16 @@ if (dialect === 'mysql') { ...@@ -381,6 +381,16 @@ if (dialect === 'mysql') {
arguments: ['myTable', {where: {field: {not: 3}}}], arguments: ['myTable', {where: {field: {not: 3}}}],
expectation: 'SELECT * FROM `myTable` WHERE `myTable`.`field` != 3;', expectation: 'SELECT * FROM `myTable` WHERE `myTable`.`field` != 3;',
context: QueryGenerator context: QueryGenerator
}, {
title: 'Regular Expression in where clause',
arguments: ['myTable', {where: {field: {$regexp: '^[h|a|t]'}}}],
expectation: "SELECT * FROM `myTable` WHERE `myTable`.`field` REGEXP '^[h|a|t]';",
context: QueryGenerator
}, {
title: 'Regular Expression negation in where clause',
arguments: ['myTable', {where: {field: {$notRegexp: '^[h|a|t]'}}}],
expectation: "SELECT * FROM `myTable` WHERE `myTable`.`field` NOT REGEXP '^[h|a|t]';",
context: QueryGenerator
} }
], ],
......
...@@ -502,6 +502,26 @@ if (dialect.match(/^postgres/)) { ...@@ -502,6 +502,26 @@ if (dialect.match(/^postgres/)) {
arguments: ['myTable', {where: {field: {not: 3}}}], arguments: ['myTable', {where: {field: {not: 3}}}],
expectation: 'SELECT * FROM myTable WHERE myTable.field != 3;', expectation: 'SELECT * FROM myTable WHERE myTable.field != 3;',
context: {options: {quoteIdentifiers: false}} context: {options: {quoteIdentifiers: false}}
}, {
title: 'Regular Expression in where clause',
arguments: ['myTable', {where: {field: {$regexp: '^[h|a|t]'}}}],
expectation: "SELECT * FROM \"myTable\" WHERE \"myTable\".\"field\" ~ '^[h|a|t]';",
context: QueryGenerator
}, {
title: 'Regular Expression negation in where clause',
arguments: ['myTable', {where: {field: {$notRegexp: '^[h|a|t]'}}}],
expectation: "SELECT * FROM \"myTable\" WHERE \"myTable\".\"field\" !~ '^[h|a|t]';",
context: QueryGenerator
}, {
title: 'Case-insensitive Regular Expression in where clause',
arguments: ['myTable', {where: {field: {$iRegexp: '^[h|a|t]'}}}],
expectation: "SELECT * FROM \"myTable\" WHERE \"myTable\".\"field\" ~* '^[h|a|t]';",
context: QueryGenerator
}, {
title: 'Case-insensitive Regular Expression negation in where clause',
arguments: ['myTable', {where: {field: {$notIRegexp: '^[h|a|t]'}}}],
expectation: "SELECT * FROM \"myTable\" WHERE \"myTable\".\"field\" !~* '^[h|a|t]';",
context: QueryGenerator
} }
], ],
...@@ -615,7 +635,6 @@ if (dialect.match(/^postgres/)) { ...@@ -615,7 +635,6 @@ if (dialect.match(/^postgres/)) {
expectation: "INSERT INTO mySchema.myTable (name) VALUES ('foo'';DROP TABLE mySchema.myTable;');", expectation: "INSERT INTO mySchema.myTable (name) VALUES ('foo'';DROP TABLE mySchema.myTable;');",
context: {options: {quoteIdentifiers: false}} context: {options: {quoteIdentifiers: false}}
} }
], ],
bulkInsertQuery: [ bulkInsertQuery: [
......
...@@ -908,6 +908,78 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -908,6 +908,78 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
}); });
} }
if (current.dialect.supports.REGEXP) {
suite('$regexp', () => {
testsql('username', {
$regexp: '^sw.*r$'
}, {
mysql: "`username` REGEXP '^sw.*r$'",
postgres: '"username" ~ \'^sw.*r$\''
});
});
suite('$regexp', () => {
testsql('newline', {
$regexp: '^new\nline$'
}, {
mysql: "`newline` REGEXP '^new\nline$'",
postgres: '"newline" ~ \'^new\nline$\''
});
});
suite('$notRegexp', () => {
testsql('username', {
$notRegexp: '^sw.*r$'
}, {
mysql: "`username` NOT REGEXP '^sw.*r$'",
postgres: '"username" !~ \'^sw.*r$\''
});
});
suite('$notRegexp', () => {
testsql('newline', {
$notRegexp: '^new\nline$'
}, {
mysql: "`newline` NOT REGEXP '^new\nline$'",
postgres: '"newline" !~ \'^new\nline$\''
});
});
if (current.dialect.name === 'postgres') {
suite('$iRegexp', () => {
testsql('username', {
$iRegexp: '^sw.*r$'
}, {
postgres: '"username" ~* \'^sw.*r$\''
});
});
suite('$iRegexp', () => {
testsql('newline', {
$iRegexp: '^new\nline$'
}, {
postgres: '"newline" ~* \'^new\nline$\''
});
});
suite('$notIRegexp', () => {
testsql('username', {
$notIRegexp: '^sw.*r$'
}, {
postgres: '"username" !~* \'^sw.*r$\''
});
});
suite('$notIRegexp', () => {
testsql('newline', {
$notIRegexp: '^new\nline$'
}, {
postgres: '"newline" !~* \'^new\nline$\''
});
});
}
}
suite('fn', () => { suite('fn', () => {
test('{name: this.sequelize.fn(\'LOWER\', \'DERP\')}', function() { test('{name: this.sequelize.fn(\'LOWER\', \'DERP\')}', function() {
expectsql(sql.whereQuery({name: this.sequelize.fn('LOWER', 'DERP')}), { expectsql(sql.whereQuery({name: this.sequelize.fn('LOWER', 'DERP')}), {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!