Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,24 @@
"test": "env CRDB_VERSION=$npm_config_crdb_version mocha --check-leaks --colors -t 300000 --reporter spec \"tests/*_test.js\""
},
"dependencies": {
"lodash": "^4.17.20",
"lodash": "^4.17.21",
"p-settle": "^4.1.1",
"pg": "^8.4.1",
"semver": "^7.3.2"
"pg": "^8.21.0",
"semver": "^7.6.2"
},
"devDependencies": {
"assert": "^2.0.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"chai-datetime": "^1.7.0",
"assert": "^2.1.0",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.2",
"chai-datetime": "^1.8.0",
"cls-hooked": "^4.2.2",
"delay": "^5.0.0",
"mocha": "^8.2.0",
"mocha": "^10.4.0",
"p-timeout": "^4.1.0",
"prettier": "2.2.1",
"sequelize": "^6.13.0",
"sinon": "^9.2.4",
"sinon-chai": "^3.5.0"
"prettier": "^2.8.8",
"sequelize": "^6.37.8",
"sinon": "^17.0.1",
"sinon-chai": "^3.7.0"
},
"peerDependencies": {
"sequelize": "5 - 6"
Expand Down
157 changes: 136 additions & 21 deletions source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@

'use strict';

// Intercept require calls to load 'sequelize' from the parent application context.
// This prevents multiple conflicting instances of Sequelize when developing/linking.
const Module = require('module');
const originalRequire = Module.prototype.require;
Module.prototype.require = function (id) {
if (
id.startsWith('sequelize') &&
this.filename.includes('sequelize-cockroachdb')
) {
try {
const parentPaths = module.parent ? module.parent.paths : [];
const resolved = require.resolve(id, {
paths: [process.cwd(), ...parentPaths]
});
return originalRequire.call(this, resolved);
} catch (e) {
// Fallback to original require if resolution fails
}
}
return originalRequire.call(this, id);
};

// Ensure the user did not forget to install Sequelize.
try {
require('sequelize');
Expand All @@ -28,18 +50,18 @@ const { Sequelize, DataTypes, Model } = require('sequelize');
const QueryGenerator = require('sequelize/lib/dialects/postgres/query-generator');

// Ensure Sequelize version compatibility.
const version_helper = require ('./version_helper.js')
const version_helper = require('./version_helper.js');
const semver = require('semver');

const sequelizeVersion = version_helper.GetSequelizeVersion()
const sequelizeVersion = version_helper.GetSequelizeVersion();

if (semver.satisfies(sequelizeVersion, '<=4')) {
throw new Error(
`Sequelize versions 4 and below are not supported by sequelize-cockroachdb. Detected version is ${sequelizeVersion}.`
);
}

require('./telemetry.js')
require('./telemetry.js');

//// [1] Override the `upsert` query method from Sequelize v5 to make it work with CockroachDB
if (semver.satisfies(sequelizeVersion, '5.x')) {
Expand Down Expand Up @@ -81,17 +103,16 @@ PostgresDialect.prototype.supports.lockKey = false;
// invalid integers.
intType.prototype.escape = false;

intType.prototype.$stringify = intType.prototype._stringify = function stringify(
value
) {
var rep = String(value);
if (!/^[-+]?[0-9]+$/.test(rep)) {
throw new Sequelize.ValidationError(
util.format('%j is not a valid integer', value)
);
}
return rep;
};
intType.prototype.$stringify = intType.prototype._stringify =
function stringify(value) {
var rep = String(value);
if (!/^[-+]?[0-9]+$/.test(rep)) {
throw new Sequelize.ValidationError(
util.format('%j is not a valid integer', value)
);
}
return rep;
};
});

// [4] Fix int to string conversion
Expand All @@ -103,14 +124,76 @@ const {
ConnectionManager.prototype.__loadDialectModule =
ConnectionManager.prototype._loadDialectModule;
ConnectionManager.prototype._loadDialectModule = function (...args) {
const pg = this.__loadDialectModule(...args);
pg.types.setTypeParser(20, function (val) {
if (val > Number.MAX_SAFE_INTEGER) return String(val);
else return parseInt(val, 10);
});
let pg = this.__loadDialectModule(...args);
if (args[0] === 'pg' && (!pg || !pg.types)) {
pg = require('pg');
}
if (pg && pg.types) {
pg.types.setTypeParser(20, function (val) {
if (val > Number.MAX_SAFE_INTEGER) return String(val);
else return parseInt(val, 10);
});
}
return pg;
};

const PostgresConnectionManager = require('sequelize/lib/dialects/postgres/connection-manager');
const originalConnect = PostgresConnectionManager.prototype.connect;
PostgresConnectionManager.prototype.connect = async function (config) {
const connection = await originalConnect.call(this, config);
this.sequelize.isCockroachDB = true; // Default to true for CockroachDB package
try {
const result = await connection.query('SELECT version() AS version');
const versionStr = result.rows && result.rows[0] && result.rows[0].version;
if (versionStr) {
this.sequelize.isCockroachDB = versionStr.includes('CockroachDB');
}
} catch (err) {
// ignore
}

// Dynamically configure feature support flags if the target database is standard PostgreSQL
if (!this.sequelize.isCockroachDB) {
this.sequelize.dialect.supports.EXCEPTION = true;
this.sequelize.dialect.supports.lockOuterJoinFailure = true;
this.sequelize.dialect.supports.skipLocked = true;
this.sequelize.dialect.supports.lockKey = true;
}
return connection;
};

QueryGenerator.prototype.pgEnum = function (
tableName,
attr,
dataType,
options
) {
const enumName = this.pgEnumName(tableName, attr, options);
let values;

if (dataType.values) {
values = `ENUM(${dataType.values
.map(value => this.escape(value))
.join(', ')})`;
} else {
values = dataType.toString().match(/^ENUM\(.+\)/)[0];
}

let sql;
if (this.sequelize.isCockroachDB) {
sql = `CREATE TYPE IF NOT EXISTS ${enumName} AS ${values};`;
} else {
sql = `DO ${this.escape(
`BEGIN CREATE TYPE ${enumName} AS ${values}; EXCEPTION WHEN duplicate_object THEN null; END`
)};`;
}

if (!!options && options.force === true) {
sql = this.pgEnumDrop(tableName, attr) + sql;
}
return sql;
};

QueryGenerator.prototype.__describeTableQuery =
QueryGenerator.prototype.describeTableQuery;
QueryGenerator.prototype.describeTableQuery = function (...args) {
Expand All @@ -128,7 +211,10 @@ QueryGenerator.prototype.describeTableQuery = function (...args) {
// Change unimplemented column
.replace('relid', 'oid')
// Aggregate enums in sort order
.replace('array_agg(e.enumlabel)', 'array_agg(e.enumlabel ORDER BY e.enumsortorder ASC)')
.replace(
'array_agg(e.enumlabel)',
'array_agg(e.enumlabel ORDER BY e.enumsortorder ASC)'
)
);
};

Expand Down Expand Up @@ -301,7 +387,36 @@ Model.findOrCreate = async function findOrCreate(options) {
// Got to explicitly cast it is a GEOGRAPHY type.
DataTypes.postgres.GEOGRAPHY.prototype.bindParam = (value, options) => {
return `ST_GeomFromGeoJSON(${options.bindParam(value)}::json)::geography`;
}
};

// [8] Handle SPATIAL indexes by mapping them to GIST indexes
const originalAddIndexQuery = QueryGenerator.prototype.addIndexQuery;
QueryGenerator.prototype.addIndexQuery = function (
tableName,
attributes,
options,
rawTablename
) {
let opts = options;
if (!Array.isArray(attributes)) {
opts = attributes;
}
if (
opts &&
typeof opts.type === 'string' &&
opts.type.toUpperCase() === 'SPATIAL'
) {
opts.using = 'gist';
delete opts.type;
}
return originalAddIndexQuery.call(
this,
tableName,
attributes,
options,
rawTablename
);
};

//// Done!

Expand Down
5 changes: 2 additions & 3 deletions source/patch-upsert-v5.js
Original file line number Diff line number Diff line change
Expand Up @@ -609,9 +609,8 @@ const postgresQueryPatches = {
attribute.fieldName === key || attribute.field === key
);

this.instance.dataValues[
(attr && attr.fieldName) || key
] = record;
this.instance.dataValues[(attr && attr.fieldName) || key] =
record;
}
}
}
Expand Down
64 changes: 40 additions & 24 deletions source/telemetry.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
const { Sequelize, QueryTypes } = require('sequelize');

