├── .github └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ └── stale.yml ├── .gitignore ├── .ruby-version ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── mysql-binuuid-rails.rb └── mysql-binuuid │ ├── type.rb │ └── version.rb ├── mysql-binuuid-rails.gemspec └── test ├── gemfiles ├── rails-6.1.gemfile ├── rails-7.0.gemfile ├── rails-7.1.gemfile └── rails-8.0.gemfile ├── integration └── mysql_integration_test.rb ├── mysql-binuuid ├── type_data_test.rb └── type_test.rb └── test_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request_target ] 4 | 5 | concurrency: 6 | group: ci-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | tests: 11 | # Continue on error so that we see the results of all tests in the matrix. 12 | continue-on-error: true 13 | strategy: 14 | matrix: 15 | ruby: [ "3.4", "3.3", "3.2", "3.1" ] 16 | rails: [ "8.0", "7.1", "7.0", "6.1" ] 17 | exclude: 18 | # Rails 8 needs Ruby 3.2 or higher 19 | - { ruby: "3.1", rails: "8.0" } 20 | name: "tests (Ruby: ${{ matrix.ruby }}, Rails: ${{ matrix.rails }})" 21 | runs-on: ubuntu-latest 22 | env: 23 | MYSQL_HOST: '127.0.0.1' 24 | BUNDLE_GEMFILE: test/gemfiles/rails-${{ matrix.rails }}.gemfile 25 | services: 26 | mysql: 27 | image: mysql:8 28 | ports: 29 | - "3306:3306" 30 | env: 31 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 32 | options: --health-cmd "mysqladmin ping" --health-interval 5s --health-timeout 5s --health-retries 5 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: ruby/setup-ruby@v1 36 | with: 37 | bundler-cache: true 38 | ruby-version: ${{ matrix.ruby }} 39 | - run: bundle exec rake 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '33 17 * * 4' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'ruby' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v3 28 | 29 | # Initializes the CodeQL tools for scanning. 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v2 32 | with: 33 | languages: ${{ matrix.language }} 34 | 35 | - name: Perform CodeQL Analysis 36 | uses: github/codeql-action/analyze@v2 37 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues and PRs 2 | on: 3 | schedule: 4 | - cron: '40 6 * * *' 5 | jobs: 6 | stale: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/stale@v3 10 | with: 11 | stale-pr-message: >- 12 | This pull request has been automatically marked as stale because it has 13 | not had recent activity. It will be closed if no further activity occurs 14 | within a week. 15 | stale-issue-message: >- 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs within 18 | a week. 19 | stale-issue-label: stale 20 | stale-pr-label: stale 21 | days-before-stale: 90 22 | days-before-close: 7 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /.DS_Store 3 | /tmp 4 | /*.gem 5 | Gemfile.lock 6 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.1 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.0 2 | * Up required Ruby version to 2.6. 3 | 4 | # 1.2.1 5 | * Development: Now that we're running Semaphore, no need for Travis (#31) 6 | * Reduce dependencies listed in gemspec (#30) (Dependency on Rails removed, 7 | only need to depend on ActiveRecord) 8 | 9 | # 1.2.0 10 | * Set minimum Ruby version from 2.3 to 2.4 (2.3 is EOL and no longer maintained) 11 | * Fixed an issue where a UUID would be unpacked again while it's a perfectly 12 | fine UUID already. Thanks @sirwolfgang. 13 | 14 | # 1.1.1 15 | * Fixes possible SQL injection for ActiveRecord columns typed with 16 | MySQLBinUUID::Type. Thank you @ejoubaud, @geoffevason and @viraptor. 17 | 18 | # 1.1.0 19 | * Set minimum Ruby version from 2.2 to 2.3 20 | * Set default Ruby version to 2.5.1 21 | * Updated README shipped with the gem 22 | 23 | # 1.0.0 24 | * Initial release. 25 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Thank you! 2 | 3 | A word of thanks to all those that have contributed to this project: 4 | 5 | * Emmanuel Joubaud - [@ejoubaud](https://github.com/ejoubaud) 6 | * Geoff Evason - [@geoffevason](https://github.com/geoffevason) 7 | * Mark Oude Veldhuis - [@markoudev](https://github.com/markoudev) 8 | * Stanisław Pitucha - [@viraptor](https://github.com/viraptor) 9 | * Tjalling van der Wal - [@tjallingvanderwal](https://github.com/tjallingvanderwal) 10 | * Zane Wolfgang Pickett - [@sirwolfgang](https://github.com/sirwolfgang) 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in the gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Mark Oude Veldhuis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/nedap/mysql-binuuid-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/nedap/mysql-binuuid-rails/actions/workflows/ci.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/7bcb6538e7666bc37f9a/maintainability)](https://codeclimate.com/github/nedap/mysql-binuuid-rails/maintainability) 2 | 3 | 4 | # mysql-binuuid-rails 5 | `mysql-binuuid-rails` lets you define attributes of a UUID type on your models 6 | by leveraging the Attributes API that has been available since Rails 5. By doing 7 | so, you can store your UUIDs as binary values in your database, and still be 8 | able to query using the string representations since the database will take care 9 | of the type conversion. 10 | 11 | As the name suggests, it only supports MySQL. If you're on PostgreSQL, you 12 | can use UUIDs the proper way already. 13 | 14 | If you were to store a UUID of 32 characters (without the dashes) as text in 15 | your database, it would cost you at least 32 bytes. And that only counts if 16 | every character only requires 1 byte. But that completely depends on your 17 | encoding. If every character requires 2 bytes, storing it would already cost 18 | you 64 bytes. And that's a lot, if you think about the fact that a UUID is 19 | only 128 bits. 20 | 21 | Being 128 bits, a UUID fits precisely in a column of 16 bytes. Though it won't 22 | be really readable it sure saves up a lot of space and it's only 4x bigger 23 | than a 32-bit integer, or 2x bigger than a 64-bit integer. 24 | 25 | Not to mention the space you'll be saving when you create an index on the 26 | column holding your UUID. 27 | 28 | # Installation 29 | You know the drill, add this line to your gemfile: 30 | 31 | ``` 32 | gem 'mysql-binuuid-rails' 33 | ``` 34 | 35 | 36 | # Usage 37 | Using binary columns for UUIDs is very easy. There's only two steps you need to 38 | perform which are described here. 39 | 40 | ## Adding the column to store your UUID 41 | Suppose you have a model called `Book` to which you want to add a unique 42 | identifier in the form of a UUID. First, make sure your database is able to 43 | hold this attribute. So let's create a migration. 44 | 45 | ``` 46 | $ rails g migration AddUuidColumnToBooks 47 | ``` 48 | 49 | Open up the migration file and change it as you'd like: 50 | 51 | ```ruby 52 | class AddUuidColumnToBooks < ActiveRecord::Migration[5.1] 53 | def change 54 | # 'uuid' is the column name, and 'binary' is the column type. You have to 55 | # specify it as a binary column yourself. And because we know that a UUID 56 | # takes up 16 bytes, we set can specify its limit. 57 | add_column :books, :uuid, :binary, limit: 16 58 | end 59 | end 60 | ``` 61 | 62 | Perform the migration: 63 | 64 | ``` 65 | rails db:migrate 66 | ``` 67 | 68 | ## Tell your model how to handle the binary UUID column 69 | All you have to do now, is specify in your `Book` model how Rails should handle 70 | the `uuid` column. Open up `app/models/book.rb` and simply add the following 71 | single line: 72 | 73 | ```ruby 74 | class Book < ApplicationRecord 75 | attribute :uuid, MySQLBinUUID::Type.new 76 | end 77 | ``` 78 | 79 | 80 | # Migrating from ActiveUUID 81 | There's a couple of things you need to take into consideration when you're 82 | migrating from ActiveUUID to `mysql-binuuid-rails`. 83 | 84 | ## Replace `include ActiveUUID::UUID` in your models 85 | In your models where you did `include ActiveUUID::UUID`, you now have to 86 | specify the attribute which is a UUID instead: 87 | 88 | ```ruby 89 | class Book < ApplicationRecord 90 | attribute :uuid, MySQLBinUUID::Type.new 91 | end 92 | ``` 93 | 94 | ## No `uuid` column in database migrations 95 | ActiveUUID comes with a neat column type that you can use in migrations. Since 96 | `mysql-binuuid-rails` does not, you will have to change all migrations in which 97 | you leveraged on that migration column if you want your migrations to keep 98 | working for new setups. 99 | 100 | The idea behind *not* providing a `uuid` type for columnns in migrations is 101 | that you are aware of what the actual type of the column is you're creating, 102 | and that it is not hidden magic. 103 | 104 | It's pretty simple: 105 | 106 | 107 | ```ruby 108 | # Anywhere where you did this in your migrations... 109 | 110 | create_table :books do |t| 111 | t.uuid :reference, ... 112 | end 113 | 114 | # ..you should change these kinds of lines into the kind described 115 | # below. It's what ActiveUUID did for you, but what you now have 116 | # to do yourself. 117 | 118 | create_table :books do |t| 119 | t.binary :reference, limit: 16, ... 120 | end 121 | ``` 122 | 123 | ## No UUIDTools 124 | ActiveUUID comes with [UUIDTools](https://github.com/sporkmonger/uuidtools). 125 | `mysql-binuuid-rails` does not. When you retrieve a UUID typed attribute from 126 | a model when using ActiveUUID, the result is a `UUIDTools::UUID` object. When 127 | you retrieve a UUID typed attribute from a model when using 128 | `mysql-binuuid-rails`, you just get a `String` of 36 characters (it includes 129 | the dashes). 130 | 131 | Migrating shouldn't be that difficult though. `UUIDTools::UUID` implements 132 | `#to_s`, which returns precisely the same as `mysql-binuuid-rails` returns 133 | by default. But it's good to be aware of this in case you're running into 134 | weirdness. 135 | 136 | 137 | # Contributing 138 | To start coding on `mysql-binuuid-rails`, fork the project, clone it locally 139 | and then run `bin/setup` to get up and running. If you want to fool around in 140 | a console with the changes you made, run `bin/console`. 141 | 142 | Bug reports and pull requests are welcome on GitHub at 143 | https://github.com/nedap/mysql-binuuid-rails 144 | 145 | ## Testing 146 | For the most recent major version of ActiveRecord, tests are run against the 147 | latest patch level of all minor versions. For earlier major versions, tests are 148 | run against the latest minor/patch. 149 | 150 | Run tests yourself to verify everything is still working: 151 | 152 | ``` 153 | $ bundle exec rake 154 | ``` 155 | 156 | ## Contributors 157 | See [CONTRIBUTORS.md](CONTRIBUTORS.md). 158 | 159 | 160 | # License 161 | The gem is available as open source under the terms of the 162 | [MIT License](http://opensource.org/licenses/MIT). 163 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "active_record/railtie" 5 | require "mysql-binuuid-rails" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/mysql-binuuid-rails.rb: -------------------------------------------------------------------------------- 1 | require 'mysql-binuuid/type' 2 | 3 | ActiveModel::Type.register(:uuid, MySQLBinUUID::Type) 4 | 5 | module MySQLBinUUID 6 | end 7 | -------------------------------------------------------------------------------- /lib/mysql-binuuid/type.rb: -------------------------------------------------------------------------------- 1 | module MySQLBinUUID 2 | class InvalidUUID < StandardError; end 3 | 4 | class Type < ActiveModel::Type::Binary 5 | def type 6 | :uuid 7 | end 8 | 9 | # Invoked when a value that is returned from the database needs to be 10 | # displayed into something readable again. 11 | def cast(value) 12 | if value.is_a?(MySQLBinUUID::Type::Data) 13 | # It could be a Data object, in which case we should add dashes to the 14 | # string value from there. 15 | add_dashes(value.to_s) 16 | elsif value.is_a?(String) && value.encoding == Encoding::ASCII_8BIT && strip_dashes(value).length != 32 17 | # We cannot unpack something that looks like a UUID, with or without 18 | # dashes. Not entirely sure why ActiveRecord does a weird combination of 19 | # cast and serialize before anything needs to be saved.. 20 | undashed_uuid = value.unpack1('H*') 21 | add_dashes(undashed_uuid.to_s) 22 | else 23 | super 24 | end 25 | end 26 | 27 | # Invoked when the provided value needs to be serialized before storing 28 | # it to the database. 29 | def serialize(value) 30 | return if value.nil? 31 | undashed_uuid = strip_dashes(value) 32 | 33 | # To avoid SQL injection, verify that it looks like a UUID. ActiveRecord 34 | # does not explicity escape the Binary data type. escaping is implicit as 35 | # the Binary data type always converts its value to a hex string. 36 | unless valid_undashed_uuid?(undashed_uuid) 37 | raise MySQLBinUUID::InvalidUUID, "#{value} is not a valid UUID" 38 | end 39 | 40 | Data.new(undashed_uuid) 41 | end 42 | 43 | # We're inheriting from the Binary type since ActiveRecord in that case 44 | # will get the hex value. All we need to do to provide the hex value of the 45 | # UUID, is to return the UUID without dashes. And because this inherits 46 | # from Binary::Data, ActiveRecord will quote the hex value as required by 47 | # the database to store it. 48 | class Data < ActiveModel::Type::Binary::Data 49 | def initialize(value) 50 | @value = value 51 | end 52 | 53 | def hex 54 | @value 55 | end 56 | end 57 | 58 | private 59 | 60 | # A UUID consists of 5 groups of characters. 61 | # 8 chars - 4 chars - 4 chars - 4 chars - 12 characters 62 | # 63 | # This function re-introduces the dashes since we removed them during 64 | # serialization, so: 65 | # 66 | # add_dashes("2b4a233152694c6e9d1e098804ab812b") 67 | # => "2b4a2331-5269-4c6e-9d1e-098804ab812b" 68 | # 69 | def add_dashes(uuid) 70 | return uuid if uuid =~ /\-/ 71 | [uuid[0..7], uuid[8..11], uuid[12..15], uuid[16..19], uuid[20..-1]].join("-") 72 | end 73 | 74 | # A UUID has 4 dashes is displayed with 4 dashes at the same place all 75 | # the time. So they don't add anything semantically. We can safely remove 76 | # them before storing to the database, and re-add them whenever we 77 | # retrieved a value from the database. 78 | # 79 | # strip_dashes("2b4a2331-5269-4c6e-9d1e-098804ab812b") 80 | # => "2b4a233152694c6e9d1e098804ab812b" 81 | # 82 | def strip_dashes(uuid) 83 | uuid.delete("-") 84 | end 85 | 86 | # Verify that the undashed version of a UUID only contains characters that 87 | # represent a hexadecimal value. 88 | def valid_undashed_uuid?(value) 89 | value =~ /\A[[:xdigit:]]{32}\z/ 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/mysql-binuuid/version.rb: -------------------------------------------------------------------------------- 1 | module MySQLBinUUID 2 | VERSION = "1.3.0" 3 | end 4 | -------------------------------------------------------------------------------- /mysql-binuuid-rails.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | require "mysql-binuuid/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "mysql-binuuid-rails" 9 | spec.version = MySQLBinUUID::VERSION 10 | spec.authors = ["Mark Oude Veldhuis"] 11 | spec.email = ["mark.oudeveldhuis@nedap.com"] 12 | 13 | spec.summary = "Let ActiveRecord serialize and cast your UUIDs to and from binary columns in your database." 14 | spec.homepage = "https://github.com/nedap/mysql-binuuid-rails" 15 | spec.license = "MIT" 16 | 17 | spec.require_paths = ["lib"] 18 | spec.files = Dir["**/*"].select { |f| File.file?(f) } 19 | .reject { |f| f.end_with?(".gem") } 20 | 21 | spec.required_ruby_version = ">= 2.7" 22 | 23 | spec.add_runtime_dependency "activerecord", ">= 5" 24 | 25 | spec.add_development_dependency "bundler" 26 | spec.add_development_dependency "rake" 27 | spec.add_development_dependency "mysql2" 28 | spec.add_development_dependency "minitest" 29 | spec.add_development_dependency "rails", ">= 5" # required for a console 30 | end 31 | -------------------------------------------------------------------------------- /test/gemfiles/rails-6.1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec path: "../.." 3 | gem "activerecord", "~> 6.1.0" 4 | 5 | gem "bigdecimal" 6 | gem "drb" 7 | gem "mutex_m" 8 | -------------------------------------------------------------------------------- /test/gemfiles/rails-7.0.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec path: "../.." 3 | gem "activerecord", "~> 7.0.0" 4 | 5 | gem "bigdecimal" 6 | gem "drb" 7 | gem "mutex_m" 8 | -------------------------------------------------------------------------------- /test/gemfiles/rails-7.1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec path: "../.." 3 | gem "activerecord", "~> 7.1.0" 4 | -------------------------------------------------------------------------------- /test/gemfiles/rails-8.0.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec path: "../.." 3 | gem "activerecord", "~> 8.0.0" 4 | -------------------------------------------------------------------------------- /test/integration/mysql_integration_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class MyUuidModel < ActiveRecord::Base 4 | attribute :the_uuid, MySQLBinUUID::Type.new 5 | end 6 | 7 | class MyUuidModelWithValidations < MyUuidModel 8 | validates :the_uuid, uniqueness: true 9 | end 10 | 11 | class MySQLIntegrationTest < ActiveSupport::TestCase 12 | def connection 13 | ActiveRecord::Base.connection 14 | end 15 | 16 | def db_config 17 | { 18 | adapter: "mysql2", 19 | host: ENV["MYSQL_HOST"] || "localhost", 20 | username: ENV["MYSQL_USERNAME"] || "root", 21 | password: ENV["MYSQL_PASSWORD"] || "", 22 | database: "binuuid_rails_test" 23 | } 24 | end 25 | 26 | setup do 27 | db_config_without_db_name = db_config.dup 28 | db_config_without_db_name.delete(:database) 29 | 30 | # Create a connection without selecting a database first to create the db 31 | ActiveRecord::Base.establish_connection(db_config_without_db_name) 32 | connection.create_database(db_config[:database], charset: "utf8mb4") 33 | 34 | # Then establish a new connection with the database name 35 | ActiveRecord::Base.establish_connection(db_config) 36 | connection.create_table("my_uuid_models") 37 | connection.add_column("my_uuid_models", "the_uuid", :binary, limit: 16) 38 | 39 | # Uncomment this line to get logging on stdout 40 | # ActiveRecord::Base.logger = Logger.new(STDOUT) 41 | end 42 | 43 | teardown do 44 | connection.drop_database(db_config[:database]) 45 | end 46 | 47 | class BeforePersistedTest < MySQLIntegrationTest 48 | test "does not change the uuid when retrieved without saving" do 49 | sample_uuid = SecureRandom.uuid 50 | my_model = MyUuidModel.new(the_uuid: sample_uuid) 51 | assert_equal sample_uuid, my_model.the_uuid 52 | assert_equal sample_uuid, my_model.attributes["the_uuid"] 53 | end 54 | 55 | test "validates uniqueness" do 56 | uuid = SecureRandom.uuid 57 | MyUuidModelWithValidations.create!(the_uuid: uuid) 58 | duplicate = MyUuidModelWithValidations.new(the_uuid: uuid) 59 | 60 | assert_equal false, duplicate.valid? 61 | assert_equal :taken, duplicate.errors.details[:the_uuid].first[:error] 62 | end 63 | end 64 | 65 | class AfterPersistedTest < MySQLIntegrationTest 66 | setup do 67 | @sample_uuid = SecureRandom.uuid 68 | @my_model = MyUuidModel.create!(the_uuid: @sample_uuid) 69 | end 70 | 71 | teardown do 72 | MyUuidModel.delete_all 73 | end 74 | 75 | test "stores a binary value in the database" do 76 | raw_value = connection.execute("SELECT * FROM my_uuid_models").to_a.first[1] 77 | assert_equal raw_value.encoding, Encoding::ASCII_8BIT 78 | end 79 | 80 | test "stores a binary value without dashes" do 81 | raw_value = connection.execute("SELECT * FROM my_uuid_models").to_a.first[1] 82 | 83 | # Create a version without dashes of the sample uuid 84 | sample_uuid_no_dashes = @sample_uuid.delete("-") 85 | 86 | # Put it in an array so we can create the binary representation we 87 | # also get from the database. 88 | assert_equal [sample_uuid_no_dashes].pack("H*"), raw_value 89 | end 90 | 91 | test "can be found using .find_by" do 92 | find_result = MyUuidModel.find_by(the_uuid: @sample_uuid) 93 | assert_equal find_result, @my_model 94 | assert_equal find_result.the_uuid, @sample_uuid 95 | end 96 | 97 | test "can be found using .where" do 98 | results = MyUuidModel.where(the_uuid: @sample_uuid) 99 | assert_equal results.count, 1 100 | assert_equal results.first, @my_model 101 | assert_equal results.first.the_uuid, @sample_uuid 102 | end 103 | 104 | test "can't be used to inject SQL using .where" do 105 | # In Rails 7.1, the error gets wrapped in an ActiveRecord::StatementInvalid. 106 | expected_error = ActiveRecord.version.to_s.start_with?("7.1") ? ActiveRecord::StatementInvalid : MySQLBinUUID::InvalidUUID 107 | assert_raises(expected_error) do 108 | MyUuidModel.where(the_uuid: "' OR ''='").first 109 | end 110 | end 111 | 112 | test "can't be used to inject SQL using .find_by" do 113 | assert_raises MySQLBinUUID::InvalidUUID do 114 | MyUuidModel.find_by(the_uuid: "' OR ''='") 115 | end 116 | end 117 | 118 | test "can't be used to inject SQL while creating" do 119 | assert_raises MySQLBinUUID::InvalidUUID do 120 | MyUuidModel.create!(the_uuid: "40' + x'40") 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /test/mysql-binuuid/type_data_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | module MySQLBinUUID 4 | class Type 5 | class DataTest < ActiveSupport::TestCase 6 | test "is of kind ActiveModel::Type::Binary::Data" do 7 | assert_kind_of ActiveModel::Type::Binary::Data, MySQLBinUUID::Type::Data.new(nil) 8 | end 9 | 10 | test "returns the raw value as hex value" do 11 | assert_equal "e7db0d1a", MySQLBinUUID::Type::Data.new("e7db0d1a").hex 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/mysql-binuuid/type_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | module MySQLBinUUID 4 | class TypeTest < ActiveSupport::TestCase 5 | setup do 6 | @type = MySQLBinUUID::Type.new 7 | end 8 | 9 | test "#type reports :uuid as its type" do 10 | assert_equal :uuid, @type.type 11 | end 12 | 13 | test "#cast returns a dashed uuid if provided with a MySQLBinUUID::Type::Data" do 14 | uuid = "c5997c21-3355-4603-9e41-4fdc7194fe2d" 15 | data = MySQLBinUUID::Type::Data.new(uuid) 16 | 17 | assert_equal uuid, @type.cast(data) 18 | end 19 | 20 | test "#cast returns a dashed uuid if provided with a binary string" do 21 | uuid = "6d7c7ff2-dca8-45eb-b3a0-3b9a24a5270e" 22 | binstring = [uuid.delete("-")].pack("H*") 23 | binstring.force_encoding("ASCII-8BIT") 24 | assert_equal uuid, @type.cast(binstring) 25 | end 26 | 27 | test "#cast returns the value itself if provided with something else" do 28 | assert_equal 42, @type.cast(42) 29 | end 30 | 31 | test "#cast returns a uuid if provided with a uuid" do 32 | uuid = SecureRandom.uuid.encode(Encoding::ASCII_8BIT) 33 | data = MySQLBinUUID::Type.new.cast(uuid) 34 | 35 | assert_equal uuid, @type.cast(data) 36 | end 37 | 38 | test "#serialize returns nil if provided with nil (touché)" do 39 | assert_nil @type.serialize(nil) 40 | end 41 | 42 | test "#serialize returns a MySQLBinUUID::Type::Data with stripped values if provided with a UUID" do 43 | uuid = "3511f33f-3c93-4806-9846-52b4a7618298" 44 | 45 | assert_instance_of MySQLBinUUID::Type::Data, @type.serialize(uuid) 46 | assert_equal "3511f33f3c934806984652b4a7618298", @type.serialize(uuid).to_s 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "minitest/pride" 3 | 4 | require "active_record" 5 | require "securerandom" 6 | 7 | require_relative "../lib/mysql-binuuid-rails" 8 | require_relative "../lib/mysql-binuuid/type" 9 | --------------------------------------------------------------------------------