db_memoize 0.1.6 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2134329e831a84420b606a2f64aeae8703ebcd7a
4
- data.tar.gz: 2abf35de9b06d53424513a0432cb884c73f1f4cd
3
+ metadata.gz: 254ec2be767020e1ff61a4b5ddbfc561f894b716
4
+ data.tar.gz: b27ccaf67e4a4dc32901fe802ab6b7da5872d9ba
5
5
  SHA512:
6
- metadata.gz: 208ba78d6912596d059e40e4f77f9393025a926c4c973a2599882a6980ce836c008442466d3db0c84fd8e1a9e2a625ca901f26d697f64054abdccf7976c700f9
7
- data.tar.gz: 994bdc8506e1886f57276380da78431812d9c74b99c66ea9f02ce9bffef87b1f13e27182c556ba5dab8363f8896de9c4ae9d2442be214a1c324cf007cb416c9f
6
+ metadata.gz: e430b58340a10f27910e558068cca7ac75ab8e07ec9d0aa5898d340aeeaa142dc3241779d3f1c02d16755d8390ddaee44518a704c563e9c5b7a5aa2ac670ea36
7
+ data.tar.gz: 82949ec2ac4094966459edbeadb5f34c91f03d9f20cda7e98ee9c1978b3b6fb12a671623473fdb7db88554a01f0ec9fcfb28c8fe8da873f5f218adbd1d07b430
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /log/
@@ -4,5 +4,6 @@ rvm:
4
4
  - 2.3.1
5
5
  before_install: gem install bundler -v 1.13.6
6
6
  script:
7
+ - bundle exec rake db:test:create
7
8
  - bundle exec rspec
8
9
  - bundle exec rubocop
data/README.md CHANGED
@@ -35,10 +35,8 @@ will call the original method only once. Consecutive calls will return a cached
35
35
 
36
36
  If the method takes arguments..
37
37
 
38
- ```
39
- record.hello('Maria')
40
- record.hello('John')
41
- ```
38
+ record.hello('Maria')
39
+ record.hello('John')
42
40
 
43
41
  a cached value will be created for every set of arguments.
44
42
 
@@ -46,67 +44,47 @@ a cached value will be created for every set of arguments.
46
44
 
47
45
  To clear cached values for a single method
48
46
 
49
- ```
50
- record.unmemoize(:hello)
51
- ```
47
+ record.unmemoize(:hello)
52
48
 
53
49
  To clear all cached values of one record
54
50
 
55
- ```
56
- record.unmemoize
57
- ```
51
+ record.unmemoize
58
52
 
59
53
  To clear cached values of given records for a single method
60
54
 
61
- ```
62
- Letter.unmemoize([letter1, letter2], :hello)
63
- ```
55
+ Letter.unmemoize([letter1, letter2], :hello)
64
56
 
65
57
  To clear all cached values of given records
66
58
 
67
- ```
68
- Letter.unmemoize([letter1, letter2])
69
- ```
59
+ Letter.unmemoize([letter1, letter2])
70
60
 
71
61
  Instead of ActiveRecord instances it's sufficient to pass in the ids of the records, too
72
62
 
73
- ```
74
- Letter.unmemoize([23,24])
75
- ```
63
+ Letter.unmemoize([23,24])
76
64
 
77
65
  ### Gotchas
78
66
 
79
67
  As the cached values themselves are writtten to the database, are ActiveRecord records (of type `DbMemoize::Value`) and are reqistered as an association you can access all of the cached values of an object like this:
80
68
 
81
- ```
82
- record.memoized_values
83
- ```
69
+ record.memoized_values
84
70
 
85
71
  This means you can also very easily perform eager loading on them:
86
72
 
87
- ```
88
- Letter.includes(:memoized_values).all
89
- ```
73
+ Letter.includes(:memoized_values).all
90
74
 
91
75
  DbMemoize by default will write log output to STDOUT. You can change this by setting another logger like so:
92
76
 
93
- ```
94
- DbMemoize.logger = your_logger
95
- ```
77
+ DbMemoize.logger = your_logger
96
78
 
97
79
  ### Rake Tasks
98
80
 
