sequelize: Can't change default parser for Postgresql using setTypeParser after version 4.32.3

What are you doing?

I’m trying to change the default parser for decimal type (numeric in postgresql) from string to float using the following code:

var pg = require('pg');
pg.types.setTypeParser(1700, 'text', parseFloat);

This was working until version 4.32.4

What do you expect to happen?

The same behavior prior to version 4.32.4

What is actually happening?

The parser doesn’t change.

Dialect: postgres Dialect version: 7.4.1 Database version: 9.6 Sequelize version: 4.33.4

Note : Your issue may be ignored OR closed by maintainers if it’s not tested against latest version OR does not follow issue template.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 4
  • Comments: 20 (5 by maintainers)

Most upvoted comments

@EyePulp @psotres

I would like to start by thanking you, your help was invaluable.

Having a look it seems we can get the workaround to look a little cleaner by extending the class instead of trying to clone the entire object.

Defining our CustomDecimal as follows alllows us to override the parse function and keep all of the other information associated.

class CustomDecimal extends (Sequelize.DataTypes.DECIMAL as Sequelize.DataTypes.NumberDataTypeConstructor) {
  static parse (value: string): number {
    return parseFloat(value)
  }
}

This enables us to make the hook itself a bit cleaner.

hooks: {
    afterConnect (): Promise<void> | void {
      const dTypes = {
        DECIMAL: CustomDecimal
      };
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (this as any as Sequelize.Sequelize).connectionManager.refreshTypeParser(dTypes)
    }
  }

May be important to mention that I am using TypeScript. Hopefully this helps!

After digging through the code, I was able to get it to work with this

sequelize.Sequelize.DataTypes.postgres.DECIMAL.parse = parseFloat;

Wonder how long this hack will last…

Edit: The old pg.types.setTypeParser method still works for other DataTypes, like DATEONLY (1082). I’m a bit confused on what is going on in the code…

@EyePulp - I’m happy to give it a go - but have no way to verify right now.

Removing Typescript related code gives the following:

class CustomDecimal extends Sequelize.DataTypes.DECIMAL {
  static parse (value) {
    return parseFloat(value)
  }
}

hooks: {
  afterConnect: function () {
    const dTypes = {
      DECIMAL: CustomDecimal
    };
    this.connectionManager.refreshTypeParser(dTypes)
  }
}

Good luck!

@psotres This seemed to rear its head again under v5.x. The final call to

this.connectionManager.refreshTypeParser(types);

would fail – it seems like the duped type { ...this.Sequelize.DataTypes.postgres.DECIMAL } wasn’t pulling all the properties – .DECIMAL is a Function with additional properties hung on it, such as key, types, parse etc…

Also under v5 there doesn’t seem to be a this.Sequelize in scope when afterConnect runs setParsers() – there is a this.sequelize (note the lower case), but it doesn’t have the .DataTypes property. I instead used the Sequelize from my upper scoped initial import of the library.

I couldn’t seem to clone it in a one-liner and keep those properties, so I wrote a cloning function to grab some things both manually and via Object.getOwnPropertyNames()

This is starting to feel pretty hacky, but tests are passing. 🤷‍♂

const Sequelize = require('sequelize');


// other sequelize setup code



hooks: {
    afterConnect: [
        // sequelize defaults some numeric fields to strings to avoid overflows in parsing,
        // but our numeric/int values remain small enough that it's (probably) a safe override
        // so we adjust to to use appropriate parsers:
        // https://github.com/sequelize/sequelize/issues/9074#issuecomment-423271834

        function setParsers() {
            const cloneType = function(t, parseFn) {
                const clone = {};
                for (const name of Object.getOwnPropertyNames(t)) {
                    clone[name] = t[name];
                }
                (clone.key = t.key), (clone.types = t.types), (clone.parse = parseFn);

                return clone;
            };

            const dTypes = {
                DECIMAL: cloneType(Sequelize.DataTypes.postgres.DECIMAL, parseFloat),
                INTEGER: cloneType(Sequelize.DataTypes.postgres.INTEGER, v => parseInt(v, 10)),
                BIGINT: cloneType(Sequelize.DataTypes.postgres.BIGINT, v => parseInt(v, 10)),
            };
            this.connectionManager.refreshTypeParser(dTypes);
        },
    ],
},

