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

Commit e45df29d by Pedro Augusto de Paula Barbosa Committed by GitHub

feat(postgres): add TSVECTOR datatype and `@@` operator (#12955)

Co-authored-by: Jano Kacer <jan@vestberry.com>
Co-authored-by: Sébastien BRAMILLE <2752200+oktapodia@users.noreply.github.com>
1 parent 9013836f
...@@ -270,6 +270,7 @@ DataTypes.STRING.BINARY // VARCHAR BINARY ...@@ -270,6 +270,7 @@ DataTypes.STRING.BINARY // VARCHAR BINARY
DataTypes.TEXT // TEXT DataTypes.TEXT // TEXT
DataTypes.TEXT('tiny') // TINYTEXT DataTypes.TEXT('tiny') // TINYTEXT
DataTypes.CITEXT // CITEXT PostgreSQL and SQLite only. DataTypes.CITEXT // CITEXT PostgreSQL and SQLite only.
DataTypes.TSVECTOR // TSVECTOR PostgreSQL only.
``` ```
### Boolean ### Boolean
......
...@@ -261,6 +261,7 @@ Post.findAll({ ...@@ -261,6 +261,7 @@ Post.findAll({
[Op.notIRegexp]: '^[h|a|t]', // !~* '^[h|a|t]' (PG only) [Op.notIRegexp]: '^[h|a|t]', // !~* '^[h|a|t]' (PG only)
[Op.any]: [2, 3], // ANY ARRAY[2, 3]::INTEGER (PG only) [Op.any]: [2, 3], // ANY ARRAY[2, 3]::INTEGER (PG only)
[Op.match]: Sequelize.fn('to_tsquery', 'fat & rat') // match text search for strings 'fat' and 'rat' (PG only)
// In Postgres, Op.like/Op.iLike/Op.notLike can be combined to Op.any: // In Postgres, Op.like/Op.iLike/Op.notLike can be combined to Op.any:
[Op.like]: { [Op.any]: ['cat', 'hat'] } // LIKE ANY ARRAY['cat', 'hat'] [Op.like]: { [Op.any]: ['cat', 'hat'] } // LIKE ANY ARRAY['cat', 'hat']
......
...@@ -941,6 +941,21 @@ class MACADDR extends ABSTRACT { ...@@ -941,6 +941,21 @@ class MACADDR extends ABSTRACT {
} }
/** /**
* The TSVECTOR type stores text search vectors.
*
* Only available for Postgres
*
*/
class TSVECTOR extends ABSTRACT {
validate(value) {
if (typeof value !== 'string') {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid string', value));
}
return true;
}
}
/**
* A convenience class holding commonly used data types. The data types are used when defining a new model using `Sequelize.define`, like this: * A convenience class holding commonly used data types. The data types are used when defining a new model using `Sequelize.define`, like this:
* ```js * ```js
* sequelize.define('model', { * sequelize.define('model', {
...@@ -1023,7 +1038,8 @@ const DataTypes = module.exports = { ...@@ -1023,7 +1038,8 @@ const DataTypes = module.exports = {
CIDR, CIDR,
INET, INET,
MACADDR, MACADDR,
CITEXT CITEXT,
TSVECTOR
}; };
_.each(DataTypes, (dataType, name) => { _.each(DataTypes, (dataType, name) => {
......
...@@ -42,7 +42,8 @@ const OperatorHelpers = { ...@@ -42,7 +42,8 @@ const OperatorHelpers = {
[Op.and]: ' AND ', [Op.and]: ' AND ',
[Op.or]: ' OR ', [Op.or]: ' OR ',
[Op.col]: 'COL', [Op.col]: 'COL',
[Op.placeholder]: '$$PLACEHOLDER$$' [Op.placeholder]: '$$PLACEHOLDER$$',
[Op.match]: '@@'
}, },
OperatorsAliasMap: {}, OperatorsAliasMap: {},
......
...@@ -36,6 +36,7 @@ module.exports = BaseTypes => { ...@@ -36,6 +36,7 @@ module.exports = BaseTypes => {
BaseTypes.CIDR.types.postgres = ['cidr']; BaseTypes.CIDR.types.postgres = ['cidr'];
BaseTypes.INET.types.postgres = ['inet']; BaseTypes.INET.types.postgres = ['inet'];
BaseTypes.MACADDR.types.postgres = ['macaddr']; BaseTypes.MACADDR.types.postgres = ['macaddr'];
BaseTypes.TSVECTOR.types.postgres = ['tsvector'];
BaseTypes.JSON.types.postgres = ['json']; BaseTypes.JSON.types.postgres = ['json'];
BaseTypes.JSONB.types.postgres = ['jsonb']; BaseTypes.JSONB.types.postgres = ['jsonb'];
BaseTypes.TIME.types.postgres = ['time']; BaseTypes.TIME.types.postgres = ['time'];
......
...@@ -57,6 +57,7 @@ PostgresDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototy ...@@ -57,6 +57,7 @@ PostgresDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototy
JSON: true, JSON: true,
JSONB: true, JSONB: true,
HSTORE: true, HSTORE: true,
TSVECTOR: true,
deferrableConstraints: true, deferrableConstraints: true,
searchPath: true searchPath: true
}); });
......
...@@ -84,7 +84,8 @@ const Op = { ...@@ -84,7 +84,8 @@ const Op = {
values: Symbol.for('values'), values: Symbol.for('values'),
col: Symbol.for('col'), col: Symbol.for('col'),
placeholder: Symbol.for('placeholder'), placeholder: Symbol.for('placeholder'),
join: Symbol.for('join') join: Symbol.for('join'),
match: Symbol.for('match')
}; };
module.exports = Op; module.exports = Op;
...@@ -362,6 +362,18 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { ...@@ -362,6 +362,18 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
}); });
if (current.dialect.supports.TSVECTOR) {
it('calls parse and stringify for TSVECTOR', async () => {
const Type = new Sequelize.TSVECTOR();
if (['postgres'].includes(dialect)) {
await testSuccess(Type, 'swagger');
} else {
testFailure(Type);
}
});
}
it('calls parse and stringify for ENUM', async () => { it('calls parse and stringify for ENUM', async () => {
const Type = new Sequelize.ENUM('hat', 'cat'); const Type = new Sequelize.ENUM('hat', 'cat');
......
...@@ -1007,6 +1007,31 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -1007,6 +1007,31 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
} }
if (dialect === 'postgres') {
it('allows the creation of a TSVECTOR field', async function() {
const User = this.sequelize.define('UserWithTSVECTOR', {
name: Sequelize.TSVECTOR
});
await User.sync({ force: true });
await User.create({ name: 'John Doe' });
});
it('TSVECTOR only allow string', async function() {
const User = this.sequelize.define('UserWithTSVECTOR', {
username: { type: Sequelize.TSVECTOR }
});
try {
await User.sync({ force: true });
await User.create({ username: 42 });
} catch (err) {
if (!(err instanceof Sequelize.ValidationError)) throw err;
expect(err).to.be.ok;
}
});
}
if (current.dialect.supports.index.functionBased) { if (current.dialect.supports.index.functionBased) {
it("doesn't allow duplicated records with unique function based indexes", async function() { it("doesn't allow duplicated records with unique function based indexes", async function() {
const User = this.sequelize.define('UserWithUniqueUsernameFunctionIndex', { const User = this.sequelize.define('UserWithUniqueUsernameFunctionIndex', {
......
...@@ -273,6 +273,22 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -273,6 +273,22 @@ describe(Support.getTestDialectTeaser('Model'), () => {
expect(user.username).to.equal('longUserNAME'); expect(user.username).to.equal('longUserNAME');
}); });
} }
if (dialect === 'postgres') {
it('should allow case-sensitive find on TSVECTOR type', async function() {
const User = this.sequelize.define('UserWithCaseInsensitiveName', {
username: Sequelize.TSVECTOR
});
await User.sync({ force: true });
await User.create({ username: 'longUserNAME' });
const user = await User.findOne({
where: { username: 'longUserNAME' }
});
expect(user).to.exist;
expect(user.username).to.equal("'longUserNAME'");
});
}
}); });
describe('eager loading', () => { describe('eager loading', () => {
......
...@@ -1206,6 +1206,20 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ...@@ -1206,6 +1206,20 @@ describe(Support.getTestDialectTeaser('SQL'), () => {
} }
} }
if (current.dialect.supports.TSVESCTOR) {
describe('Op.match', () => {
testsql(
'username',
{
[Op.match]: Support.sequelize.fn('to_tsvector', 'swagger')
},
{
postgres: "[username] @@ to_tsvector('swagger')"
}
);
});
}
describe('fn', () => { describe('fn', () => {
it('{name: this.sequelize.fn(\'LOWER\', \'DERP\')}', function() { it('{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!