|
require 'bundler/inline' |
|
|
|
## |
|
# Get dependencies |
|
gemfile do |
|
source 'https://rubygems.org' |
|
gem 'pg' |
|
gem 'activerecord', require: 'active_record' |
|
gem 'benchmark-ips' |
|
gem 'pry' |
|
gem 'minitest' |
|
end |
|
|
|
## |
|
# Ensure DB exists |
|
ActiveRecord::Base.yield_self do |base| |
|
base.establish_connection( |
|
adapter: 'postgresql', |
|
host: 'localhost', |
|
database: 'postgres' |
|
) |
|
begin |
|
base.connection.create_database('test_stuff') |
|
rescue ActiveRecord::DatabaseAlreadyExists |
|
nil |
|
end |
|
base.establish_connection( |
|
adapter: 'postgresql', |
|
host: 'localhost', |
|
database: 'test_stuff' |
|
) |
|
end |
|
|
|
## |
|
# Define the schema |
|
ActiveRecord::Schema.define do |
|
create_table('side_a', id: :bigint, primary_key: :id, force: true) do |t| |
|
t.string 'name' |
|
t.string 'version' |
|
end |
|
create_table('side_b', id: :bigint, primary_key: :id, force: true) do |t| |
|
t.string 'name' |
|
t.string 'version' |
|
t.integer 'version_major' |
|
t.integer 'version_minor' |
|
t.integer 'version_patch' |
|
end |
|
create_table('side_c', id: :bigint, primary_key: :id, force: true) do |t| |
|
t.string 'name' |
|
t.string 'version' |
|
t.integer 'version_major' |
|
t.integer 'version_minor' |
|
t.integer 'version_patch' |
|
end |
|
create_table('side_d', id: :bigint, primary_key: :id, force: true) do |t| |
|
t.string 'name' |
|
t.string 'version' |
|
t.integer 'version_arr', array: true |
|
end |
|
|
|
create_table('side_e', id: :bigint, primary_key: :id, force: true) do |t| |
|
t.string 'name' |
|
t.string 'version' |
|
t.integer 'version_arr', array: true |
|
end |
|
|
|
create_table('side_f', id: :bigint, primary_key: :id, force: true) do |t| |
|
t.string 'name' |
|
t.integer 'version' |
|
end |
|
|
|
add_index :side_a, :version |
|
add_index :side_b, [:version_major, :version_minor, :version_patch], name: :version |
|
add_index :side_c, [:version_patch, :version_minor, :version_major], name: :version_rev |
|
add_index :side_e, [:version_arr] |
|
add_index :side_f, :version |
|
end |
|
|
|
class SideA < ActiveRecord::Base |
|
self.table_name = 'side_a' |
|
|
|
scope :ordered, -> { order(Arel.sql("string_to_array(version, '.')::int[]")) } |
|
end |
|
class SideB < ActiveRecord::Base |
|
self.table_name = 'side_b' |
|
|
|
scope :ordered, -> { order('version_major ASC, version_minor ASC, version_patch ASC') } |
|
def version=(value) |
|
super |
|
self.version_major, self.version_minor, self.version_patch = *value.split('.') |
|
end |
|
end |
|
class SideC < ActiveRecord::Base |
|
self.table_name = 'side_c' |
|
scope :ordered, -> { order('version_major ASC, version_minor ASC, version_patch ASC') } |
|
def version=(value) |
|
super |
|
self.version_major, self.version_minor, self.version_patch = *value.split('.') |
|
end |
|
end |
|
class SideD < ActiveRecord::Base |
|
self.table_name = 'side_d' |
|
scope :ordered, -> { order('version_arr ASC') } |
|
def version=(value) |
|
super |
|
self.version_arr = *value.split('.') |
|
end |
|
end |
|
|
|
class SideE < ActiveRecord::Base |
|
self.table_name = 'side_e' |
|
scope :ordered, -> { order('version_arr ASC') } |
|
def version=(value) |
|
super |
|
self.version_arr = *value.split('.') |
|
end |
|
end |
|
|
|
## |
|
# Define a custom attribute type to encode Semver Strings as zero-padded integers |
|
# e.g. "1.23.56" => 001023056 |
|
class SemverType < ActiveRecord::Type::Value |
|
def serialize(value) |
|
return value if value.is_a?(Integer) |
|
value.to_s.split('.').map { _1.rjust(3, '0') }.join.to_i |
|
end |
|
def deserialize(value) |
|
value.to_s.rjust(9, '0').chars.each_slice(3).map(&:join).map(&:to_i).join('.') |
|
end |
|
def type |
|
:integer |
|
end |
|
def assert_valid_value(value) |
|
value.to_s.match(/\d{1,3}\.\d{1,3}\.\d{1,3}/) |
|
end |
|
end |
|
ActiveRecord::Type.register(:semver, SemverType) |
|
|
|
class SideF < ActiveRecord::Base |
|
self.table_name = 'side_f' |
|
attribute :version, :semver |
|
scope :ordered, -> { order('version') } |
|
end |
|
|
|
## |
|
# Build a bunch of fake data |
|
maj_versions = (0..2).to_a |
|
min_versions = (0..12).to_a |
|
patch_versions = (0..15).to_a |
|
all_version_strings = maj_versions.product(min_versions, patch_versions).map { _1.join('.') } |
|
|
|
document_names = 10.times.map.with_index(1) { "Document #{_2}" } |
|
names_and_versions = document_names.product(all_version_strings) |
|
attributes = names_and_versions.map do |(name, version_string)| |
|
{ |
|
name: name, |
|
version: version_string, |
|
} |
|
end |
|
## |
|
# Populate the tables |
|
puts "populating side_a" |
|
SideA.create(attributes) |
|
puts "populating side_b" |
|
SideB.create(attributes) |
|
puts "populating side_c" |
|
SideC.create(attributes) |
|
puts "populating side_d" |
|
SideD.create(attributes) |
|
puts "populating side_e" |
|
SideE.create(attributes) |
|
puts "populating side_f" |
|
SideF.create(attributes) |
|
|
|
work = -> (klass) { |
|
klass.ordered.limit(100).load |
|
klass.ordered.count |
|
} |
|
## |
|
# Run the benchmark |
|
Benchmark.ips do |test| |
|
test.report('A: Indexed String') { work.(SideA) } |
|
test.report('B: Split index maj,min,patch') { work.(SideB) } |
|
test.report('C: Split index patch,min,max') { work.(SideC) } |
|
test.report('D: Integer Array') { work.(SideD) } |
|
test.report('E: Integer Array indexed') { work.(SideE) } |
|
test.report('F: Padded integer') { work.(SideF) } |
|
end |
|
|
|
ActiveRecord::Base.connection.yield_self do |connection| |
|
connection.disconnect! |
|
end |