Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Improved Data Type validation/conversion. #677

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

arthurschreiber
Copy link
Collaborator

This refactors the data type logic in tedious, making it more strict and more accurate.

If the parameter specified by the user can not be converted to the data type of the parameter without a loss of precision, the request will not be executed and an error will be passed to the request's callback instead.

The same parameter value conversion logic is now also used for both regular requests as well as for bulk loads, fixing a long standing issue where storing the same data via these two methods would result either result in a failure or different data being stored in the database.


This is dependent upon #676 being merged first.

@arthurschreiber
Copy link
Collaborator Author

@v-suhame @chdh @atsage @Hadis-Fard Would you mind taking a look at this?

return value;
dateValue = value;
} else {
dateValue = new Date(Date.parse(value));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to remove this redundant conversion to Date too 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I'm just keeping it because the old code allowed passing in strings as values for Time. We can remove this in a breaking change later. 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brought that up because datetime2n and datetimeoffsetn had conversion removed. Shouldn't the same behaviour apply for all 3 datatype?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, unfortunately I think that there might be users out there that use time and pass in timestrings. I'm not 100% sure, and I'd rather not break this right now, but sometime in the future. 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅 I think I should elaborated a bit more. If users are passing time in timestrings format, then it is likely they will do the same for datetime2 and datetimeoffset, as they all have nano second precision.

Code fragment from timen.js and datetimeoffsetn.js looks like,

validate(value) {
  ...
  dateValue = new Date(Date.parse(value));
  ...
}

writeParameterData: function(..){
  ..
  if (parameter.value != null) {
    const time = new Date(+parameter.value); // datetime2n.js doesn't have this conversion
    ...
}

We should add the conversion(pointed in the code fragment) back into datetime2n.js to allow string passed into datetime2.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, you're totally right! ❤️ Fixed this in 0d4e197.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all 3 types now use Date.parse in the validate method to allow conversion from strings, and they no longer perform redundant conversion inside the writeParameterData method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweet 👍

return value;

if (!Number.isFinite(numberValue) || (typeof value === 'string' && value !== numberValue.toString()) || numberValue.toString() !== numberValue.toFixed(4)) {
return new TypeError(`The given value could not be converted to ${this.name}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about removing or having an alternative for numberValue.toString() !== numberValue.toFixed(4)?

This condition is too strict, if input has scale < 4 it will throw exception, but driver should allow scale <= 4. Same goes for smallmoney.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add a test for money/smallmoney type with scale < 4? All current tests are for scale 4.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I missed handling of numbers that have less than 4 numbers after the decimal point. 😓

Copy link
Member

@IanChokS IanChokS Mar 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

numberValue.toString().split('.')[1].length > 4

validate: function(value) {
if (value == null) {
validate(value, length, precision, scale) {
if (value === undefined || value === null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is precision & scale here by mistake?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, will drop those! 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f9b49e1.

return value;

if (!Number.isFinite(numberValue) || (typeof value === 'string' && value !== numberValue.toString()) || numberValue !== Math.fround(numberValue)) {
return new TypeError(`The given value could not be converted to ${this.name}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't using Math.fround() be a breaking change? 🤔 I'm afraid that most of the data passed would fail at this point, and hinder the usage of real type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this one's a bit harder to decide.

The loss of precision is a huge issue tho, because most users will not know that real is limited and won't save the exact value they're trying to send, and thus the data in the database can no longer be trusted. That's why I think raising an error here is better than before and fixes a bug. What do you think? 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree to raising error here ✌️ I was just wondering how much of an impact it would have on existing user(i.e. will it be a breaking change), but as you pointed, users must have already run into precision loss, so it better to have this in sooner rather than later.

return TypeError('Invalid string.');
}
value = value.toString();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: remove precision, scale

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! 👍

src/request.js Outdated
@@ -175,11 +175,13 @@ module.exports = class Request extends EventEmitter {
validateParameters() {
for (let i = 0, len = this.parameters.length; i < len; i++) {
const parameter = this.parameters[i];
const value = parameter.type.validate(parameter.value);
const value = parameter.type.validate(parameter.value, parameter.length, parameter.precision, parameter.scale);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precision and scale are never actually used the validate(), let's remove them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a good point. They should be used for numeric and decimal (and maybe for some of the date types?). I'll take another look! 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0968cb4.

@arthurschreiber arthurschreiber force-pushed the arthur/better-type-validation branch 2 times, most recently from 31d6acd to 9ae5f2f Compare January 30, 2018 17:04
This refactors the data type logic in tedious, making it more strict and
more accurate.

If the parameter specified by the user can not be converted to the data
type of the parameter without a loss of precision, the request will not
be executed and an error will be passed to the request's callback
instead.

The same parameter value conversion logic is now also used for both
regular requests as well as for bulk loads, fixing a long standing issue
where storing the same data via these two methods would result either
result in a failure or different data being stored in the database.
@arthurschreiber
Copy link
Collaborator Author

Heya @tediousjs/microsoft-contributors 👋

how do you feel about this changeset? Should we declare it as a breaking change and merge and release it together with other breaking changes as v3.0?

@Suraiya-Hameed
Copy link
Member

+1 to releasing this along with other breaking changes. This PR is good to go once #677 (review) is handled 😄

src/bulk-load.js Outdated
length: c.length,
scale: c.scale,
precision: c.precision,
value: row[arr ? i : c.objName]
}, this.options);
value: c.type.validate(row[arr ? i : c.objName], c.length, c.precision, c.scale)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate() doesn't throw error but it returns error if validation fails. Can you add check to verify type of validate()'s return value before passing it to writeParameterData?

@MichaelSun90
Copy link
Contributor

@arthurschreiber, I think this PR may be outdated since the recent merged pip related changes? If you can confirm that, then I think can close this PR for now.

@@ -17,20 +17,28 @@ module.exports = {
writeParameterData: function(buffer, parameter) {
if (parameter.value != null) {
buffer.writeUInt8(8);
buffer.writeDoubleLE(parseFloat(parameter.value));
buffer.writeDoubleLE(parameter.value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this change is necessary?

Comment on lines 78 to 83
return {
value: column.type.validate(columnValue, column.length, column.scale, column.precision),
length: column.length,
scale: column.scale,
precision: column.precision
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning an object causes data-type value inputs to vary: if using TVP, data-type inputs would be this object, whereas anything else would be a single value. To avoid this two variations of data-type inputs, this TVP .validate() method should only return the single value, such as return column.type.validate(columnValue, column.length, column.scale, column.precision)

@codecov
Copy link

codecov bot commented Mar 25, 2020

Codecov Report

Merging #677 into master will decrease coverage by 0.92%.
The diff coverage is 81.77%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #677      +/-   ##
==========================================
- Coverage   80.77%   79.85%   -0.93%     
==========================================
  Files          86       86              
  Lines        4384     4427      +43     
  Branches      793      807      +14     
==========================================
- Hits         3541     3535       -6     
- Misses        584      630      +46     
- Partials      259      262       +3     
Impacted Files Coverage Δ
src/data-type.ts 100.00% <ø> (ø)
src/data-types/date.ts 48.00% <0.00%> (-2.00%) ⬇️
src/data-types/binary.ts 70.27% <25.00%> (-6.21%) ⬇️
src/data-types/varbinary.ts 71.21% <25.00%> (-3.40%) ⬇️
src/data-types/bigint.ts 77.27% <40.00%> (-12.21%) ⬇️
src/data-types/uniqueidentifier.ts 83.33% <50.00%> (+5.55%) ⬆️
src/data-types/tvp.ts 89.65% <63.63%> (+0.76%) ⬆️
src/bulk-load.ts 79.37% <66.66%> (-1.04%) ⬇️
src/data-types/nvarchar.ts 90.24% <72.72%> (-3.27%) ⬇️
src/data-types/varchar.ts 83.33% <72.72%> (-3.24%) ⬇️
... and 22 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2fb901a...0ff5ba0. Read the comment docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants