surus 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -4,24 +4,28 @@ Surus
4
4
  # Description
5
5
 
6
6
  Surus extends ActiveRecord with PostgreSQL specific functionality. It includes
7
- hstore and array serializers and helper scopes.
7
+ hstore and array serializers and helper scopes. It also includes a helper to
8
+ control PostgreSQL synchronous commit behavior.
8
9
 
9
10
  # Installation
10
11
 
11
12
  gem install surus
12
13
 
13
- or add to your Gemfile
14
+ Or add to your Gemfile.
14
15
 
15
16
  gem 'surus'
16
17
 
17
18
  # Hstore
18
19
 
19
- Hashes can be serialized to an hstore column.
20
+ Hashes can be serialized to an hstore column. hstore is a PostgreSQL key/value
21
+ type that can be indexed for fast searching.
20
22
 
21
23
  class User < ActiveRecord::Base
22
24
  serialize :properties, Surus::Hstore::Serializer.new
23
25
  end
24
26
 
27
+ User.create :properties => { :favorite_color => "green", :results_per_page => 20 }
28
+
25
29
  Even though the underlying hstore can only use strings for keys and values
26
30
  (and NULL for values) Surus can successfully maintain type for integers,
27
31
  floats, bigdecimals, and dates. It does this by storing an extra key
@@ -34,6 +38,9 @@ Hstores can be searched with helper scopes.
34
38
  User.hstore_has_all_keys(:properties, "favorite_color", "gender")
35
39
  User.hstore_has_any_keys(:properties, "favorite_color", "favorite_artist")
36
40
 
