Skip to content

Commit c3d1abc

Browse files
authored
Implementing type reflection from mysql result (#1068)
1 parent c66eb36 commit c3d1abc

File tree

4 files changed

+286
-2
lines changed

4 files changed

+286
-2
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,12 @@ client.query("SELECT * FROM users WHERE group='githubbers'", :symbolize_keys =>
165165
end
166166
```
167167

168-
You can get the headers and the columns in the order that they were returned
168+
You can get the headers, columns, and the field types in the order that they were returned
169169
by the query like this:
170170

171171
``` ruby
172172
headers = results.fields # <= that's an array of field names, in order
173+
types = results.field_types # <= that's an array of field types, in order
173174
results.each(:as => :array) do |row|
174175
# Each row is an array, ordered the same as the query results
175176
# An otter's den is called a "holt" or "couch"

ext/mysql2/result.c

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ static rb_encoding *binaryEncoding;
1717
*/
1818
#define MYSQL2_MIN_TIME 2678400ULL
1919

20+
#define MYSQL2_MAX_BYTES_PER_CHAR 3
21+
22+
/* From Mysql documentations:
23+
* To distinguish between binary and nonbinary data for string data types,
24+
* check whether the charsetnr value is 63. If so, the character set is binary,
25+
* which indicates binary rather than nonbinary data. This enables you to distinguish BINARY
26+
* from CHAR, VARBINARY from VARCHAR, and the BLOB types from the TEXT types.
27+
*/
28+
#define MYSQL2_BINARY_CHARSET 63
29+
30+
#ifndef MYSQL_TYPE_JSON
31+
#define MYSQL_TYPE_JSON 245
32+
#endif
33+
2034
#define GET_RESULT(self) \
2135
mysql2_result_wrapper *wrapper; \
2236
Data_Get_Struct(self, mysql2_result_wrapper, wrapper);
@@ -169,9 +183,171 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, int symbo
169183
return rb_field;
170184
}
171185

186+
static VALUE rb_mysql_result_fetch_field_type(VALUE self, unsigned int idx) {
187+
VALUE rb_field_type;
188+
GET_RESULT(self);
189+
190+
if (wrapper->fieldTypes == Qnil) {
191+
wrapper->numberOfFields = mysql_num_fields(wrapper->result);
192+
wrapper->fieldTypes = rb_ary_new2(wrapper->numberOfFields);
193+
}
194+
195+
rb_field_type = rb_ary_entry(wrapper->fieldTypes, idx);
196+
if (rb_field_type == Qnil) {
197+
MYSQL_FIELD *field = NULL;
198+
rb_encoding *default_internal_enc = rb_default_internal_encoding();
199+
rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding);
200+
int precision;
201+
202+
field = mysql_fetch_field_direct(wrapper->result, idx);
203+
204+
switch(field->type) {
205+
case MYSQL_TYPE_NULL: // NULL
206+
rb_field_type = rb_str_new_cstr("null");
207+
break;
208+
case MYSQL_TYPE_TINY: // signed char
209+
rb_field_type = rb_sprintf("tinyint(%ld)", field->length);
210+
break;
211+
case MYSQL_TYPE_SHORT: // short int
212+
rb_field_type = rb_sprintf("smallint(%ld)", field->length);
213+
break;
214+
case MYSQL_TYPE_YEAR: // short int
215+
rb_field_type = rb_sprintf("year(%ld)", field->length);
216+
break;
217+
case MYSQL_TYPE_INT24: // int
218+
rb_field_type = rb_sprintf("mediumint(%ld)", field->length);
219+
break;
220+
case MYSQL_TYPE_LONG: // int
221+
rb_field_type = rb_sprintf("int(%ld)", field->length);
222+
break;
223+
case MYSQL_TYPE_LONGLONG: // long long int
224+
rb_field_type = rb_sprintf("bigint(%ld)", field->length);
225+
break;
226+
case MYSQL_TYPE_FLOAT: // float
227+
rb_field_type = rb_sprintf("float(%ld,%d)", field->length, field->decimals);
228+
break;
229+
case MYSQL_TYPE_DOUBLE: // double
230+
rb_field_type = rb_sprintf("double(%ld,%d)", field->length, field->decimals);
231+
break;
232+
case MYSQL_TYPE_TIME: // MYSQL_TIME
233+
rb_field_type = rb_str_new_cstr("time");
234+
break;
235+
case MYSQL_TYPE_DATE: // MYSQL_TIME
236+
case MYSQL_TYPE_NEWDATE: // MYSQL_TIME
237+
rb_field_type = rb_str_new_cstr("date");
238+
break;
239+
case MYSQL_TYPE_DATETIME: // MYSQL_TIME
240+
rb_field_type = rb_str_new_cstr("datetime");
241+
break;
242+
case MYSQL_TYPE_TIMESTAMP: // MYSQL_TIME
243+
rb_field_type = rb_str_new_cstr("timestamp");
244+
break;
245+
case MYSQL_TYPE_DECIMAL: // char[]
246+
case MYSQL_TYPE_NEWDECIMAL: // char[]
247+
/*
248+
Handle precision similar to this line from mysql's code:
249+
https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/field.cc#L2246
250+
*/
251+
precision = field->length - (field->decimals > 0 ? 2 : 1);
252+
rb_field_type = rb_sprintf("decimal(%ld,%d)", precision, field->decimals);
253+
break;
254+
case MYSQL_TYPE_STRING: // char[]
255+
if (field->flags & ENUM_FLAG) {
256+
rb_field_type = rb_str_new_cstr("enum");
257+
} else if (field->flags & SET_FLAG) {
258+
rb_field_type = rb_str_new_cstr("set");
259+
} else {
260+
if (field->charsetnr == MYSQL2_BINARY_CHARSET) {
261+
rb_field_type = rb_sprintf("binary(%ld)", field->length);
262+
} else {
263+
rb_field_type = rb_sprintf("char(%ld)", field->length / MYSQL2_MAX_BYTES_PER_CHAR);
264+
}
265+
}
266+
break;
267+
case MYSQL_TYPE_VAR_STRING: // char[]
268+
if (field->charsetnr == MYSQL2_BINARY_CHARSET) {
269+
rb_field_type = rb_sprintf("varbinary(%ld)", field->length);
270+
} else {
271+
rb_field_type = rb_sprintf("varchar(%ld)", field->length / MYSQL2_MAX_BYTES_PER_CHAR);
272+
}
273+
break;
274+
case MYSQL_TYPE_VARCHAR: // char[]
275+
rb_field_type = rb_sprintf("varchar(%ld)", field->length / MYSQL2_MAX_BYTES_PER_CHAR);
276+
break;
277+
case MYSQL_TYPE_TINY_BLOB: // char[]
278+
rb_field_type = rb_str_new_cstr("tinyblob");
279+
break;
280+
case MYSQL_TYPE_BLOB: // char[]
281+
if (field->charsetnr == MYSQL2_BINARY_CHARSET) {
282+
switch(field->length) {
283+
case 255:
284+
rb_field_type = rb_str_new_cstr("tinyblob");
285+
break;
286+
case 65535:
287+
rb_field_type = rb_str_new_cstr("blob");
288+
break;
289+
case 16777215:
290+
rb_field_type = rb_str_new_cstr("mediumblob");
291+
break;
292+
case 4294967295:
293+
rb_field_type = rb_str_new_cstr("longblob");
294+
default:
295+
break;
296+
}
297+
} else {
298+
if (field->length == (255 * MYSQL2_MAX_BYTES_PER_CHAR)) {
299+
rb_field_type = rb_str_new_cstr("tinytext");
300+
} else if (field->length == (65535 * MYSQL2_MAX_BYTES_PER_CHAR)) {
301+
rb_field_type = rb_str_new_cstr("text");
302+
} else if (field->length == (16777215 * MYSQL2_MAX_BYTES_PER_CHAR)) {
303+
rb_field_type = rb_str_new_cstr("mediumtext");
304+
} else if (field->length == 4294967295) {
305+
rb_field_type = rb_str_new_cstr("longtext");
306+
} else {
307+
rb_field_type = rb_sprintf("text(%ld)", field->length);
308+
}
309+
}
310+
break;
311+
case MYSQL_TYPE_MEDIUM_BLOB: // char[]
312+
rb_field_type = rb_str_new_cstr("mediumblob");
313+
break;
314+
case MYSQL_TYPE_LONG_BLOB: // char[]
315+
rb_field_type = rb_str_new_cstr("longblob");
316+
break;
317+
case MYSQL_TYPE_BIT: // char[]
318+
rb_field_type = rb_sprintf("bit(%ld)", field->length);
319+
break;
320+
case MYSQL_TYPE_SET: // char[]
321+
rb_field_type = rb_str_new_cstr("set");
322+
break;
323+
case MYSQL_TYPE_ENUM: // char[]
324+
rb_field_type = rb_str_new_cstr("enum");
325+
break;
326+
case MYSQL_TYPE_GEOMETRY: // char[]
327+
rb_field_type = rb_str_new_cstr("geometry");
328+
break;
329+
case MYSQL_TYPE_JSON: // json
330+
rb_field_type = rb_str_new_cstr("json");
331+
break;
332+
default:
333+
rb_field_type = rb_str_new_cstr("unknown");
334+
break;
335+
}
336+
337+
rb_enc_associate(rb_field_type, conn_enc);
338+
if (default_internal_enc) {
339+
rb_field_type = rb_str_export_to_enc(rb_field_type, default_internal_enc);
340+
}
341+
342+
rb_ary_store(wrapper->fieldTypes, idx, rb_field_type);
343+
}
344+
345+
return rb_field_type;
346+
}
347+
172348
static VALUE mysql2_set_field_string_encoding(VALUE val, MYSQL_FIELD field, rb_encoding *default_internal_enc, rb_encoding *conn_enc) {
173349
/* if binary flag is set, respect its wishes */
174-
if (field.flags & BINARY_FLAG && field.charsetnr == 63) {
350+
if (field.flags & BINARY_FLAG && field.charsetnr == MYSQL2_BINARY_CHARSET) {
175351
rb_enc_associate(val, binaryEncoding);
176352
} else if (!field.charsetnr) {
177353
/* MySQL 4.x may not provide an encoding, binary will get the bytes through */
@@ -716,6 +892,25 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) {
716892
return wrapper->fields;
717893
}
718894

895+
static VALUE rb_mysql_result_fetch_field_types(VALUE self) {
896+
unsigned int i = 0;
897+
898+
GET_RESULT(self);
899+
900+
if (wrapper->fieldTypes == Qnil) {
901+
wrapper->numberOfFields = mysql_num_fields(wrapper->result);
902+
wrapper->fieldTypes = rb_ary_new2(wrapper->numberOfFields);
903+
}
904+
905+
if ((my_ulonglong)RARRAY_LEN(wrapper->fieldTypes) != wrapper->numberOfFields) {
906+
for (i=0; i<wrapper->numberOfFields; i++) {
907+
rb_mysql_result_fetch_field_type(self, i);
908+
}
909+
}
910+
911+
return wrapper->fieldTypes;
912+
}
913+
719914
static VALUE rb_mysql_result_each_(VALUE self,
720915
VALUE(*fetch_row_func)(VALUE, MYSQL_FIELD *fields, const result_each_args *args),
721916
const result_each_args *args)
@@ -934,6 +1129,7 @@ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_
9341129
wrapper->resultFreed = 0;
9351130
wrapper->result = r;
9361131
wrapper->fields = Qnil;
1132+
wrapper->fieldTypes = Qnil;
9371133
wrapper->rows = Qnil;
9381134
wrapper->encoding = encoding;
9391135
wrapper->streamingComplete = 0;
@@ -971,6 +1167,7 @@ void init_mysql2_result() {
9711167
cMysql2Result = rb_define_class_under(mMysql2, "Result", rb_cObject);
9721168
rb_define_method(cMysql2Result, "each", rb_mysql_result_each, -1);
9731169
rb_define_method(cMysql2Result, "fields", rb_mysql_result_fetch_fields, 0);
1170+
rb_define_method(cMysql2Result, "field_types", rb_mysql_result_fetch_field_types, 0);
9741171
rb_define_method(cMysql2Result, "free", rb_mysql_result_free_, 0);
9751172
rb_define_method(cMysql2Result, "count", rb_mysql_result_count, 0);
9761173
rb_define_alias(cMysql2Result, "size", "count");

ext/mysql2/result.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_
66

77
typedef struct {
88
VALUE fields;
9+
VALUE fieldTypes;
910
VALUE rows;
1011
VALUE client;
1112
VALUE encoding;

spec/mysql2/result_spec.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
r = Mysql2::Result.new
1010
expect { r.count }.to raise_error(TypeError)
1111
expect { r.fields }.to raise_error(TypeError)
12+
expect { r.field_types }.to raise_error(TypeError)
1213
expect { r.size }.to raise_error(TypeError)
1314
expect { r.each }.to raise_error(TypeError)
1415
end
@@ -119,6 +120,90 @@
119120
end
120121
end
121122

123+
context "#field_types" do
124+
let(:test_result) { @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1") }
125+
126+
it "method should exist" do
127+
expect(test_result).to respond_to(:field_types)
128+
end
129+
130+
it "should return correct types" do
131+
expected_types = %w[
132+
mediumint(9)
133+
varchar(10)
134+
bit(64)
135+
bit(1)
136+
tinyint(4)
137+
tinyint(1)
138+
smallint(6)
139+
mediumint(9)
140+
int(11)
141+
bigint(20)
142+
float(10,3)
143+
float(10,3)
144+
double(10,3)
145+
decimal(10,3)
146+
decimal(10,3)
147+
date
148+
datetime
149+
timestamp
150+
time
151+
year(4)
152+
char(10)
153+
varchar(10)
154+
binary(10)
155+
varbinary(10)
156+
tinyblob
157+
tinytext
158+
blob
159+
text
160+
mediumblob
161+
mediumtext
162+
longblob
163+
longtext
164+
enum
165+
set
166+
]
167+
168+
expect(test_result.field_types).to eql(expected_types)
169+
end
170+
171+
it "should return an array of field types in proper order" do
172+
result = @client.query(
173+
"SELECT cast('a' as char), " \
174+
"cast(1.2 as decimal(15, 2)), " \
175+
"cast(1.2 as decimal(15, 5)), " \
176+
"cast(1.2 as decimal(15, 4)), " \
177+
"cast(1.2 as decimal(15, 10)), " \
178+
"cast(1.2 as decimal(14, 0)), " \
179+
"cast(1.2 as decimal(15, 0)), " \
180+
"cast(1.2 as decimal(16, 0)), " \
181+
"cast(1.0 as decimal(16, 1))",
182+
)
183+
184+
expected_types = %w[
185+
varchar(1)
186+
decimal(15,2)
187+
decimal(15,5)
188+
decimal(15,4)
189+
decimal(15,10)
190+
decimal(14,0)
191+
decimal(15,0)
192+
decimal(16,0)
193+
decimal(16,1)
194+
]
195+
196+
expect(result.field_types).to eql(expected_types)
197+
end
198+
199+
it "should return json type on mysql 8.0" do
200+
next unless /8.\d+.\d+/ =~ @client.server_info[:version]
201+
202+
result = @client.query("SELECT JSON_OBJECT('key', 'value')")
203+
expect(result.field_types).to eql(['json'])
204+
end
205+
end
206+
122207
context "streaming" do
123208
it "should maintain a count while streaming" do
124209
result = @client.query('SELECT 1', stream: true, cache_rows: false)

0 commit comments

Comments
 (0)