activeid 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|