99
81
  To _warmup_ your cache you can pre-generate cached values via a rake task like this (only works for methods not depending on arguments)
100
82
 
101
- ```
102
- bundle exec rake db_memoize:warmup class=Letter methods=hello,bye
103
- ```
83
+ bundle exec rake db_memoize:warmup class=Letter methods=hello,bye
104
84
 
105
85
  Similarly you can wipe all cached values for a given class
106
86
 
107
- ```
108
- bundle exec rake db_memoize:clear class=Letter
109
- ```
87
+ bundle exec rake db_memoize:clear class=Letter
110
88
 
111
89
  ### Setup
112
90
 
data/Rakefile CHANGED
@@ -3,4 +3,9 @@ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task "db:test:create" do
7
+ Rake.sh "dropdb db_memoize_test || true"
8
+ Rake.sh "createdb db_memoize_test"
9
+ end
10
+
11
+ task :default => %w(db:test:create spec)
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency 'activerecord', '~> 4.2'
26
26
 
27
27
  spec.add_development_dependency 'rake', '~> 10.5'
28
- spec.add_development_dependency 'sqlite3'
28
+ spec.add_development_dependency 'pg'
29
29
  spec.add_development_dependency 'rspec-rails', '~> 3.4'
30
30
  spec.add_development_dependency 'pry', '~> 0.10'
31
31
  spec.add_development_dependency 'pry-byebug', '~> 2.0'
@@ -2,7 +2,9 @@ require 'active_record'
2
2
  require 'active_support'
3
3
  require 'digest'
4
4
  require 'benchmark'
5
+
5
6
  require 'db_memoize/version'
7
+ require 'db_memoize/metal'
6
8
  require 'db_memoize/value'
7
9
  require 'db_memoize/helpers'
8
10
  require 'db_memoize/model'
@@ -12,7 +12,21 @@ module DbMemoize
12
12
  end
13
13
 
14
14
  def log(model, method_name, msg)
15
- DbMemoize.logger.send(DbMemoize.log_level, "DbMemoize <#{model.class.name} id: #{model.id}>##{method_name} - #{msg}")
15
+ DbMemoize.logger.send(DbMemoize.log_level) do
16
+ "DbMemoize <#{model.class.name}##{model.id}>##{method_name} - #{msg}"
17
+ end
18
+ end
19
+
20
+ def calculate_arguments_hash(arguments)
21
+ arguments.empty? ? nil : ::Digest::MD5.hexdigest(Marshal.dump(arguments))
22
+ end
23
+
24
+ def marshal(value)
25
+ Marshal.dump(value)
26
+ end
27
+
28
+ def unmarshal(value)
29
+ Marshal.load(value)
16
30
  end
17
31
  end
18
32
  end
