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.
- checksums.yaml +7 -0
- data/.editorconfig +21 -0
- data/.github/workflows/macos.yml +45 -0
- data/.github/workflows/ubuntu.yml +47 -0
- data/.github/workflows/windows.yml +40 -0
- data/.gitignore +195 -0
- data/.hound.yml +3 -0
- data/.rubocop.yml +18 -0
- data/Gemfile +7 -0
- data/LICENSE.md +19 -0
- data/README.adoc +411 -0
- data/Rakefile +27 -0
- data/activeid.gemspec +42 -0
- data/examples/name_based_uuids.rb +92 -0
- data/examples/registering_active_record_type.rb +74 -0
- data/examples/storing_uuids_as_binaries.rb +88 -0
- data/examples/storing_uuids_as_strings.rb +81 -0
- data/examples/storing_uuids_natively.rb +93 -0
- data/examples/time_based_uuids.rb +58 -0
- data/examples/using_migrations.rb +50 -0
- data/gemfiles/Rails-5_0.gemfile +8 -0
- data/gemfiles/Rails-5_1.gemfile +8 -0
- data/gemfiles/Rails-5_2.gemfile +8 -0
- data/gemfiles/Rails-head.gemfile +8 -0
- data/lib/active_id.rb +12 -0
- data/lib/active_id/all.rb +2 -0
- data/lib/active_id/connection_patches.rb +65 -0
- data/lib/active_id/model.rb +55 -0
- data/lib/active_id/railtie.rb +12 -0
- data/lib/active_id/type.rb +100 -0
- data/lib/active_id/utils.rb +77 -0
- data/lib/active_id/version.rb +3 -0
- data/spec/integration/examples_for_uuid_models.rb +92 -0
- data/spec/integration/examples_for_uuid_models_having_namespaces.rb +12 -0
- data/spec/integration/examples_for_uuid_models_having_natural_keys.rb +11 -0
- data/spec/integration/migrations_spec.rb +92 -0
- data/spec/integration/model_without_uuids_spec.rb +44 -0
- data/spec/integration/no_patches_spec.rb +26 -0
- data/spec/integration/storing_uuids_as_binaries_spec.rb +34 -0
- data/spec/integration/storing_uuids_as_strings_spec.rb +22 -0
- data/spec/spec_helper.rb +64 -0
- data/spec/support/0_logger.rb +2 -0
- data/spec/support/1_db_connection.rb +3 -0
- data/spec/support/2_db_cleaner.rb +14 -0
- data/spec/support/database.yml +12 -0
- data/spec/support/fabricators.rb +15 -0
- data/spec/support/models.rb +41 -0
- data/spec/support/schema.rb +38 -0
- data/spec/unit/attribute_type_spec.rb +70 -0
- data/spec/unit/utils_spec.rb +97 -0
- 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
|
data/lib/active_id.rb
ADDED
@@ -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,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,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
|