41
+
42
+ Read more in the [PostgreSQL hstore documentation](http://www.postgresql.org/docs/9.1/static/hstore.html).
43
+
37
44
  # Array
38
45
 
39
46
  Ruby arrays can be serialized to PostgreSQL arrays. Surus includes support
@@ -46,12 +53,99 @@ for text, integer, float, and decimal arrays.
46
53
  serialize :favorite_decimals, Surus::Array::DecimalSerializer.new
47
54
  end
48
55
 
56
+ User.create :permissions => %w{ read_notes write_notes, manage_topics },
57
+ :favorite_integers => [1, 2, 3],
58
+ :favorite_floats => [1.3, 2.2, 3.1],
59
+ :favorite_decimals => [BigDecimal("3.14"), BigDecimal("4.23"]
60
+
49
61
  Arrays can be searched with helper scopes.
50
62
 
51
63
  User.array_has(:permissions, "admin")
52
64
  User.array_has(:permissions, "manage_accounts", "manage_users")
53
65
  User.array_has_any(:favorite_integers, 7, 11, 42)
54
66
 
67
+ # Synchronous Commit
68
+
69
+ PostgreSQL can trade durability for speed. By disabling synchronous commit,
70
+ transactions will return before the data is actually stored on the disk. This
71
+ can be substantially faster, but it entails a short window where a crash
72
+ could cause data loss (but not data corruption). This can be enabled for an
73
+ entire session or per transaction.
74
+
75
+ User.synchronous_commit # -> true
76
+
77
+ User.transaction do
78
+ User.synchronous_commit false
79
+ @user.save
80
+ end # This transaction can return before the data is written to the drive
81
+
82
+ # synchronous_commit returns to its former value outside of the transaction
83
+ User.synchronous_commit # -> true
84
+
85
+ # synchronous_commit can be turned off permanently
86
+ User.synchronous_commit false
87
+
88
+ Read more in the [PostgreSQL asynchronous commit documentation](http://www.postgresql.org/docs/9.1/interactive/wal-async-commit.html).
89
+
90
+ # Benchmarks
91
+
92
+ Using PostgreSQL's hstore enables searches to be done quickly in the database.
93
+
94
+ jack@moya:~/work/surus$ ruby -I lib -I bench bench/hstore_find.rb
95
+ Skipping EAV test. Use -e to enable (VERY SLOW!)
96
+ Skipping YAML test. Use -y to enable (VERY SLOW!)
97
+ Creating Surus test data... Done.
98
+
99
+ 2000 records with 5 string key/value pairs
100
+ Finding all by inclusion of a key 200 times
101
+ user system total real
102
+ Surus 0.120000 0.030000 0.150000 ( 0.356240)
103
+
104
+ Arrays are also searchable.
105
+
106
+ jack@moya:~/work/surus$ ruby -I lib -I bench bench/array_find.rb
107
+ Skipping YAML test. Use -y to enable (VERY SLOW!)
108
+ Creating Surus test data... Done.
109
+
110
+ 2000 records with 10 element arrays
111
+ Finding all where array includes value 200 times
112
+ user system total real
113
+ Surus 0.120000 0.040000 0.160000 ( 0.531735)
114
+
115
+ Disabling synchronous commit can improve commit speed by 50% or more.
116
+
117
+ jack@moya:~/work/surus$ ruby -I lib -I bench bench/synchronous_commit.rb
118
+ Generating random data before test to avoid bias... Done.
119
+
120
+ Writing 1000 narrow records
121
+ user system total real
122
+ enabled 0.550000 0.870000 1.420000 ( 3.025896)
123
+ disabled 0.700000 0.580000 1.280000 ( 1.788585)
124
+ disabled per transaction 0.870000 0.580000 1.450000 ( 2.072150)
125
+ enabled / single transaction 0.700000 0.330000 1.030000 ( 1.280455)
126
+ disabled / single transaction 0.660000 0.340000 1.000000 ( 1.252301)
127
+
128
+ Writing 1000 wide records
129
+ user system total real
130
+ enabled 1.030000 0.870000 1.900000 ( 3.559709)
131
+ disabled 0.930000 0.780000 1.710000 ( 2.259340)
132
+ disabled per transaction 0.970000 0.850000 1.820000 ( 2.478290)
133
+ enabled / single transaction 0.890000 0.500000 1.390000 ( 1.693629)
134
+ disabled / single transaction 0.820000 0.450000 1.270000 ( 1.554767)
135
+
136
+ Many more benchmarks are in the bench directory. Most accept parameters to
137
+ adjust the amount of test data.
138
+
139
+ ## Running the benchmarks
140
+
141
+ 1. Create a database
142
+ 2. Configure bench/database.yml to connect to it.
143
+ 3. Load bench/database_structure.sql into your bench database.
144
+ 4. Run benchmark scripts from root of gem directory (remember pass ruby
145
+ the include paths for lib and bench)
146
+
147
+
148
+
55
149
  # License
56
150
 
57
151
  MIT
@@ -0,0 +1,46 @@
1
+ require 'benchmark_helper'
2
+
3
+ options = {}
4
+ optparse = OptionParser.new do |opts|
5
+ options[:records] = 1000
6
+ opts.on '-r NUM', '--records NUM', Integer, 'Number of records to create' do |n|
7
+ options[:records] = n
8
+ end
9
+
10
+ options[:elements] = 10
11
+ opts.on '-e NUM', '--elements NUM', Integer, 'Number of elements' do |n|
12
+ options[:elements] = n
13
+ end
14
+ end
15
+
16
+ optparse.parse!
17
+
18
+ clean_database
19
+
20
+ num_records = options[:records]
21
+ num_elements = options[:elements]
22
+
23
+ arrays = num_records.times.map do
24
+ num_elements.times.map { SecureRandom.hex(4) }
25
+ end
26
+
27
+ puts
28
+ puts "Writing #{num_records} records with a #{num_elements} element string array"
29
+
30
+ Benchmark.bm(8) do |x|
31
+ x.report("Surus") do
32
+ SurusKeyValueRecord.transaction do
33
+ num_records.times do |i|
34
+ SurusTextArrayRecord.create! :names => arrays[i]
35
+ end
36
+ end
37
+ end
38
+
39
+ x.report("YAML") do
40
+ YamlKeyValueRecord.transaction do
41
+ num_records.times do |i|
42
+ YamlArrayRecord.create! :names => arrays[i]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,79 @@
1
+ require 'benchmark_helper'
2
+
3
+ options = {}
4
+ optparse = OptionParser.new do |opts|
5
+ options[:records] = 2_000
6
+ opts.on '-r NUM', '--records NUM', Integer, 'Number of records to create' do |n|
7
+ options[:records] = n
8
+ end
9
+
10
+ options[:elements] = 10
11
+ opts.on '-e NUM', '--elements NUM', Integer, 'Number of elements per array' do |n|
12
+ options[:elements] = n
13
+ end
14
+
15
+ options[:finds] = 200
16
+ opts.on '-f NUM', '--finds NUM', Integer, 'Number of finds to perform' do |n|
17
+ options[:finds] = n
18
+ end
19
+
20
+ options[:yaml] = false
21
+ opts.on '-y', '--yaml', 'Include YAML in benchmark (VERY SLOW!)' do
22
+ options[:yaml] = true
23
+ end
24
+ end
25
+
26
+ optparse.parse!
27
+
28
+ clean_database
29
+
30
+ num_records = options[:records]
31
+ num_elements = options[:elements]
32
+ num_finds = options[:finds]
33
+ yaml = options[:yaml]
34
+
35
+ puts "Skipping YAML test. Use -y to enable (VERY SLOW!)" unless yaml
36
+
37
+ arrays = num_records.times.map do
38
+ num_elements.times.map { SecureRandom.hex(4) }
39
+ end
40
+
41
+ print "Creating Surus test data... "
42
+ SurusKeyValueRecord.transaction do
43
+ num_records.times do |i|
44
+ SurusTextArrayRecord.create! :names => arrays[i]
45
+ end
46
+ end
47
+ puts "Done."
48
+
49
+ if yaml
50
+ print "Creating YAML test data... "
51
+ YamlArrayRecord.transaction do
52
+ num_records.times do |i|
53
+ YamlArrayRecord.create! :names => arrays[i]
54
+ end
55
+ end
56
+ puts "Done."
57
+ end
58
+
59
+ puts
60
+ puts "#{num_records} records with #{num_elements} element arrays"
61
+ puts "Finding all where array includes value #{num_finds} times"
62
+
63
+ values_to_find = arrays.sample(num_finds).map { |a| a.sample }
64
+
65
+ Benchmark.bm(8) do |x|
66
+ x.report("Surus") do
67
+ values_to_find.each do |value_to_find|
68
+ SurusTextArrayRecord.array_has(:names, value_to_find).all
69
+ end
70
+ end
71
+
72
+ if yaml
73
+ x.report("YAML") do
74
+ values_to_find.each do |value_to_find|
75
+ YamlArrayRecord.all.select { |r| r.names.include?(value_to_find) }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,34 @@
1
+ require 'benchmark_helper'
2
+
3
+ options = {}
4
+ optparse = OptionParser.new do |opts|
5
+ options[:elements] = 10
6
+ opts.on '-e NUM', '--elements NUM', Integer, 'Number of elements per array' do |n|
7
+ options[:elements] = n
8
+ end
9
+ end
10
+
11
+ optparse.parse!
12
+
13
+ clean_database
14
+
15
+ num_elements = options[:elements]
16
+ array = num_elements.times.map { SecureRandom.hex(4) }
17
+
18
+ SurusTextArrayRecord.create! :names => array
19
+ YamlArrayRecord.create! :names => array
20
+
21
+ num_reads = 3_000
22
+
23
+ puts
24
+ puts "Reading a single record with a #{num_elements} element array #{num_reads} times"
25
+
26
+ Benchmark.bm(8) do |x|
27
+ x.report("Surus") do
28
+ num_reads.times { SurusTextArrayRecord.first.names }
29
+ end
30
+
31
+ x.report("YAML") do
32
+ num_reads.times { YamlArrayRecord.first.names }
33
+ end
34
+ end
@@ -0,0 +1,65 @@
1
+ require 'surus'
2
+ require 'benchmark'
3
+ require 'securerandom'
4
+ require 'optparse'
5
+
6
+ database_config = YAML.load_file(File.expand_path("../database.yml", __FILE__))
7
+ ActiveRecord::Base.establish_connection database_config["bench"]
8
+
9
+ class YamlKeyValueRecord < ActiveRecord::Base
10
+ serialize :properties
11
+ end
12
+
13
+ class SurusKeyValueRecord < ActiveRecord::Base
14
+ serialize :properties, Surus::Hstore::Serializer.new
15
+ end
16
+
17
+ class EavMasterRecord < ActiveRecord::Base
18
+ has_many :eav_detail_records
19
+
20
+ def properties
21
+ @properties ||= eav_detail_records.each_with_object({}) do |d, hash|
22
+ hash[d.key] = d.value
23
+ end
24
+ end
25
+
26
+ def properties=(value)
27
+ @properties = value
28
+ end
29
+
30
+ after_save :persist_properties
31
+ def persist_properties
32
+ eav_detail_records.clear
33
+ @properties.each do |k,v|
34
+ eav_detail_records.create! :key => k, :value => v
35
+ end
36
+ end
37
+ end
38
+
39
+ class EavDetailRecord < ActiveRecord::Base
40
+ belongs_to :eav_master_record
41
+ end
42
+
43
+ class YamlArrayRecord < ActiveRecord::Base
44
+ serialize :names
45
+ end
46
+
47
+ class SurusTextArrayRecord < ActiveRecord::Base
48
+ serialize :names, Surus::Array::TextSerializer.new
49
+ end
50
+
51
+ class WideRecord < ActiveRecord::Base
52
+ end
53
+
54
+ class NarrowRecord < ActiveRecord::Base
55
+ end
56
+
57
+ def clean_database
58
+ YamlKeyValueRecord.delete_all
59
+ SurusKeyValueRecord.delete_all
60
+ EavDetailRecord.delete_all
61
+ EavMasterRecord.delete_all
62
+ YamlArrayRecord.delete_all
63
+ WideRecord.delete_all
64
+ NarrowRecord.delete_all
65
+ end
@@ -0,0 +1,4 @@
1
+ bench:
2
+ adapter: postgresql
3
+ encoding: unicode
4
+ database: surus_bench
@@ -0,0 +1,92 @@
1
+ CREATE EXTENSION IF NOT EXISTS hstore;
2
+
3
+ DROP TABLE IF EXISTS yaml_key_value_records;
4
+
5
+ CREATE TABLE yaml_key_value_records(
6
+ id serial PRIMARY KEY,
7
+ properties varchar
8
+ );
9
+
10
+
11
+ DROP TABLE IF EXISTS surus_key_value_records;
12
+
13
+ CREATE TABLE surus_key_value_records(
14
+ id serial PRIMARY KEY,
15
+ properties hstore
16
+ );
17
+
18
+ CREATE INDEX ON surus_key_value_records USING GIN (properties);
19
+
20
+
21
+ DROP TABLE IF EXISTS eav_detail_records;
22
+ DROP TABLE IF EXISTS eav_key_value_records;
23
+
24
+ CREATE TABLE eav_master_records(
25
+ id serial PRIMARY KEY
26
+ );
27
+
28
+ CREATE TABLE eav_detail_records(
29
+ id serial PRIMARY KEY,
30
+ eav_master_record_id integer REFERENCES eav_master_records,
31
+ "key" varchar NOT NULL,
32
+ "value" varchar NOT NULL
33
+ );
34
+
35
+ CREATE INDEX ON eav_detail_records (eav_master_record_id);
36
+ CREATE INDEX ON eav_detail_records ("key");
37
+ CREATE UNIQUE INDEX ON eav_detail_records(eav_master_record_id, "key");
38
+ CREATE INDEX ON eav_detail_records ("value");
39
+
40
+
41
+ DROP TABLE IF EXISTS yaml_array_records;
42
+
43
+ CREATE TABLE yaml_array_records(
44
+ id serial PRIMARY KEY,
45
+ names text
46
+ );
47
+
48
+
49
+ DROP TABLE IF EXISTS surus_text_array_records;
50
+
51
+ CREATE TABLE surus_text_array_records(
52
+ id serial PRIMARY KEY,
53
+ names text[]
54
+ );
55
+
56
+ CREATE INDEX ON surus_text_array_records USING GIN (names);
57
+
58
+
59
+
60
+ DROP TABLE IF EXISTS wide_records;
61
+
62
+ -- a couple indexed fields and a number of unindexed fields
63
+ CREATE TABLE wide_records(
64
+ id serial PRIMARY KEY,
65
+ a varchar NOT NULL,
66
+ b varchar NOT NULL,
67
+ c varchar NOT NULL,
68
+ d varchar NOT NULL,
69
+ e varchar NOT NULL,
70
+ f varchar NOT NULL,
71
+ g varchar NOT NULL,
72
+ h varchar NOT NULL,
73
+ i varchar NOT NULL,
74
+ j varchar NOT NULL
75
+ );
76
+
77
+ CREATE INDEX ON wide_records (a);
78
+ CREATE INDEX ON wide_records (b);
79
+
80
+
81
+
82
+ DROP TABLE IF EXISTS narrow_records;
83
+
84
+ -- a one indexed fields and a couple unindexed fields
85
+ CREATE TABLE narrow_records(
86
+ id serial PRIMARY KEY,
87
+ a varchar NOT NULL,
88
+ b varchar NOT NULL,
89
+ c varchar NOT NULL
90
+ );
91
+
92
+ CREATE INDEX ON narrow_records (a);
@@ -0,0 +1,57 @@
1
+ require 'benchmark_helper'
2
+
3
+ options = {}
4
+ optparse = OptionParser.new do |opts|
5
+ options[:records] = 200
6
+ opts.on '-r NUM', '--records NUM', Integer, 'Number of records to create' do |n|
7
+ options[:records] = n
8
+ end
9
+
10
+ options[:pairs] = 5
11
+ opts.on '-p NUM', '--pairs NUM', Integer, 'Number of key/value pairs' do |n|
12
+ options[:pairs] = n
13
+ end
14
+ end
15
+
16
+ optparse.parse!
17
+
18
+ clean_database
19
+ GC.disable
20
+
21
+ num_records = options[:records]
22
+ num_key_value_pairs = options[:pairs]
23
+
24
+ key_value_pairs = num_records.times.map do
25
+ num_key_value_pairs.times.each_with_object({}) do |n, hash|
26
+ hash[SecureRandom.hex(4)] = SecureRandom.hex(4)
27
+ end
28
+ end
29
+
30
+ puts
31
+ puts "Writing #{num_records} records with #{num_key_value_pairs} string key/value pairs"
32
+
33
+ Benchmark.bm(8) do |x|
34
+ x.report("EAV") do
35
+ EavMasterRecord.transaction do
36
+ num_records.times do |i|
37
+ EavMasterRecord.create! :properties => key_value_pairs[i]
38
+ end
39
+ end
40
+ end
41
+
42
+ x.report("Surus") do
43
+ SurusKeyValueRecord.transaction do
44
+ num_records.times do |i|
45
+ SurusKeyValueRecord.create! :properties => key_value_pairs[i]
46
+ end
47
+ end
48
+ end
49
+
50
+ x.report("YAML") do
51
+ YamlKeyValueRecord.transaction do
52
+ num_records.times do |i|
53
+ YamlKeyValueRecord.create! :properties => key_value_pairs[i]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,105 @@
1
+ require 'benchmark_helper'
2
+
3
+ options = {}
4
+ optparse = OptionParser.new do |opts|
5
+ options[:records] = 2_000
6
+ opts.on '-r NUM', '--records NUM', Integer, 'Number of records to create' do |n|
7
+ options[:records] = n
8
+ end
9
+
10
+ options[:pairs] = 5
11
+ opts.on '-p NUM', '--pairs NUM', Integer, 'Number of key/value pairs' do |n|
12
+ options[:pairs] = n
13
+ end
14
+
15
+ options[:eav] = false
16
+ opts.on '-e', '--eav', 'Include EAV in benchmark (VERY SLOW!)' do
17
+ options[:eav] = true
18
+ end
19
+
20
+ options[:yaml] = false
21
+ opts.on '-y', '--yaml', 'Include YAML in benchmark (VERY SLOW!)' do
22
+ options[:yaml] = true
23
+ end
24
+ end
25
+
26
+ optparse.parse!
27
+
28
+ clean_database
29
+
30
+ num_records = options[:records]
31
+ num_key_value_pairs = options[:pairs]
32
+ eav = options[:eav]
33
+ yaml = options[:yaml]
34
+
35
+ puts "Skipping EAV test. Use -e to enable (VERY SLOW!)" unless eav
36
+ puts "Skipping YAML test. Use -y to enable (VERY SLOW!)" unless yaml
37
+
38
+ key_value_pairs = num_records.times.map do
39
+ num_key_value_pairs.times.each_with_object({}) do |n, hash|
40
+ hash[SecureRandom.hex(2)] = SecureRandom.hex(4)
41
+ end
42
+ end
43
+
44
+ if eav
45
+ print "Creating EAV test data... "
46
+ EavMasterRecord.transaction do
47
+ num_records.times do |i|
48
+ EavMasterRecord.create! :properties => key_value_pairs[i]
49
+ end
50
+ end
51
+ puts "Done."
52
+ end
53
+
54
+ print "Creating Surus test data... "
55
+ SurusKeyValueRecord.transaction do
56
+ num_records.times do |i|
57
+ SurusKeyValueRecord.create! :properties => key_value_pairs[i]
58
+ end
59
+ end
60
+ puts "Done."
61
+
62
+ if yaml
63
+ print "Creating YAML test data... "
64
+ YamlKeyValueRecord.transaction do
65
+ num_records.times do |i|
66
+ YamlKeyValueRecord.create! :properties => key_value_pairs[i]
67
+ end
68
+ end
69
+ puts "Done."
70
+ end
71
+
72
+
73
+ num_finds = 200
74
+ keys_to_find = key_value_pairs.sample(num_finds).map { |h| h.keys.sample }
75
+
76
+ puts
77
+ puts "#{num_records} records with #{num_key_value_pairs} string key/value pairs"
78
+ puts "Finding all by inclusion of a key #{num_finds} times"
79
+
80
+ Benchmark.bm(8) do |x|
81
+ if eav
82
+ x.report("EAV") do
83
+ keys_to_find.each do |key_to_find|
84
+ EavMasterRecord
85
+ .includes(:eav_detail_records)
86
+ .where("EXISTS(SELECT 1 FROM eav_detail_records WHERE eav_master_records.id=eav_detail_records.eav_master_record_id AND key=?)", key_to_find)
87
+ .all
88
+ end
89
+ end
90
+ end
91
+
92
+ x.report("Surus") do
93
+ keys_to_find.each do |key_to_find|
94
+ SurusKeyValueRecord.hstore_has_key(:properties, key_to_find).all
95
+ end
96
+ end
97
+
98
+ if yaml
99
+ x.report("YAML") do
100
+ keys_to_find.each do |key_to_find|
101
+ YamlKeyValueRecord.all.select { |r| r.properties.key?(key_to_find) }
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,42 @@
1
+ require 'benchmark_helper'
2
+
3
+ options = {}
4
+ optparse = OptionParser.new do |opts|
5
+ options[:pairs] = 5
6
+ opts.on '-p NUM', '--pairs NUM', Integer, 'Number of key/value pairs' do |n|
7
+ options[:pairs] = n
8
+ end
9
+ end
10
+
11
+ optparse.parse!
12
+
13
+ clean_database
14
+
15
+ num_key_value_pairs = options[:pairs]
16
+
17
+ key_value_pair = num_key_value_pairs.times.each_with_object({}) do |n, hash|
18
+ hash[SecureRandom.hex(4)] = SecureRandom.hex(4)
19
+ end
20
+
21
+ EavMasterRecord.create! :properties => key_value_pair
22
+ SurusKeyValueRecord.create! :properties => key_value_pair
23
+ YamlKeyValueRecord.create! :properties => key_value_pair
24
+
25
+ num_reads = 3_000
26
+
27
+ puts
28
+ puts "Reading a single record with #{num_key_value_pairs} string key/value pairs #{num_reads} times"
29
+
30
+ Benchmark.bm(8) do |x|
31
+ x.report("EAV") do
32
+ num_reads.times { EavMasterRecord.first.properties }
33
+ end
34
+
35
+ x.report("Surus") do
36
+ num_reads.times { SurusKeyValueRecord.first.properties }
37
+ end
38
+
39
+ x.report("YAML") do
40
+ num_reads.times { YamlKeyValueRecord.first.properties }
41
+ end
42
+ end
@@ -0,0 +1,90 @@
1
+ require 'benchmark_helper'
2
+
3
+ options = {}
4
+ optparse = OptionParser.new do |opts|
5
+ options[:records] = 1000
6
+ opts.on '-r NUM', '--records NUM', Integer, 'Number of records to create' do |n|
7
+ options[:records] = n
8
+ end
9
+ end
10
+
11
+ optparse.parse!
12
+ num_records = options[:records]
13
+ narrow_field_width = 8
14
+ wide_field_width = 25
15
+
16
+ print "Generating random data before test to avoid bias... "
17
+ random_string = SecureRandom.base64(num_records * 10 * wide_field_width)
18
+ puts "Done."
19
+
20
+ random_io = StringIO.new(random_string)
21
+
22
+ create_wide_record = lambda do
23
+ WideRecord.create! :a => random_io.read(wide_field_width),
24
+ :b => random_io.read(wide_field_width),
25
+ :c => random_io.read(wide_field_width),
26
+ :d => random_io.read(wide_field_width),
27
+ :e => random_io.read(wide_field_width),
28
+ :f => random_io.read(wide_field_width),
29
+ :g => random_io.read(wide_field_width),
30
+ :h => random_io.read(wide_field_width),
31
+ :i => random_io.read(wide_field_width),
32
+ :j => random_io.read(wide_field_width)
33
+ end
34
+
35
+ create_narrow_record = lambda do
36
+ NarrowRecord.create! :a => random_io.read(narrow_field_width),
37
+ :b => random_io.read(narrow_field_width),
38
+ :c => random_io.read(narrow_field_width)
39
+ end
40
+
41
+ { "narrow" => create_narrow_record, "wide" => create_wide_record }.each do |text, create_record|
42
+ puts
43
+ puts "Writing #{num_records} #{text} records"
44
+
45
+ Benchmark.bm(30) do |x|
46
+ clean_database
47
+ random_io.rewind
48
+ WideRecord.synchronous_commit true
49
+ x.report("enabled") do
50
+ num_records.times { create_record.call }
51
+ end
52
+
53
+ clean_database
54
+ random_io.rewind
55
+ WideRecord.synchronous_commit false
56
+ x.report("disabled") do
57
+ num_records.times { create_record.call }
58
+ end
59
+
60
+ clean_database
61
+ random_io.rewind
62
+ WideRecord.synchronous_commit true
63
+ x.report("disabled per transaction") do
64
+ num_records.times do
65
+ WideRecord.transaction do
66
+ WideRecord.synchronous_commit false
67
+ create_record.call
68
+ end
69
+ end
70
+ end
71
+
72
+ clean_database
73
+ random_io.rewind
74
+ WideRecord.synchronous_commit true
75
+ x.report("enabled / single transaction") do
76
+ WideRecord.transaction do
77
+ num_records.times { create_record.call }
78
+ end
79
+ end
80
+
81
+ clean_database
82
+ random_io.rewind
83
+ WideRecord.synchronous_commit false
84
+ x.report("disabled / single transaction") do
85
+ WideRecord.transaction do
86
+ num_records.times { create_record.call }
87
+ end
88
+ end
89
+ end
90
+ end
@@ -7,3 +7,5 @@ require 'surus/array/integer_serializer'
7
7
  require 'surus/array/float_serializer'
8
8
  require 'surus/array/decimal_serializer'
9
9
  require 'surus/array/scope'
10
+ require 'surus/synchronous_commit/connection'
11
+ require 'surus/synchronous_commit/model'
@@ -1,10 +1,22 @@
1
1
  module Surus
2
2
  module Array
3
3
  module Scope
4
+ # Adds where condition that requires column to contain all values
5
+ #
6
+ # Examples:
7
+ # User.array_has(:permissions, "manage_users")
8
+ # User.array_has(:permissions, "manage_users", "manage_roles")
9
+ # User.array_has(:permissions, ["manage_users", "manage_roles"])
4
10
  def array_has(column, *values)
5
11
  where("#{connection.quote_column_name(column)} @> ARRAY[?]", values.flatten)
6
12
  end
7
13
 
14
+ # Adds where condition that requires column to contain any values
15
+ #
16
+ # Examples:
17
+ # User.array_has_any(:permissions, "manage_users")
18
+ # User.array_has_any(:permissions, "manage_users", "manage_roles")
19
+ # User.array_has_any(:permissions, ["manage_users", "manage_roles"])
8
20
  def array_has_any(column, *values)
9
21
  where("#{connection.quote_column_name(column)} && ARRAY[?]", values.flatten)
10
22
  end
@@ -1,18 +1,36 @@
1
1
  module Surus
2
2
  module Hstore
3
3
  module Scope
4
+ # Adds a where condition that requires column to contain hash
5
+ #
6
+ # Example:
7
+ # User.hstore_has_pairs(:properties, "favorite_color" => "green")
4
8
  def hstore_has_pairs(column, hash)
5
9
  where("#{connection.quote_column_name(column)} @> ?", Serializer.new.dump(hash))
6
10
  end
7
11
 
12
+ # Adds a where condition that requires column to contain key
13
+ #
14
+ # Example:
15
+ # User.hstore_has_key(:properties, "favorite_color")
8
16
  def hstore_has_key(column, key)
9
17
  where("#{connection.quote_column_name(column)} ? :key", :key => key)
10
18
  end
11
19
 
20
+ # Adds a where condition that requires column to contain all keys.
21
+ #
22
+ # Example:
23
+ # User.hstore_has_all_keys(:properties, "favorite_color", "favorite_song")
24
+ # User.hstore_has_all_keys(:properties, ["favorite_color", "favorite_song"])
12
25
  def hstore_has_all_keys(column, *keys)
13
26
  where("#{connection.quote_column_name(column)} ?& ARRAY[:keys]", :keys => keys.flatten)
14
27
  end
15
28
 
29
+ # Adds a where condition that requires column to contain any keys.
30
+ #
31
+ # Example:
32
+ # User.hstore_has_any_keys(:properties, "favorite_color", "favorite_song")
33
+ # User.hstore_has_any_keys(:properties, ["favorite_color", "favorite_song"])
16
34
  def hstore_has_any_keys(column, *keys)
17
35
  where("#{connection.quote_column_name(column)} ?| ARRAY[:keys]", :keys => keys.flatten)
18
36
  end
@@ -108,6 +108,8 @@ module Surus
108
108
  def stringify(value)
109
109
  if value.kind_of?(String)
110
110
  [value, "String"]
111
+ elsif value.kind_of?(Symbol)
112
+ [value.to_s, "Symbol"]
111
113
  elsif value.kind_of?(Integer)
112
114
  [value.to_s, "Integer"]
113
115
  elsif value.kind_of?(Float)
@@ -129,6 +131,8 @@ module Surus
129
131
 
130
132
  def typecast(value, type)
131
133
  case type
134
+ when "Symbol"
135
+ value.to_sym
132
136
  when "Integer"
133
137
  Integer(value)
134
138
  when "Float"
@@ -0,0 +1,34 @@
1
+ module Surus
2
+ module SynchronousCommit
3
+ module Connection
4
+ # When called without any value returns the current synchronous_commit
5
+ # value.
6
+ #
7
+ # When called with a value it is delegated to #synchronous_commit=
8
+ def synchronous_commit(value=:not_passed_param)
9
+ if value == :not_passed_param
10
+ select_value("SHOW synchronous_commit") == "on"
11
+ else
12
+ self.synchronous_commit = value
13
+ end
14
+ end
15
+
16
+ # Changes current synchronous_commit state. If a transaction is currently
17
+ # in progress the change will be reverted at the end of the transaction.
18
+ #
19
+ # Requires true or false to be passed exactly -- not merely truthy or falsy
20
+ def synchronous_commit=(value)
21
+ raise ArgumentError, "argument must be true or false" unless value == true || value == false
22
+
23
+ execute "SET #{'LOCAL' if open_transactions > 0} synchronous_commit TO #{value ? 'ON' : 'OFF'}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ # If Surus is loaded before establish_connection is called then
30
+ # PostgreSQLAdapter will not be loaded yet. require it to ensure
31
+ # it is available
32
+ require "active_record/connection_adapters/postgresql_adapter"
33
+
34
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :include, Surus::SynchronousCommit::Connection
@@ -0,0 +1,11 @@
1
+ module Surus
2
+ module SynchronousCommit
3
+ # synchronous_commit and synchronous_commit= are delegated to the underlying
4
+ # connection object
5
+ module Model
6
+ delegate :synchronous_commit, :synchronous_commit=, :to => :connection
7
+ end
8
+ end
9
+ end
10
+
11
+ ActiveRecord::Base.extend Surus::SynchronousCommit::Model
@@ -1,3 +1,3 @@
1
1
  module Surus
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -30,6 +30,7 @@ describe Surus::Hstore::Serializer do
30
30
  end
31
31
 
32
32
  [
33
+ [:foo, "symbol"],
33
34
  [0, "integer 0"],
34
35
  [1, "positive integer"],
35
36
  [-1, "negative integer"],
@@ -31,7 +31,7 @@ end
31
31
 
32
32
 
33
33
  RSpec.configure do |config|
34
- config.around do |example|
34
+ config.around :disable_transactions => nil do |example|
35
35
  ActiveRecord::Base.transaction do
36
36
  example.call
37
37
  raise ActiveRecord::Rollback
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ describe Surus::SynchronousCommit::Connection, :disable_transactions => true do
4
+ let(:conn) { ActiveRecord::Base.connection }
5
+ before { conn.execute "SET synchronous_commit TO ON;" }
6
+ after { conn.execute "SET synchronous_commit TO ON;" }
7
+
8
+ describe "synchronous_commit" do
9
+ context "without parameter" do
10
+ subject { conn.synchronous_commit }
11
+
12
+ context "when synchronous_commit commit is off" do
13
+ before { conn.execute "SET synchronous_commit TO OFF;" }
14
+ it { should == false }
15
+ end
16
+
17
+ context "when synchronous_commit commit is on" do
18
+ before { conn.execute "SET synchronous_commit TO ON;" }
19
+ it { should == true }
20
+ end
21
+ end
22
+
23
+ context "with parameter" do
24
+ context "true" do
25
+ before { conn.execute "SET synchronous_commit TO OFF;" }
26
+ it "sets synchronous_commit to on" do
27
+ conn.synchronous_commit true
28
+ conn.select_value("SHOW synchronous_commit").should == "on"
29
+ end
30
+ end
31
+
32
+ context "false" do
33
+ before { conn.execute "SET synchronous_commit TO ON;" }
34
+ it "sets synchronous_commit to off" do
35
+ conn.synchronous_commit false
36
+ conn.select_value("SHOW synchronous_commit").should == "off"
37
+ end
38
+ end
39
+
40
+ context "invalid value" do
41
+ it "raises ArgumentError" do
42
+ expect{conn.synchronous_commit "foo"}.to raise_error(ArgumentError)
43
+ end
44
+ end
45
+
46
+ context "inside transaction" do
47
+ before { conn.execute "SET synchronous_commit TO OFF;" }
48
+
49
+ it "only persists for duration of transaction" do
50
+ conn.transaction do
51
+ conn.synchronous_commit true
52
+ conn.select_value("SHOW synchronous_commit").should == "on"
53
+ end
54
+ conn.select_value("SHOW synchronous_commit").should == "off"
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ describe "synchronous_commit=" do
61
+ context "true" do
62
+ before { conn.execute "SET synchronous_commit TO OFF;" }
63
+ it "sets synchronous_commit to on" do
64
+ conn.synchronous_commit true
65
+ conn.select_value("SHOW synchronous_commit").should == "on"
66
+ end
67
+ end
68
+
69
+ context "false" do
70
+ before { conn.execute "SET synchronous_commit TO ON;" }
71
+ it "sets synchronous_commit to off" do
72
+ conn.synchronous_commit false
73
+ conn.select_value("SHOW synchronous_commit").should == "off"
74
+ end
75
+ end
76
+
77
+ context "invalid value" do
78
+ it "raises ArgumentError" do
79
+ expect{conn.synchronous_commit "foo"}.to raise_error(ArgumentError)
80
+ end
81
+ end
82
+
83
+ context "inside transaction" do
84
+ before { conn.execute "SET synchronous_commit TO OFF;" }
85
+
86
+ it "only persists for duration of transaction" do
87
+ conn.transaction do
88
+ conn.synchronous_commit true
89
+ conn.select_value("SHOW synchronous_commit").should == "on"
90
+ end
91
+ conn.select_value("SHOW synchronous_commit").should == "off"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Surus::SynchronousCommit::Model do
4
+ let(:conn) { ActiveRecord::Base.connection }
5
+
6
+ describe "synchronous_commit" do
7
+ it "is delegated to connection" do
8
+ conn.should_receive(:synchronous_commit)
9
+ ActiveRecord::Base.synchronous_commit
10
+ end
11
+ end
12
+
13
+ describe "synchronous_commit=" do
14
+ it "is delegated to connection" do
15
+ conn.should_receive(:synchronous_commit=)
16
+ ActiveRecord::Base.synchronous_commit = true
17
+ end
18
+ end
19
+ end
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  s.email = ["jack@jackchristensen.com"]
10
10
  s.homepage = "https://github.com/JackC/surus"
11
11
  s.summary = %q{PostgreSQL extensions for ActiveRecord}
12
- s.description = %q{PostgreSQL extensions for ActiveRecord}
12
+ s.description = %q{Includes serializers and search scopes for hstore and array. Also includes control over synchronous_commit to boost insert and update speed.}
13
13
 
14
14
  s.rubyforge_project = ""
15
15
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: surus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-03 00:00:00.000000000 Z
12
+ date: 2012-02-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pg
16
- requirement: &17079080 !ruby/object:Gem::Requirement
16
+ requirement: &13148960 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *17079080
24
+ version_requirements: *13148960
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: activerecord
27
- requirement: &17091280 !ruby/object:Gem::Requirement
27
+ requirement: &13161940 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 3.1.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *17091280
35
+ version_requirements: *13161940
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rspec
38
- requirement: &17089120 !ruby/object:Gem::Requirement
38
+ requirement: &13159120 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 2.8.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *17089120
46
+ version_requirements: *13159120
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: guard
49
- requirement: &17086120 !ruby/object:Gem::Requirement
49
+ requirement: &13155700 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 0.10.0
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *17086120
57
+ version_requirements: *13155700
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: guard-rspec
60
- requirement: &17102600 !ruby/object:Gem::Requirement
60
+ requirement: &13173200 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,8 +65,9 @@ dependencies:
65
65
  version: 0.6.0
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *17102600
69
- description: PostgreSQL extensions for ActiveRecord
68
+ version_requirements: *13173200
69
+ description: Includes serializers and search scopes for hstore and array. Also includes
70
+ control over synchronous_commit to boost insert and update speed.
70
71
  email:
71
72
  - jack@jackchristensen.com
72
73
  executables: []
@@ -81,6 +82,16 @@ files:
81
82
  - LICENSE
82
83
  - README.md
83
84
  - Rakefile
85
+ - bench/array_create.rb
86
+ - bench/array_find.rb
87
+ - bench/array_serialize.rb
88
+ - bench/benchmark_helper.rb
89
+ - bench/database.yml
90
+ - bench/database_structure.sql
91
+ - bench/hstore_create.rb
92
+ - bench/hstore_find.rb
93
+ - bench/hstore_serialize.rb
94
+ - bench/synchronous_commit.rb
84
95
  - lib/surus.rb
85
96
  - lib/surus/array/decimal_serializer.rb
86
97
  - lib/surus/array/float_serializer.rb
@@ -89,6 +100,8 @@ files:
89
100
  - lib/surus/array/text_serializer.rb
90
101
  - lib/surus/hstore/scope.rb
91
102
  - lib/surus/hstore/serializer.rb
103
+ - lib/surus/synchronous_commit/connection.rb
104
+ - lib/surus/synchronous_commit/model.rb
92
105
  - lib/surus/version.rb
93
106
  - spec/array/decimal_serializer_spec.rb
94
107
  - spec/array/float_serializer_spec.rb
@@ -100,6 +113,8 @@ files:
100
113
  - spec/hstore/scope_spec.rb
101
114
  - spec/hstore/serializer_spec.rb
102
115
  - spec/spec_helper.rb
116
+ - spec/synchronous_commit/connection_spec.rb
117
+ - spec/synchronous_commit/model_spec.rb
103
118
  - surus.gemspec
104
119
  homepage: https://github.com/JackC/surus
105
120
  licenses: []