@@ -0,0 +1,102 @@
1
+ module DbMemoize
2
+ module Metal
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ # base.metal # initialize the metal adapter
6
+ end
7
+
8
+ module ClassMethods
9
+ def metal
10
+ @metal ||= Adapter.new(self)
11
+ end
12
+ end
13
+
14
+ class Adapter
15
+ class PkInfo
16
+ attr_reader :column, :type
17
+
18
+ def initialize(base_klass)
19
+ @column = base_klass.primary_key
20
+ @type = @column && base_klass.columns_hash.fetch(@column).type
21
+ end
22
+ end
23
+
24
+ def initialize(base_klass)
25
+ @base_klass = base_klass
26
+ @query_cache = {}
27
+ end
28
+
29
+ private
30
+
31
+ # setup primary key information. This is necessary to allow the create! method
32
+ # to return the primary key of a newly created entry.
33
+ def primary_key
34
+ @primary_key ||= PkInfo.new(@base_klass)
35
+ end
36
+
37
+ def table_name
38
+ @base_klass.table_name
39
+ end
40
+
41
+ def raw_connection
42
+ @base_klass.connection.raw_connection # do not memoize me!
43
+ end
44
+
45
+ def column?(column_name)
46
+ @base_klass.columns_hash.key?(column_name)
47
+ end
48
+
49
+ def insert_sql(field_names)
50
+ @query_cache[field_names] ||= _insert_sql(field_names)
51
+ end
52
+
53
+ DATABASE_IDENTIFIER_REGEX = /\A\w+\z/
54
+
55
+ def check_database_identifiers!(*strings)
56
+ strings.each do |s|
57
+ next if DATABASE_IDENTIFIER_REGEX =~ s.to_s
58
+ raise ArgumentError, "Invalid database identifier: #{s.inspect}"
59
+ end
60
+ end
61
+
62
+ def _insert_sql(field_names)
63
+ check_database_identifiers! table_name, *field_names
64
+
65
+ placeholders = 0.upto(field_names.count - 1).map { |idx| "$#{idx + 1}" }
66
+
67
+ if column?('created_at')
68
+ field_names << 'created_at'
69
+ placeholders << 'current_timestamp'
70
+ end
71
+
72
+ if column?('updated_at')
73
+ field_names << 'updated_at'
74
+ placeholders << 'current_timestamp'
75
+ end
76
+
77
+ sql = "INSERT INTO #{table_name} (#{field_names.join(',')}) VALUES(#{placeholders.join(',')})"
78
+ sql += " RETURNING #{primary_key.column}" if primary_key.column
79
+ sql
80
+ end
81
+
82
+ public
83
+
84
+ def create!(record)
85
+ keys, values = record.to_a.transpose
86
+
87
+ sql = insert_sql(keys)
88
+ result = raw_connection.exec_params(sql, values)
89
+
90
+ # if we don't have an ID column then the sql does not return any value. The result
91
+ # object would be this: #<PG::Result status=PGRES_COMMAND_OK ntuples=0 nfields=0 cmd_tuples=1>
92
+ # we just return nil in that case; otherwise we return the first entry of the first result row
93
+ # which would be the stringified id.
94
+ first_row = result.each_row.first
95
+ return nil unless first_row
96
+
97
+ id = first_row.first
98
+ primary_key.type == :integer ? Integer(id) : id
99
+ end
100
+ end
101
+ end
102
+ end
@@ -3,15 +3,27 @@ module DbMemoize
3
3
  class << self
4
4
  def create_tables(migration)
5
5
  migration.create_table :memoized_values, id: false do |t|
6
- t.string :entity_table_name
7
- t.integer :entity_id
8
- t.string :method_name
6
+ t.string :entity_table_name, null: false
7
+ t.integer :entity_id, null: false
8
+ t.string :method_name, null: false
9
9
  t.string :arguments_hash
10
10
  t.binary :value
11
- t.datetime :created_at
11
+ t.datetime :created_at, null: false
12
12
  end
13
13
 
14
14
  migration.add_index :memoized_values, [:entity_table_name, :entity_id]
15
+ migrate_empty_arguments_support(migration)
16
+ end
17
+
18
+ def migrate_empty_arguments_support(migration)
19
+ # entity_id/entity_table_name should have a better chance to be useful, since
20
+ # there is more variance in entity_ids than there is in entity_table_names.
21
+ migration.remove_index :memoized_values, [:entity_table_name, :entity_id]
22
+ migration.add_index :memoized_values, [:entity_id, :entity_table_name]
23
+
24
+ # add an index to be useful to look up entries where arguments_hash is NULL.
25
+ # (which is the case for plain attributes of an object)
26
+ migration.execute 'CREATE INDEX memoized_attributes_idx ON memoized_values((arguments_hash IS NULL))'
15
27
  end
16
28
  end
17
29
  end
@@ -8,11 +8,11 @@ module DbMemoize
8
8
  end
9
9
 
10
10
  value = nil
11
- args_hash = ::Digest::MD5.hexdigest(Marshal.dump(args))
11
+ args_hash = Helpers.calculate_arguments_hash(args)
12
12
  cached_value = find_memoized_value(method_name, args_hash)
13
13
 
14
14
  if cached_value
15
- value = Marshal.load(cached_value.value)
15
+ value = Helpers.unmarshal(cached_value.value)
16
16
  Helpers.log(self, method_name, 'cache hit')
17
17
  else
18
18
  time = ::Benchmark.realtime do
