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

Avoid RangeError on integers larger than LONG_LONG #764

Merged

Conversation

nurse
Copy link
Contributor

@nurse nurse commented Jun 28, 2016

CRuby's rb_big2ull raises RangeError if the argument is out of [LONG_MIN, ULONG_MAX].
To avoid that calling to_s and set as decimal with some tricks for edge cases.

int64_min = -(1 << 63)
result = stmt.execute(int64_max, int64_min)
expect(result.to_a).to eq(['max' => int64_max, 'min' => int64_min])
@client.query 'DROP TABLE IF EXISTS mysql2_stmt_q'
Copy link
Collaborator

Choose a reason for hiding this comment

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

No need to drop the table since these tests aren't creating/querying from it.

@sodabrew
Copy link
Collaborator

Thank you! Will this also work in the future merged Integer / FixNum / BigNum world of Ruby 2.4.0?

@nurse nurse force-pushed the dont-raise-rangeerror-for-large-integer branch from f47cb50 to 1a0703c Compare June 29, 2016 04:46
nurse added a commit to nurse/mysql2 that referenced this pull request Jun 29, 2016
@nurse
Copy link
Contributor Author

nurse commented Jun 29, 2016

Yeah, TYPE(v) == T_BIGNUM still works with Ruby 2.4.

@sodabrew
Copy link
Collaborator

Ruby 1.8.7 does have rb_big_cmp, but it is static: http://rxr.whitequark.org/mri/source/bignum.c?v=1.8.7-p374#1107

You could call Bignum#<=> to get the same effect?

/* elsewhere in the file */
static ID id_cmp;
id_cmp = rb_intern("<=>");
/**/

#ifdef HAVE_RB_BIG_CMP
  VALUE less_than_llong_min = rb_big_cmp(bignum, LL2NUM(LLONG_MIN));
#else
  VALUE less_than_llong_min = rb_funcall(bignum, id_cmp, 1, LL2NUM(LLONG_MIN));
#endif
  if (less_than_llong_min == INT2FIX(-1)) {
    // code
  } else {
    // code
  }

@nurse
Copy link
Contributor Author

nurse commented Jun 29, 2016

rb_funcall can be an alternative but it is slow.

I noticed len == 8 && nlz_bits == 0 && rb_absint_singlebit_p(bignum) is the best condition because it doesn't allocate extra object.
For older rubies I'm using rb_big_and or rb_big_cmp now, but rb_funcall may be better for the people who maintains the code. I'll fix them if you prefer that.

