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

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
DataTypes.TEXT // TEXT
DataTypes.TEXT('tiny') // TINYTEXT
DataTypes.CITEXT // CITEXT PostgreSQL and SQLite only.
DataTypes.TSVECTOR // TSVECTOR PostgreSQL only.
```
### Boolean
......
......@@ -261,6 +261,7 @@ Post.findAll({
[Op.notIRegexp]: '^[h|a|t]', // !~* '^[h|a|t]' (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:
[Op.like]: { [Op.any]: ['cat', 'hat'] } // LIKE ANY ARRAY['cat', 'hat']
......
......@@ -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:
* ```js
* sequelize.define('model', {
......@@ -1023,7 +1038,8 @@ const DataTypes = module.exports = {
CIDR,
INET,
MACADDR,
CITEXT
CITEXT,
TSVECTOR
};
_.each(DataTypes, (dataType, name) => {
......
......@@ -42,7 +42,8 @@ const OperatorHelpers = {
[Op.and]: ' AND ',
[Op.or]: ' OR ',
[Op.col]: 'COL',
[Op.placeholder]: '$$PLACEHOLDER$$'
[Op.placeholder]: '$$PLACEHOLDER$$',
[Op.match]: '@@'
},
OperatorsAliasMap: {},
......
......@@ -36,6 +36,7 @@ module.exports = BaseTypes => {
BaseTypes.CIDR.types.postgres = ['cidr'];
BaseTypes.INET.types.postgres = ['inet'];
BaseTypes.MACADDR.types.postgres = ['macaddr'];
BaseTypes.TSVECTOR.types.postgres = ['tsvector'];
BaseTypes.JSON.types.postgres = ['json'];
BaseTypes.JSONB.types.postgres = ['jsonb'];
BaseTypes.TIME.types.postgres = ['time'];
......
......@@ -57,6 +57,7 @@ PostgresDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototy
JSON: true,
JSONB: true,
HSTORE: true,
TSVECTOR: true,
deferrableConstraints: true,
searchPath: true
});
......
......@@ -84,7 +84,8 @@ const Op = {
values: Symbol.for('values'),
col: Symbol.for('col'),
placeholder: Symbol.for('placeholder'),
join: Symbol.for('join')
join: Symbol.for('join'),
match: Symbol.for('match')
};
module.exports = Op;
......@@ -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 () => {
const Type = new Sequelize.ENUM('hat', 'cat');
......
......@@ -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) {
it("doesn't allow duplicated records with unique function based indexes", async function() {
const User = this.sequelize.define('UserWithUniqueUsernameFunctionIndex', {
......
......@@ -273,6 +273,22 @@ describe(Support.getTestDialectTeaser('Model'), () => {
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', () => {
......
......@@ -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', () => {
it('{name: this.sequelize.fn(\'LOWER\', \'DERP\')}', function() {
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!