activeid 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +21 -0
  3. data/.github/workflows/macos.yml +45 -0
  4. data/.github/workflows/ubuntu.yml +47 -0
  5. data/.github/workflows/windows.yml +40 -0
  6. data/.gitignore +195 -0
  7. data/.hound.yml +3 -0
  8. data/.rubocop.yml +18 -0
  9. data/Gemfile +7 -0
  10. data/LICENSE.md +19 -0
  11. data/README.adoc +411 -0
  12. data/Rakefile +27 -0
  13. data/activeid.gemspec +42 -0
  14. data/examples/name_based_uuids.rb +92 -0
  15. data/examples/registering_active_record_type.rb +74 -0
  16. data/examples/storing_uuids_as_binaries.rb +88 -0
  17. data/examples/storing_uuids_as_strings.rb +81 -0
  18. data/examples/storing_uuids_natively.rb +93 -0
  19. data/examples/time_based_uuids.rb +58 -0
  20. data/examples/using_migrations.rb +50 -0
  21. data/gemfiles/Rails-5_0.gemfile +8 -0
  22. data/gemfiles/Rails-5_1.gemfile +8 -0
  23. data/gemfiles/Rails-5_2.gemfile +8 -0
  24. data/gemfiles/Rails-head.gemfile +8 -0
  25. data/lib/active_id.rb +12 -0
  26. data/lib/active_id/all.rb +2 -0
  27. data/lib/active_id/connection_patches.rb +65 -0
  28. data/lib/active_id/model.rb +55 -0
  29. data/lib/active_id/railtie.rb +12 -0
  30. data/lib/active_id/type.rb +100 -0
  31. data/lib/active_id/utils.rb +77 -0
  32. data/lib/active_id/version.rb +3 -0
  33. data/spec/integration/examples_for_uuid_models.rb +92 -0
  34. data/spec/integration/examples_for_uuid_models_having_namespaces.rb +12 -0
  35. data/spec/integration/examples_for_uuid_models_having_natural_keys.rb +11 -0
  36. data/spec/integration/migrations_spec.rb +92 -0
  37. data/spec/integration/model_without_uuids_spec.rb +44 -0
  38. data/spec/integration/no_patches_spec.rb +26 -0
  39. data/spec/integration/storing_uuids_as_binaries_spec.rb +34 -0
  40. data/spec/integration/storing_uuids_as_strings_spec.rb +22 -0
  41. data/spec/spec_helper.rb +64 -0
  42. data/spec/support/0_logger.rb +2 -0
  43. data/spec/support/1_db_connection.rb +3 -0
  44. data/spec/support/2_db_cleaner.rb +14 -0
  45. data/spec/support/database.yml +12 -0
  46. data/spec/support/fabricators.rb +15 -0
  47. data/spec/support/models.rb +41 -0
  48. data/spec/support/schema.rb +38 -0
  49. data/spec/unit/attribute_type_spec.rb +70 -0
  50. data/spec/unit/utils_spec.rb +97 -0
  51. metadata +313 -0