@@ -44,7 +44,8 @@ module DbMemoize
44
44
  # autocomplete_info: "my autocomplete_info"
45
45
  #
46
46
  def memoize_values(values, *args)
47
- args_hash = ::Digest::MD5.hexdigest(Marshal.dump(args))
47
+ # [TODO] - when creating many memoized values: should we even support arguments here?
48
+ args_hash = Helpers.calculate_arguments_hash(args)
48
49
 
49
50
  values.each do |name, value|
50
51
  create_memoized_value(name, args_hash, value)
@@ -53,16 +54,14 @@ module DbMemoize
53
54
 
54
55
  private
55
56
 
56
- def create_memoized_value(method_name, args_hash, value)
57
- # [TODO] - It would be nice to have an optimized, pg-based inserter
58
- # here, for up to 10 times speed. However, the memoized_values
59
- # array must then be properly reset.
60
- memoized_values.create!(
61
- entity_table_name: self.class.table_name,
62
- method_name: method_name.to_s,
63
- arguments_hash: args_hash,
64
- value: Marshal.dump(value)
65
- )
57
+ def create_memoized_value(method_name, arguments_hash, value)
58
+ ::DbMemoize::Value.metal.create! entity_table_name: self.class.table_name,
59
+ entity_id: id,
60
+ method_name: method_name.to_s,
61
+ arguments_hash: arguments_hash,
62
+ value: Helpers.marshal(value)
63
+
64
+ @association_cache.delete :memoized_values
66
65
  end
67
66
 
68
67
  def find_memoized_value(method_name, args_hash)
@@ -101,19 +100,18 @@ module DbMemoize
101
100
  end
102
101
 
103
102
  def memoize_values(records_or_ids, values, *args)
103
+ # [TODO] - when creating many memoized values: should we even support arguments here?
104
104
  transaction do
105
- ids = Helpers.find_ids(records_or_ids)
106
- args_hash = ::Digest::MD5.hexdigest(Marshal.dump(args))
105
+ ids = Helpers.find_ids(records_or_ids)
106
+ arguments_hash = Helpers.calculate_arguments_hash(args)
107
107
 
108
108
  ids.each do |id|
109
- values.each do |name, value|
110
- DbMemoize::Value.create!(
111
- entity_table_name: table_name,
112
- entity_id: id,
113
- method_name: name,
114
- arguments_hash: args_hash,
115
- value: Marshal.dump(value)
116
- )
109
+ values.each do |method_name, value|
110
+ ::DbMemoize::Value.metal.create! entity_table_name: table_name,
111
+ entity_id: id,
112
+ method_name: method_name.to_s,
113
+ arguments_hash: arguments_hash,
114
+ value: Helpers.marshal(value)
117
115
  end
118
116
  end
119
117
  end
@@ -1,5 +1,7 @@
1
1
  module DbMemoize
2
2
  class Value < ActiveRecord::Base
3
3
  self.table_name = 'memoized_values'
4
+
5
+ include DbMemoize::Metal
4
6
  end
5
7
  end
@@ -1,3 +1,3 @@
1
1
  module DbMemoize
2
- VERSION = '0.1.6'.freeze
2
+ VERSION = '0.2.1'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: db_memoize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - johannes-kostas goetzinger
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-01-31 00:00:00.000000000 Z
11
+ date: 2017-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '10.5'
55
55
  - !ruby/object:Gem::Dependency
56
- name: sqlite3
56
+ name: pg
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -240,6 +240,7 @@ files:
240
240
  - db_memoize.gemspec
241
241
  - lib/db_memoize.rb
242
242
  - lib/db_memoize/helpers.rb
243
+ - lib/db_memoize/metal.rb
243
244
  - lib/db_memoize/migrations.rb
244
245
  - lib/db_memoize/model.rb
245
246
  - lib/db_memoize/railtie.rb
@@ -247,6 +248,7 @@ files:
247
248
  - lib/db_memoize/version.rb
248
249
  - lib/tasks/clear.rake
249
250
  - lib/tasks/warmup.rake
251
+ - log/.keep
250
252
  homepage: https://github.com/mediapeers/db_memoize
251
253
  licenses:
252
254
  - MIT