const version_helper = require('./version_helper.js')
const version_helper = require('./version_helper.js');

//// Log telemetry for Sequelize ORM.
Sequelize.addHook('afterInit', async (connection) => {
try {
if (connection.options.dialectOptions) {
var telemetryDisabled = connection.options.dialectOptions.cockroachdbTelemetryDisabled
if (telemetryDisabled) {
return
}
}

// crdb_internal.increment_feature_counter is only available on 21.1 and above.
if (!(await version_helper.IsCockroachVersion21_1Plus(connection))) {
return
}
var sequelizeVersion = version_helper.GetSequelizeVersion()
var sequelizeVersionSeries = version_helper.GetVersionSeries(sequelizeVersion.version)
var sequelizeVersionStr = (sequelizeVersionSeries===null)?sequelizeVersion:sequelizeVersionSeries
await connection.query(`SELECT crdb_internal.increment_feature_counter(concat('Sequelize ', :SequelizeVersionString))`,
{ replacements: { SequelizeVersionString: sequelizeVersionStr }, type: QueryTypes.SELECT })
Sequelize.addHook('afterInit', async connection => {
try {
if (connection.options.dialectOptions) {
var telemetryDisabled =
connection.options.dialectOptions.cockroachdbTelemetryDisabled;
if (telemetryDisabled) {
return;
}
}

var adapterVersion = version_helper.GetAdapterVersion()
await connection.query(`SELECT crdb_internal.increment_feature_counter(concat('sequelize-cockroachdb ', :AdapterVersion))`,
{ replacements: { AdapterVersion: adapterVersion.version }, type: QueryTypes.SELECT })
} catch (error) {
console.info("Could not record telemetry.\n" + error)
// crdb_internal.increment_feature_counter is only available on 21.1 and above.
if (!(await version_helper.IsCockroachVersion21_1Plus(connection))) {
return;
}
var sequelizeVersion = version_helper.GetSequelizeVersion();
var sequelizeVersionSeries = version_helper.GetVersionSeries(
sequelizeVersion.version
);
var sequelizeVersionStr =
sequelizeVersionSeries === null
? sequelizeVersion
: sequelizeVersionSeries;
await connection.query(
`SELECT crdb_internal.increment_feature_counter(concat('Sequelize ', :SequelizeVersionString))`,
{
replacements: { SequelizeVersionString: sequelizeVersionStr },
type: QueryTypes.SELECT
}
);

var adapterVersion = version_helper.GetAdapterVersion();
await connection.query(
`SELECT crdb_internal.increment_feature_counter(concat('sequelize-cockroachdb ', :AdapterVersion))`,
{
replacements: { AdapterVersion: adapterVersion.version },
type: QueryTypes.SELECT
}
);
} catch (error) {
console.info('Could not record telemetry.\n' + error);
}
});
43 changes: 23 additions & 20 deletions source/version_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,40 @@ const { version, release } = require('sequelize/package.json');
const { QueryTypes } = require('sequelize');

module.exports = {
GetSequelizeVersion: function() {
// In v4 and v5 package.json files, have a property 'release: { branch: 'v5' }'
// but in v6 it has 'release: { branches: ['v6'] }'
var branchVersion = release.branches ? release.branches[0] : release.branch;
// When executing the tests on Github Actions the version it gets from sequelize is from the repository which has a development version '0.0.0-development'
// in that case we fallback to a branch version
return semver.coerce(version === '0.0.0-development' ? branchVersion : version);
GetSequelizeVersion: function () {
// In v4 and v5 package.json files, have a property 'release: { branch: 'v5' }'
// but in v6 it has 'release: { branches: ['v6'] }'
var branchVersion = release.branches ? release.branches[0] : release.branch;
// When executing the tests on Github Actions the version it gets from sequelize is from the repository which has a development version '0.0.0-development'
// in that case we fallback to a branch version
return semver.coerce(
version === '0.0.0-development' ? branchVersion : version
);
},
GetAdapterVersion: function() {
GetAdapterVersion: function () {
const pkgVersion = require('../package.json').version;
return semver.coerce(pkgVersion);
},
IsCockroachVersion21_1Plus: async function(connection) {
const versionRow = await connection.query("SELECT version() AS version", { type: QueryTypes.SELECT });
const cockroachDBVersion = versionRow[0]["version"]
IsCockroachVersion21_1Plus: async function (connection) {
const versionRow = await connection.query('SELECT version() AS version', {
type: QueryTypes.SELECT
});
const cockroachDBVersion = versionRow[0]['version'];

return semver.gte(semver.coerce(cockroachDBVersion), "21.1.0")
return semver.gte(semver.coerce(cockroachDBVersion), '21.1.0');
},
GetCockroachDBVersionFromEnvConfig: function() {
const crdbVersion = process.env['CRDB_VERSION']
return semver.coerce(crdbVersion)
GetCockroachDBVersionFromEnvConfig: function () {
const crdbVersion = process.env['CRDB_VERSION'];
return semver.coerce(crdbVersion);
},
GetVersionSeries: function(versionStr) {
GetVersionSeries: function (versionStr) {
// Get the version series from a version string.
// E.g. "6.0.1" is of series "6.0".
const regExp=/(\d+\.\d+)(\.|$)/mg
const regExp = /(\d+\.\d+)(\.|$)/gm;
let match = regExp.exec(versionStr);
if (match === null || match.length < 3) {
return null
return null;
}
return match[1]
return match[1];
}
};

Loading