@@ -0,0 +1,58 @@
1
+ # Time-based UUIDs (version 1) store timestamp of their creation, and are
2
+ # monotonically increasing in time. This is very advantageous in some
3
+ # use cases.
4
+
5
+ ENV["DB"] ||= "sqlite3"
6
+
7
+ require "bundler/setup"
8
+ Bundler.require :development
9
+
10
+ require "active_id"
11
+ require_relative "../spec/support/0_logger"
12
+ require_relative "../spec/support/1_db_connection"
13
+
14
+ #### SCHEMA ####
15
+
16
+ ActiveRecord::Schema.define do
17
+ create_table :authors, id: false, force: true do |t|
18
+ t.string :id, limit: 36, primary_key: true
19
+ t.string :name
20
+ t.timestamps
21
+ end
22
+ end
23
+
24
+ #### MODELS ####
25
+
26
+ class Author < ActiveRecord::Base
27
+ include ActiveID::Model
28
+ attribute :id, ActiveID::Type::StringUUID.new
29
+ uuid_generator :time
30
+ end
31
+
32
+ #### PROOF ####
33
+
34
+ SolidAssert.enable_assertions
35
+
36
+ poe = Author.create! name: "Edgar Alan Poe"
37
+ thu = Author.create! name: "Thucydides"
38
+ fon = Author.create! name: "Jean de La Fontaine"
39
+ kas = Author.create! name: "Jan Kasprowicz"
40
+
41
+ # Version 1 means time-based UUIDs
42
+ assert poe.id.version == 1
43
+
44
+ # Timestamp can be extracted from UUID
45
+ assert poe.id.timestamp.between? 1.minute.ago, 1.minute.from_now
46
+
47
+ # Lexicographical ordering of version 1 UUIDs reflects their temporal ordering
48
+ assert Author.all.order(id: :asc).to_a == [poe, thu, fon, kas]
49
+
50
+ #### PROVE THAT ASSERTIONS WERE WORKING ####
51
+
52
+ begin
53
+ assert 1 == 2
54
+ rescue SolidAssert::AssertionFailedError
55
+ puts "All OK."
56
+ else
57
+ raise "Assertions do not work!"
58
+ end
@@ -0,0 +1,50 @@
1
+ # Active UUID features a convenience #uuid method, which may be used to create
2
+ # a binary column in database migration. Since it involves monkey patching,
3
+ # "active_id/all" must be loaded.
4
+
5
+ ENV["DB"] ||= "sqlite3"
6
+
7
+ require "bundler/setup"
8
+ Bundler.require :development
9
+
10
+ # Note "active_id/all", which registers new column definitions!
11
+ require "active_id/all"
12
+
13
+ require_relative "../spec/support/0_logger"
14
+ require_relative "../spec/support/1_db_connection"
15
+
16
+ #### SCHEMA ####
17
+
18
+ ActiveRecord::Schema.define do
19
+ create_table :authors, id: false, force: true do |t|
20
+ t.uuid :id, primary_key: true
21
+ t.string :name
22
+ t.timestamps
23
+ end
24
+ end
25
+
26
+ #### PROOF ####
27
+
28
+ SolidAssert.enable_assertions
29
+
30
+ id_column = ActiveRecord::Base.connection.columns("authors")[0]
31
+ assert id_column.name == "id"
32
+
33
+ case ENV["DB"]
34
+ when "sqlite3"
35
+ assert id_column.sql_type == "binary(16)"
36
+ when "mysql"
37
+ assert id_column.sql_type == "varbinary(16)"
38
+ when "postgresql"
39
+ assert id_column.sql_type == "uuid"
40
+ end
41
+
42
+ #### PROVE THAT ASSERTIONS WERE WORKING ####
43
+
44
+ begin
45
+ assert 1 == 2
46
+ rescue SolidAssert::AssertionFailedError
47
+ puts "All OK."
48
+ else
49
+ raise "Assertions do not work!"
50
+ end
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activerecord", "~> 5.0.0"
6
+
7
+ gem "codecov", require: false, group: :test
8
+ gem "simplecov", require: false, group: :test
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activerecord", "~> 5.1.0"
6
+
7
+ gem "codecov", require: false, group: :test
8
+ gem "simplecov", require: false, group: :test
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activerecord", "~> 5.2.0"
6
+
7
+ gem "codecov", require: false, group: :test
8
+ gem "simplecov", require: false, group: :test
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activerecord", github: "rails/rails"
6
+
7
+ gem "codecov", require: false, group: :test
8
+ gem "simplecov", require: false, group: :test
@@ -0,0 +1,12 @@
1
+ require "active_id/version"
2
+ require "active_id/utils"
3
+ require "active_id/model"
4
+ require "active_id/type"
5
+ require "active_id/railtie" if defined?(Rails::Railtie)
6
+ require "pp"
7
+
8
+ module ActiveID
9
+ class << self
10
+ delegate :quote_as_binary, to: Utils
11
+ end
12
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "../active_id"
2
+ require_relative "connection_patches"
@@ -0,0 +1,65 @@
1
+ require "active_record"
2
+ require "active_support/concern"
3
+
4
+ module ActiveID
5
+ module ConnectionPatches
6
+ module ColumnMethods
7
+ def uuid(*args, **options)
8
+ args.each { |name| column(name, :uuid, options) }
9
+ end
10
+ end
11
+
12
+ module Quoting
13
+ extend ActiveSupport::Concern
14
+
15
+ def self.prepended(_klass)
16
+ def native_database_types
17
+ super.merge(uuid: { name: "binary", limit: 16 })
18
+ end
19
+ end
20
+ end
21
+
22
+ module PostgreSQLQuoting
23
+ extend ActiveSupport::Concern
24
+
25
+ def self.prepended(_klass)
26
+ def native_database_types
27
+ super.merge(uuid: { name: "uuid" })
28
+ end
29
+ end
30
+ end
31
+
32
+ module ConnectionHandling
33
+ def establish_connection(*_args) # rubocop:disable Metrics/MethodLength
34
+ super
35
+
36
+ arca = ActiveRecord::ConnectionAdapters
37
+
38
+ arca::Table.include(ColumnMethods)
39
+ arca::TableDefinition.include(ColumnMethods)
40
+
41
+ if defined? arca::MysqlAdapter
42
+ arca::MysqlAdapter.prepend(Quoting)
43
+ end
44
+
45
+ if defined? arca::Mysql2Adapter
46
+ arca::Mysql2Adapter.prepend(Quoting)
47
+ end
48
+
49
+ if defined? arca::SQLite3Adapter
50
+ arca::SQLite3Adapter.prepend(Quoting)
51
+ end
52
+
53
+ if defined? arca::PostgreSQLAdapter
54
+ arca::PostgreSQLAdapter.prepend(PostgreSQLQuoting)
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.apply!
60
+ ActiveRecord::Base.singleton_class.prepend ConnectionHandling
61
+ end
62
+ end
63
+ end
64
+
65
+ ActiveID::ConnectionPatches.apply!
@@ -0,0 +1,55 @@
1
+ require "active_support"
2
+
3
+ module ActiveID
4
+ # Include this module into all models which are meant to store UUIDs.
5
+ module Model
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :_natural_key, instance_writer: false
10
+ class_attribute :_uuid_namespace, instance_writer: false
11
+ class_attribute :_uuid_generator, instance_writer: false
12
+ self._uuid_generator = :random
13
+
14
+ before_create :generate_uuids_if_needed
15
+ end
16
+
17
+ module ClassMethods
18
+ def natural_key(*attributes)
19
+ self._natural_key = attributes
20
+ end
21
+
22
+ def uuid_namespace(namespace)
23
+ namespace = Utils.cast_to_uuid(namespace)
24
+ self._uuid_namespace = namespace
25
+ end
26
+
27
+ def uuid_generator(generator_name)
28
+ self._uuid_generator = generator_name
29
+ end
30
+ end
31
+
32
+ def create_uuid
33
+ if _natural_key
34
+ # TODO if all the attributes return nil you might want to warn about this
35
+ chained = _natural_key.map { |attribute| send(attribute) }.join("-")
36
+ UUIDTools::UUID.sha1_create(_uuid_namespace || UUIDTools::UUID_OID_NAMESPACE, chained)
37
+ else
38
+ case _uuid_generator
39
+ when :random
40
+ UUIDTools::UUID.random_create
41
+ when :time
42
+ UUIDTools::UUID.timestamp_create
43
+ end
44
+ end
45
+ end
46
+
47
+ def generate_uuids_if_needed
48
+ primary_key = self.class.primary_key
49
+ primary_key_attribute_type = self.class.type_for_attribute(primary_key)
50
+ if ::ActiveID::Type::Base === primary_key_attribute_type
51
+ send("#{primary_key}=", create_uuid) unless send("#{primary_key}?")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,12 @@
1
+ require "active_id"
2
+ require "rails"
3
+
4
+ module ActiveID
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :activeid
7
+
8
+ config.to_prepare do
9
+ ActiveID::ConnectionPatches.apply!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,100 @@
1
+ require "active_record"
2
+
3
+ module ActiveID
4
+ # ActiveRecord's attribute types for serializing UUIDs.
5
+ #
6
+ # ==== Examples
7
+ #
8
+ # class Book
9
+ # attribute :id, ActiveID::Type::BinaryUUID.new
10
+ # end
11
+ #
12
+ # ==== See also
13
+ #
14
+ # * docs for +::ActiveRecord::Attributes::ClassMethods#attribute+
15
+ # * docs for +::ActiveRecord::Type::Value+
16
+ module Type
17
+ # See subclasses.
18
+ #
19
+ # @abstract Subclasses should define at least +instantiate_storage+ method.
20
+ class Base < ::ActiveRecord::Type::Value
21
+ attr_reader :storage_type
22
+
23
+ delegate :cast_to_uuid, to: ActiveID::Utils
24
+ delegate :serialize, :deserialize, to: :storage_type, prefix: :s
25
+
26
+ def initialize
27
+ @storage_type = instantiate_storage
28
+ end
29
+
30
+ # Converts binary values into UUIDs.
31
+ def deserialize(value)
32
+ cast_to_uuid(s_deserialize(value))
33
+ end
34
+
35
+ # Converts strings into UUIDs on user input assignment, called internally
36
+ # from #cast.
37
+ def cast_value(value)
38
+ cast_to_uuid(value)
39
+ end
40
+ end
41
+
42
+ # ActiveRecord's attribute type which serializes UUIDs as binaries. Useful
43
+ # for RDBSes which do not support UUIDs natively (i.e. MySQL, SQLite3).
44
+ #
45
+ # UUIDs serialized as binaries are more space efficient (16 bytes vs
46
+ # 36 characters of their text representation), which may also lead to
47
+ # performance boost if given column is indexed (a bigger piece of index can
48
+ # be kept in memory). The downside is that this representation is less
49
+ # readable for humans who access serialized values outside Rails
50
+ # (i.e. in a database console).
51
+ #
52
+ # ==== Accessing in database console
53
+ #
54
+ # In MySQL (but not in MariaDB), there is
55
+ # a {+BIN_TO_UUID()+}[https://mysqlserverteam.com/mysql-8-0-uuid-support/]
56
+ # function which converts binaries to UUID strings.
57
+ # There is {a feature request}[https://jira.mariadb.org/browse/MDEV-15854]
58
+ # in MariaDB's issue tracker to add a similar feature.
59
+ #
60
+ # ==== Caveat
61
+ #
62
+ # Does not work with PostgreSQL adapter. Nevertheless, there should not be
63
+ # any good reason to use {BinaryUUID} with PostgreSQL. Open a feature
64
+ # request if you find any.
65
+ #
66
+ # In PostgreSQL, {StringUUID} attribute type is recommended as it is
67
+ # compatible with Postgres-specific +UUID+ data type.
68
+ class BinaryUUID < Base
69
+ def serialize(value)
70
+ s_serialize(cast_to_uuid(value)&.raw)
71
+ end
72
+
73
+ protected
74
+
75
+ def instantiate_storage
76
+ ::ActiveRecord::Type::Binary.new
77
+ end
78
+ end
79
+
80
+ # ActiveRecord's attribute type which serializes UUIDs as strings.
81
+ #
82
+ # UUIDs are serialized as 36 characters long strings
83
+ # (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`). In PostgreSQL, this
84
+ # representation is compatible with +UUID+ data type, which is a unique
85
+ # feature of this RDBS. In other RDBSes, this attribute type can be
86
+ # used with textual data types (e.g. +VARCHAR(36)+), however {BinaryUUID}
87
+ # should be preferred when performance matters.
88
+ class StringUUID < Base
89
+ def serialize(value)
90
+ s_serialize(cast_to_uuid(value)&.to_s)
91
+ end
92
+
93
+ protected
94
+
95
+ def instantiate_storage
96
+ ::ActiveRecord::Type::String.new
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,77 @@
1
+ require "uuidtools"
2
+
3
+ module ActiveID
4
+ # Variety of convenience functions.
5
+ module Utils
6
+ module_function
7
+
8
+ # Casts +value+ to +UUIDTools::UUID+.
9
+ #
10
+ # When an instance of +UUIDTools::UUID+ or +nil+ is given, then this method
11
+ # returns that argument. When a +String+ is given, then it is parsed,
12
+ # and respective instance of UUIDTools::UUID is returned.
13
+ #
14
+ # A variety of string formats is recognized:
15
+ #
16
+ # - 36 characters long strings (hexadecimal number interpolated with dashes)
17
+ # - 32 characters long strings (hexadecimal number without dashes)
18
+ # - 16 bytes long strings (binary representation of UUID)
19
+ #
20
+ # @param value [UUIDTools::UUID, String, nil] value to be casted
21
+ # to +UUIDTools::UUID+.
22
+ # @raise [ArgumentError] argument cannot be casted to UUIDTools::UUID.
23
+ # @return [UUIDTools::UUID, nil] respective UUID instance or nil.
24
+ def cast_to_uuid(value)
25
+ case value
26
+ when UUIDTools::UUID, nil
27
+ value
28
+ when String
29
+ parse_uuid_string(value)
30
+ else
31
+ m = "UUID, String, or nil required, but '#{value.inspect}' was given"
32
+ raise ArgumentError, m
33
+ end
34
+ end
35
+
36
+ # Casts UUID to binary and quotes it, so that it can be used in SQL query
37
+ # interpolation.
38
+ #
39
+ # model.where("id = ?", ActiveID.quote_as_binary(some_uuid))
40
+ #
41
+ # This method is unable to determine the correct attribute type.
42
+ # It always casts UUIDs to their binary form, which may be unwanted in some
43
+ # contexts, i.e. in case of UUIDs which are meant to be serialized as
44
+ # strings or as Postgres' native +UUID+ data type. Due to this fact,
45
+ # it is generally recommended to avoid SQL query interpolation if possible.
46
+ #
47
+ # @param value [UUIDTools::UUID, String, nil] UUID or its representation
48
+ # to be quoted.
49
+ # @raise [ArgumentError] see cast_to_uuid.
50
+ # @return [::ActiveRecord::Type::Binary::Data, nil] a binary value which
51
+ # can be used in SQL queries.
52
+ def quote_as_binary(value)
53
+ uuid = cast_to_uuid(value)
54
+ uuid && ::ActiveRecord::Type::Binary::Data.new(uuid.raw)
55
+ end
56
+
57
+ # :nodoc:
58
+ # Unfortunately, UUIDTools is missing some validations, hence we have to do
59
+ # them here.
60
+ #
61
+ # TODO More validations, see specs.
62
+ def self.parse_uuid_string(str)
63
+ case str.size
64
+ when 16 then uuid = UUIDTools::UUID.parse_raw(str)
65
+ when 32 then uuid = UUIDTools::UUID.parse_hexdigest(str)
66
+ when 36 then uuid = UUIDTools::UUID.parse(str)
67
+ end
68
+ ensure
69
+ unless uuid # Guard for both exceptions and nil return values
70
+ raise ArgumentError, "Expected string which is 16, 32, or 36 " +
71
+ "characters long, and represents UUID, but #{str.inspect} was given"
72
+ end
73
+ end
74
+
75
+ private_class_method :parse_uuid_string
76
+ end
77
+ end