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

Commit cf6c05e0 by Justin Kalland Committed by Sushant

feat(postgres): CITEXT datatype (#10024)

1 parent fa587acc
...@@ -77,6 +77,7 @@ Create the following extensions in the test database: ...@@ -77,6 +77,7 @@ Create the following extensions in the test database:
CREATE EXTENSION postgis; CREATE EXTENSION postgis;
CREATE EXTENSION hstore; CREATE EXTENSION hstore;
CREATE EXTENSION btree_gist; CREATE EXTENSION btree_gist;
CREATE EXTENSION citext;
``` ```
#### 3.b Docker #### 3.b Docker
......
...@@ -120,6 +120,7 @@ Sequelize.STRING(1234) // VARCHAR(1234) ...@@ -120,6 +120,7 @@ Sequelize.STRING(1234) // VARCHAR(1234)
Sequelize.STRING.BINARY // VARCHAR BINARY Sequelize.STRING.BINARY // VARCHAR BINARY
Sequelize.TEXT // TEXT Sequelize.TEXT // TEXT
Sequelize.TEXT('tiny') // TINYTEXT Sequelize.TEXT('tiny') // TINYTEXT
Sequelize.CITEXT // CITEXT PostgreSQL only.
Sequelize.INTEGER // INTEGER Sequelize.INTEGER // INTEGER
Sequelize.BIGINT // BIGINT Sequelize.BIGINT // BIGINT
......
...@@ -141,6 +141,31 @@ TEXT.prototype.validate = function validate(value) { ...@@ -141,6 +141,31 @@ TEXT.prototype.validate = function validate(value) {
}; };
/** /**
* An unlimited length case-insensitive text column.
* Original case is preserved but acts case-insensitive when comparing values (such as when finding or unique constraints).
* Only available in Postgres.
*
* @namespace DataTypes.CITEXT
*/
function CITEXT() {
if (!(this instanceof CITEXT)) return new CITEXT();
}
inherits(CITEXT, ABSTRACT);
CITEXT.prototype.key = CITEXT.key = 'CITEXT';
CITEXT.prototype.toSql = function toSql() {
return 'CITEXT';
};
CITEXT.prototype.validate = function validate(value) {
if (!_.isString(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid string', value));
}
return true;
};
/**
* Base number type which is used to build other types * Base number type which is used to build other types
* *
* @param {Object} options type options * @param {Object} options type options
...@@ -1240,7 +1265,8 @@ const DataTypes = module.exports = { ...@@ -1240,7 +1265,8 @@ const DataTypes = module.exports = {
GEOGRAPHY, GEOGRAPHY,
CIDR, CIDR,
INET, INET,
MACADDR MACADDR,
CITEXT
}; };
_.each(DataTypes, dataType => { _.each(DataTypes, dataType => {
......
...@@ -167,7 +167,8 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -167,7 +167,8 @@ class ConnectionManager extends AbstractConnectionManager {
dataTypes.GEOGRAPHY.types.postgres.oids.length === 0 && dataTypes.GEOGRAPHY.types.postgres.oids.length === 0 &&
dataTypes.GEOMETRY.types.postgres.oids.length === 0 && dataTypes.GEOMETRY.types.postgres.oids.length === 0 &&
dataTypes.HSTORE.types.postgres.oids.length === 0 && dataTypes.HSTORE.types.postgres.oids.length === 0 &&
dataTypes.ENUM.types.postgres.oids.length === 0 dataTypes.ENUM.types.postgres.oids.length === 0 &&
dataTypes.CITEXT.types.postgres.oids.length === 0
) { ) {
return this._refreshDynamicOIDs(connection); return this._refreshDynamicOIDs(connection);
} }
...@@ -201,9 +202,9 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -201,9 +202,9 @@ class ConnectionManager extends AbstractConnectionManager {
} }
// Refresh dynamic OIDs for some types // Refresh dynamic OIDs for some types
// These include, Geometry / HStore / Enum // These include, Geometry / HStore / Enum / Citext
return (connection || this.sequelize).query( return (connection || this.sequelize).query(
"SELECT typname, typtype, oid, typarray FROM pg_type WHERE (typtype = 'b' AND typname IN ('hstore', 'geometry', 'geography')) OR (typtype = 'e')" "SELECT typname, typtype, oid, typarray FROM pg_type WHERE (typtype = 'b' AND typname IN ('hstore', 'geometry', 'geography', 'citext')) OR (typtype = 'e')"
).then(results => { ).then(results => {
let result = Array.isArray(results) ? results.pop() : results; let result = Array.isArray(results) ? results.pop() : results;
...@@ -218,10 +219,11 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -218,10 +219,11 @@ class ConnectionManager extends AbstractConnectionManager {
// Reset OID mapping for dynamic type // Reset OID mapping for dynamic type
[ [
dataTypes.postgres.GEOMETRY, dataTypes.GEOMETRY,
dataTypes.postgres.HSTORE, dataTypes.HSTORE,
dataTypes.postgres.GEOGRAPHY, dataTypes.GEOGRAPHY,
dataTypes.postgres.ENUM dataTypes.ENUM,
dataTypes.CITEXT
].forEach(type => { ].forEach(type => {
type.types.postgres.oids = []; type.types.postgres.oids = [];
type.types.postgres.array_oids = []; type.types.postgres.array_oids = [];
...@@ -231,13 +233,15 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -231,13 +233,15 @@ class ConnectionManager extends AbstractConnectionManager {
let type; let type;
if (row.typname === 'geometry') { if (row.typname === 'geometry') {
type = dataTypes.postgres.GEOMETRY; type = dataTypes.GEOMETRY;
} else if (row.typname === 'hstore') { } else if (row.typname === 'hstore') {
type = dataTypes.postgres.HSTORE; type = dataTypes.HSTORE;
} else if (row.typname === 'geography') { } else if (row.typname === 'geography') {
type = dataTypes.postgres.GEOGRAPHY; type = dataTypes.GEOGRAPHY;
} else if (row.typname === 'citext') {
type = dataTypes.CITEXT;
} else if (row.typtype === 'e') { } else if (row.typtype === 'e') {
type = dataTypes.postgres.ENUM; type = dataTypes.ENUM;
} }
type.types.postgres.oids.push(row.oid); type.types.postgres.oids.push(row.oid);
......
...@@ -168,6 +168,11 @@ module.exports = BaseTypes => { ...@@ -168,6 +168,11 @@ module.exports = BaseTypes => {
array_oids: [1009] array_oids: [1009]
}; };
BaseTypes.CITEXT.types.postgres = {
oids: [],
array_oids: []
};
function CHAR(length, binary) { function CHAR(length, binary) {
if (!(this instanceof CHAR)) return new CHAR(length, binary); if (!(this instanceof CHAR)) return new CHAR(length, binary);
BaseTypes.CHAR.apply(this, arguments); BaseTypes.CHAR.apply(this, arguments);
......
...@@ -282,6 +282,16 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { ...@@ -282,6 +282,16 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
} }
}); });
it('calls parse and stringify for CITEXT', () => {
const Type = new Sequelize.CITEXT();
if (dialect === 'postgres') {
return testSuccess(Type, 'foobar');
} else {
testFailure(Type);
}
});
it('calls parse and stringify for MACADDR', () => { it('calls parse and stringify for MACADDR', () => {
const Type = new Sequelize.MACADDR(); const Type = new Sequelize.MACADDR();
......
...@@ -32,4 +32,35 @@ if (dialect.match(/^postgres/)) { ...@@ -32,4 +32,35 @@ if (dialect.match(/^postgres/)) {
return checkTimezoneParsing(this.sequelize.options); return checkTimezoneParsing(this.sequelize.options);
}); });
}); });
describe('Dynamic OIDs', () => {
const dynamicTypesToCheck = [
DataTypes.GEOMETRY,
DataTypes.HSTORE,
DataTypes.GEOGRAPHY,
DataTypes.ENUM,
DataTypes.CITEXT
];
it('should fetch dynamic oids from the database', () => {
dynamicTypesToCheck.forEach(type => {
type.types.postgres.oids = [];
type.types.postgres.array_oids = [];
});
// Model is needed to test the ENUM dynamic OID
const User = Support.sequelize.define('User', {
perms: DataTypes.ENUM([
'foo', 'bar'
])
});
return User.sync({force: true}).then(() => {
dynamicTypesToCheck.forEach(type => {
expect(type.types.postgres.oids, `DataType.${type.key}`).to.not.be.empty;
expect(type.types.postgres.array_oids, `DataType.${type.key}`).to.not.be.empty;
});
});
});
});
} }
...@@ -998,6 +998,22 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -998,6 +998,22 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
}); });
if (dialect === 'postgres') {
it("doesn't allow case-insensitive duplicated records using CITEXT", function () {
const User = this.sequelize.define('UserWithUniqueCITEXT', {
username: {type: Sequelize.CITEXT, unique: true}
});
return User.sync({force: true}).then(() => {
return User.create({username: 'foo'});
}).then(() => {
return User.create({username: 'fOO'});
}).catch(Sequelize.UniqueConstraintError, 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", function () { it("doesn't allow duplicated records with unique function based indexes", function () {
const User = this.sequelize.define('UserWithUniqueUsernameFunctionIndex', { const User = this.sequelize.define('UserWithUniqueUsernameFunctionIndex', {
......
...@@ -474,6 +474,31 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -474,6 +474,31 @@ describe(Support.getTestDialectTeaser('Model'), () => {
expect(users[1].intVal).to.equal(10); expect(users[1].intVal).to.equal(10);
}); });
}); });
if (dialect === 'postgres') {
it('should be able to find multiple users with case-insensitive on CITEXT type', function() {
const User = this.sequelize.define('UsersWithCaseInsensitiveName', {
username: Sequelize.CITEXT
});
return User.sync({force: true}).then(() => {
return User.bulkCreate([
{username: 'lowercase'},
{username: 'UPPERCASE'},
{username: 'MIXEDcase'}
]);
}).then(() => {
return User.findAll({
where: { username: ['LOWERCASE', 'uppercase', 'mixedCase']},
order: [['id', 'ASC']]
});
}).then(users => {
expect(users[0].username).to.equal('lowercase');
expect(users[1].username).to.equal('UPPERCASE');
expect(users[2].username).to.equal('MIXEDcase');
});
});
}
}); });
describe('eager loading', () => { describe('eager loading', () => {
......
...@@ -287,6 +287,23 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -287,6 +287,23 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
}); });
}); });
if (dialect === 'postgres') {
it('should allow case-insensitive find on CITEXT type', function() {
const User = this.sequelize.define('UserWithCaseInsensitiveName', {
username: Sequelize.CITEXT
});
return User.sync({force: true}).then(() => {
return User.create({username: 'longUserNAME'});
}).then(() => {
return User.findOne({where: {username: 'LONGusername'}});
}).then(user => {
expect(user).to.exist;
expect(user.username).to.equal('longUserNAME');
});
});
}
}); });
describe('eager loading', () => { describe('eager loading', () => {
......
...@@ -8,7 +8,8 @@ const Support = require('../support'), ...@@ -8,7 +8,8 @@ const Support = require('../support'),
uuid = require('uuid'), uuid = require('uuid'),
expectsql = Support.expectsql, expectsql = Support.expectsql,
current = Support.sequelize, current = Support.sequelize,
expect = chai.expect; expect = chai.expect,
dialect = Support.getTestDialect();
// Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation // Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation
...@@ -1396,6 +1397,12 @@ suite(Support.getTestDialectTeaser('SQL'), () => { ...@@ -1396,6 +1397,12 @@ suite(Support.getTestDialectTeaser('SQL'), () => {
}); });
} }
if (dialect === 'postgres') {
testsql('ARRAY(CITEXT)', DataTypes.ARRAY(DataTypes.CITEXT), {
postgres: 'CITEXT[]'
});
}
suite('validate', () => { suite('validate', () => {
test('should throw an error if `value` is invalid', () => { test('should throw an error if `value` is invalid', () => {
const type = DataTypes.ARRAY(); const type = DataTypes.ARRAY();
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!