Skip to content

Commit

Permalink
Add UUID support (#76)
Browse files Browse the repository at this point in the history
* added to_mysql function for UUID conversion to string.

* Fixed missing require

* changed uuid read/write to binary and added spec tests

* fixed spec test

* correct fix for uuid spec test

* Update spec/db_spec.cr

Co-Authored-By: Brian J. Cardiff <[email protected]>

* update uuid specs

* update spec test for 5.6 tests

* fix for uuid slice

* added better read for uuid

* added missing uuid logic to text_result_set

* Remove code

* Refactor null_bitmap handling into mysql_read

* Handle nillable UUID

* Use sample_value helper to test read/write UUIDs

* Support UUID for binary columns only

unify behavior across sql versions.The user will need to do String <-> UUID conversion manually or via mapping converters if UUID are wanted to be stored as text

* Avoid dup in slice, allow StaticArray args as blobs

Co-authored-by: Brian J. Cardiff <[email protected]>
  • Loading branch information
kalinon and bcardiff authored Oct 7, 2020
1 parent 435b68e commit 2deace7
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 13 deletions.
28 changes: 28 additions & 0 deletions spec/db_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ DB::DriverSpecs(MySql::Any).run do
sample_value Time::Span.new(nanoseconds: 0), "Time", "TIME('00:00:00')"
sample_value Time::Span.new(hours: 10, minutes: 25, seconds: 21), "Time", "TIME('10:25:21')"
sample_value Time::Span.new(days: 0, hours: 0, minutes: 10, seconds: 5, nanoseconds: 0), "Time", "TIME('00:10:05.000')"
sample_value UUID.new("87b3042b-9b9a-41b7-8b15-a93d3f17025e"), "BLOB", "X'87b3042b9b9a41b78b15a93d3f17025e'", type_safe_value: false
sample_value UUID.new("87b3042b-9b9a-41b7-8b15-a93d3f17025e"), "binary(16)", %(UNHEX(REPLACE("87b3042b-9b9a-41b7-8b15-a93d3f17025e", "-",""))), type_safe_value: false

DB.open db_url do |db|
# needs to check version, microsecond support >= 5.7
Expand Down Expand Up @@ -231,4 +233,30 @@ DB::DriverSpecs(MySql::Any).run do
db.query_one("SELECT EXISTS(SELECT 1 FROM data WHERE id = ?);", 1, as: Bool).should be_true
db.query_one("SELECT EXISTS(SELECT 1 FROM data WHERE id = ?);", 2, as: Bool).should be_false
end

it "should raise when reading UUID from text columns" do |db|
db.exec "create table data (id int not null primary key auto_increment, uuid_text varchar(36));"
db.exec %(insert into data (uuid_text) values ("87b3042b-9b9a-41b7-8b15-a93d3f17025e");)

expect_raises(DB::Error, "The column uuid_text of type MySql::Type::VarString returns a String and can't be read as UUID") do
db.prepared.query_one("SELECT uuid_text FROM data", as: UUID)
end

expect_raises(DB::Error, "The column uuid_text of type MySql::Type::VarString returns a String and can't be read as UUID") do
db.unprepared.query_one("SELECT uuid_text FROM data", as: UUID)
end
end

it "should raise when reading UUID from binary columns with invalid length" do |db|
db.exec "create table data (id int not null primary key auto_increment, uuid_blob blob);"
db.exec %(insert into data (uuid_blob) values (X'415A617A');)

expect_raises(ArgumentError, "Invalid bytes length 4, expected 16") do
db.prepared.query_one("SELECT uuid_blob FROM data", as: UUID)
end

expect_raises(ArgumentError, "Invalid bytes length 4, expected 16") do
db.unprepared.query_one("SELECT uuid_blob FROM data", as: UUID)
end
end
end
2 changes: 1 addition & 1 deletion src/mysql.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module MySql
end
end

alias Any = DB::Any | Int16 | Int8 | Time::Span
alias Any = DB::Any | Int16 | Int8 | Time::Span | UUID

# :nodoc:
TIME_ZONE = Time::Location::UTC
Expand Down
38 changes: 29 additions & 9 deletions src/mysql/result_set.cr
Original file line number Diff line number Diff line change
Expand Up @@ -63,31 +63,51 @@ class MySql::ResultSet < DB::ResultSet
@columns[index].name
end

def read
protected def mysql_read
row_packet = @row_packet.not_nil!

is_nil = @null_bitmap[@column_index + 2]
col = @column_index
@column_index += 1
if is_nil
nil
elsif false
# this is need to make read "return" a Bool
# otherwise the base `#read(T) forall T` (which is ovewriten)
# complains to cast `read.as(Bool)` since the return type
# of #read would be a union without Bool
false
else
val = @columns[col].column_type.read(row_packet)
column = @columns[col]
yield row_packet, column
end
end

def read
mysql_read do |row_packet, column|
val = column.column_type.read(row_packet)

# http://dev.mysql.com/doc/internals/en/character-set.html
if val.is_a?(Slice(UInt8)) && @columns[col].character_set != 63
if val.is_a?(Slice(UInt8)) && column.character_set != 63
::String.new(val)
else
val
end
end
end

def read(t : UUID.class)
read(UUID?).as(UUID)
end

def read(t : (UUID | Nil).class)
mysql_read do |row_packet, column|
if column.flags.bits_set?(128)
data = row_packet.read_blob
# Check if binary flag is set
# https://dev.mysql.com/doc/dev/mysql-server/latest/group__group__cs__column__definition__flags.html#gaf74577f0e38eed5616a090965aeac323
UUID.new data
else
data = column.column_type.read(row_packet)
raise ::DB::Error.new("The column #{column.name} of type #{column.column_type} returns a #{data.class} and can't be read as UUID")
end
end
end

def read(t : Bool.class)
MySql::Type.from_mysql(read.as(Int::Signed))
end
Expand Down
33 changes: 30 additions & 3 deletions src/mysql/text_result_set.cr
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class MySql::TextResultSet < DB::ResultSet
@columns[index].name
end

def read
protected def mysql_read
row_packet = @row_packet.not_nil!

if @first_row_packet
Expand All @@ -78,19 +78,46 @@ class MySql::TextResultSet < DB::ResultSet
if is_nil
nil
else
column = @columns[col]
length = row_packet.read_lenenc_int(current_byte)
yield row_packet, column, length
end
end

def read
mysql_read do |row_packet, column, length|
val = row_packet.read_string(length)
val = @columns[col].column_type.parse(val)
val = column.column_type.parse(val)

# http://dev.mysql.com/doc/internals/en/character-set.html
if val.is_a?(Slice(UInt8)) && @columns[col].character_set != 63
if val.is_a?(Slice(UInt8)) && column.character_set != 63
::String.new(val)
else
val
end
end
end

def read(t : UUID.class)
read(UUID?).as(UUID)
end

def read(t : (UUID | Nil).class)
mysql_read do |row_packet, column, length|
if column.flags.bits_set?(128)
# Check if binary flag is set
# https://dev.mysql.com/doc/dev/mysql-server/latest/group__group__cs__column__definition__flags.html#gaf74577f0e38eed5616a090965aeac323
ary = row_packet.read_byte_array(length)
val = Bytes.new(ary.to_unsafe, ary.size)

UUID.new val
else
val = row_packet.read_string(length)
raise ::DB::Error.new("The column #{column.name} of type #{column.column_type} returns a #{val.class} and can't be read as UUID")
end
end
end

def read(t : Bool.class)
MySql::Type.from_mysql(read.as(Int::Signed))
end
Expand Down
15 changes: 15 additions & 0 deletions src/mysql/types.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require "uuid"

# :nodoc:
abstract struct MySql::Type
# Column types
Expand Down Expand Up @@ -48,6 +50,10 @@ abstract struct MySql::Type
MySql::Type::Blob
end

def self.type_for(t : ::StaticArray(T, N).class) forall T, N
MySql::Type::Blob
end

def self.type_for(t : ::Time.class)
MySql::Type::DateTime
end
Expand Down Expand Up @@ -96,6 +102,11 @@ abstract struct MySql::Type
v ? 1i8 : 0i8
end

# :nodoc:
def self.to_mysql(v : ::UUID)
v.bytes
end

# :nodoc:
def self.from_mysql(v : Int::Signed) : Bool
v != 0
Expand Down Expand Up @@ -284,6 +295,10 @@ abstract struct MySql::Type
packet.write_blob v
end

def self.write(packet, v : ::StaticArray(T, N)) forall T, N
packet.write_blob v.to_slice
end

def self.read(packet)
packet.read_blob
end
Expand Down

0 comments on commit 2deace7

Please sign in to comment.