Happy to use a different approach if a better one exists!

For anyone facing the same problem as @k98kumar, refer to https://sequelize.org/master/manual/hooks.html. It seems like @EyePulp is referring to Permanent Hooks, they can be added this way for example:

new Sequelize(..., {
  hooks: {
    afterConnect() {
      // do stuff
    }
  }
});

Anyway, @EyePulp’s workaround works for me, thanks

I’ve also been digging though the code and problem seems to have been introduced with #9025 and #9026.

The way the parser is stored for future usage in the connectionManager (via a function that will call dataType.parse method when needed instead of storing its current reference) makes adding custom parser logic quite obscure if you don’t clone the dataType object, as a subsequent modification of Sequelize.DataTypes.postgres.*.parse value in another sequelize instance will impact all instances.

My current workaround to solve the problem without having to use the hack provided by @kingjerod (which is a valid solution but it impacts all sequelize instances) is this:

const types = {
  DECIMAL: { ...sequelize.Sequelize.DataTypes.postgres.DECIMAL },
};
types.DECIMAL.parse = parseFloat;
sequelize.connectionManager.refreshTypeParser(types);

Nevertheless, as a getTypeParser(oid) method is already there, I would say the more elegant way to provide the functionality is via a new setTypeParser(oid, parser) method.

Edit: I ended up using an afterConnect hook to configure it because https://github.com/sequelize/sequelize/blob/ee9ec57d2d4f3b59515d9d4745caff447227b81a/lib/dialects/postgres/connection-manager.js#L181 overwrites the parsers a single time after the first connection is made:

hooks: {
  afterConnect: [
    once(function setParsers() {
      const types = {
        DECIMAL: { ...this.Sequelize.DataTypes.postgres.DECIMAL },
      };
      types.DECIMAL.parse = parseFloat;
      this.connectionManager.refreshTypeParser(types);
    }),
  ],
},

Note: it uses once to avoid a refresh every time db is reconnected, but it can be easily done without the dependency.

@EyePulp - I’m happy to give it a go - but have no way to verify right now.

Removing Typescript related code gives the following:

class CustomDecimal extends Sequelize.DataTypes.DECIMAL {
  static parse (value) {
    return parseFloat(value)
  }
}

hooks: {
  afterConnect: function () {
    const dTypes = {
      DECIMAL: CustomDecimal
    };
    this.connectionManager.refreshTypeParser(dTypes)
  }
}

Good luck!

This workaround worked with version 6.33, thank you

@EyePulp @psotres

I would like to start by thanking you, your help was invaluable.

Having a look it seems we can get the workaround to look a little cleaner by extending the class instead of trying to clone the entire object.

Defining our CustomDecimal as follows alllows us to override the parse function and keep all of the other information associated.

class CustomDecimal extends (Sequelize.DataTypes.DECIMAL as Sequelize.DataTypes.NumberDataTypeConstructor) {
  static parse (value: string): number {
    return parseFloat(value)
  }
}

This enables us to make the hook itself a bit cleaner.

hooks: {
    afterConnect (): Promise<void> | void {
      const dTypes = {
        DECIMAL: CustomDecimal
      };
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (this as any as Sequelize.Sequelize).connectionManager.refreshTypeParser(dTypes)
    }
  }

May be important to mention that I am using TypeScript. Hopefully this helps!

I get the following error: “Base constructors must all have the same return type.”

Could you tell me how can I fix it?

@EyePulp I’m using v5.x, but when I use the hooks, I get: Error: Unrecognized datatype for attribute "rating.hooks" with

const rating = sequelize.define('rating', {
  // properties
}, {
  hooks: {
    afterConnect: // Rest of your code
  }
})

Instead I used the addHook function:

rating.addHook('afterConnect', function setParsers() {
  const cloneType = ...
  ...
  this.connectionManager.refreshTypeParser(dTypes);
}

Assuming I used the addHook function correctly, I still got DECIMAL numbers in string format. Am I forgetting to do something or did I not set addHook up correctly?

Also for the DECIMAL and other types in dTypes, shouldn’t it have to account for nulls? So should it be like: cloneType(DataTypes.DECIMAL, v => v && parseFloat(v)