#endif
if (len > 8) goto overflow;
if (RBIGNUM_POSITIVE_P(bignum)) {
num = rb_big2ull(bignum);
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like if BIGSIZE(bignum) > SIZEOF_LONG_LONG, should be goto overflow.
http://rxr.whitequark.org/mri/source/bignum.c#3587

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, enough to if (len > 8) goto overflow;?

Copy link
Contributor Author

@nurse nurse Jun 29, 2016

Choose a reason for hiding this comment

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

Yes. len > 8 means "Bignum's raw size is larger than 8 byte.
It is the same meaning with BIGSIZE(bignum) > SIZEOF_LONG_LONG.

Anyway the spec should have int64_max3 = 1 << 64; I'll add it tomorrow.

@sodabrew sodabrew added this to the 0.4.5 milestone Jun 29, 2016
@nurse nurse changed the title [WIP] Avoid RangeError on integers larger than LONG_LONG Avoid RangeError on integers larger than LONG_LONG Jul 7, 2016
#else
len = RBIGNUM_LEN(bignum) * SIZEOF_BDIGITS;
#endif
if (len > 8) goto overflow;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Length 8 is intended to be the same as sizeof(long long int) right? Would it make sense for the integer size to be passed in as an argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed!

@nurse nurse force-pushed the dont-raise-rangeerror-for-large-integer branch 2 times, most recently from 37410c4 to 81fb72b Compare July 7, 2016 06:24
nurse added a commit to nurse/mysql2 that referenced this pull request Jul 7, 2016
@nurse
Copy link
Contributor Author

nurse commented Jul 20, 2016

@sodabrew I reflected your comments. What do you think about current one?

@sodabrew
Copy link
Collaborator

Sorry for the long delay on this PR, and it's run into a merge conflict since the recent ssl_mode PR added other items to extconf.rb. Do you have time to rebase this PR? If not I can do a rebase and squash merge mysql.

sodabrew pushed a commit to sodabrew/mysql2 that referenced this pull request Oct 19, 2016
CRuby's rb_big2ull raises RangeError if the argument is out of [LONG_MIN, ULONG_MAX].
To avoid that calling to_s and set as decimal with some tricks for edge cases.

For Ruby 1.8.7, use rb_funcall() to call Bignum#<=> through Ruby space.
For Ruby 1.9.3 and 2.0.0, use rb_big_cmp() to call Bignum#<=> directly.

For Ruby 2.1 and above, use rb_absint_size() and I noticed
  len == 8 && nlz_bits == 0 && rb_absint_singlebit_p(bignum)
is the best condition because it doesn't allocate extra object.
@nurse nurse force-pushed the dont-raise-rangeerror-for-large-integer branch from c2c67e7 to f59eb53 Compare October 19, 2016 04:49
@nurse
Copy link
Contributor Author

nurse commented Oct 19, 2016

Hmm, rebased but it fails. I'll investigate later...

@sodabrew
Copy link
Collaborator

sodabrew commented Oct 19, 2016

Thanks! There's an interaction with #783 that results in the values getting cast as BigDecimal instead of a string on return. The reason is that MySQL defaults to returning numeric values as decimals unless it knows a specific integer size from a column definition or a procedure signature.

Reminds me of #563 and #557 and #58.

@sodabrew
Copy link
Collaborator

I think #783 actually correctly fixed the big number types, and you can remove the .to_s that look like a workaround in the test?

diff --git a/spec/mysql2/statement_spec.rb b/spec/mysql2/statement_spec.rb
index 517bd31..3d00535 100644
--- a/spec/mysql2/statement_spec.rb
+++ b/spec/mysql2/statement_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe Mysql2::Statement do
     int64_min2 = -(1 << 64) + 1
     int64_min3 = -0xC000000000000000
     result = stmt.execute(int64_max1, int64_max2, int64_max3, int64_min1, int64_min2, int64_min3)
-    expect(result.to_a).to eq(['max1' => int64_max1.to_s, 'max2' => int64_max2.to_s, 'max3' => int64_max3.to_s, 'min1' => int64_min1.to_s, 'min2' => int64_min2.to_s, 'min3' => int6
+    expect(result.to_a).to eq(['max1' => int64_max1, 'max2' => int64_max2, 'max3' => int64_max3, 'min1' => int64_min1, 'min2' => int64_min2, 'min3' => int64_min3])
   end

* Use rb_absint_size and rb_absint_singlebit_p on Ruby 2.1 or later.
  They don't allocate new objects and fast.
* Use rb_big_cmp on Ruby 1.9.x and 2.0.0
* Use rb_funcall(bignum, rb_intern("<=>")) on Ruby 1.8 and REE

Note that this works on Ruby 2.4.
@nurse nurse force-pushed the dont-raise-rangeerror-for-large-integer branch from f59eb53 to f10a337 Compare October 19, 2016 10:03
@nurse
Copy link
Contributor Author

nurse commented Oct 19, 2016

@sodabrew Fixed as you says removed to_s. It still fails on some environments but seems not related with this PR...

@sodabrew sodabrew merged commit 5e2b3a6 into brianmario:master Oct 19, 2016
@nurse nurse deleted the dont-raise-rangeerror-for-large-integer branch October 19, 2016 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants