property 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,41 +1,51 @@
1
+ == 0.9.0 2010-03-20
2
+
3
+ * 3 major enhancement
4
+ * Added simple index support.
5
+ * Added complex index support.
6
+ * Added simple hooks for indexes when properties are stored in a different model.
7
+
8
+ * 1 minor enhancement
9
+ * Added 'has_column?' to schema.
10
+
1
11
  == 0.8.2 2010-02-16
2
12
 
3
13
  * 2 minor enhancements
4
- * fixed a bug where properties would be dumped even if none changed
5
- * fixed a bug where properties would not be dumped soon enough to mark
6
- storage object as dirty
14
+ * Fixed a bug where properties would be dumped even if none changed.
15
+ * Fixed a bug where properties would not be dumped soon enough to mark
16
+ storage object as dirty.
7
17
 
8
18
  == 0.8.1 2010-02-14
9
19
 
10
20
  * 2 major enhancement
11
- * enabled behavior method definitions
12
- * enabled external storage with 'store_properties_in' method
21
+ * Enabled behavior method definitions.
22
+ * Enabled external storage with 'store_properties_in' method.
13
23
 
14
24
  == 0.8.0 2010-02-11
15
25
 
16
26
  * 3 major enhancements
17
- * enabled Behaviors that can be added on an instance
18
- * enabled non-DB types
19
- * 100% test coverage
27
+ * Enabled Behaviors that can be added on an instance.
28
+ * Enabled non-DB types.
29
+ * 100% test coverage.
20
30
 
21
31
  == 0.7.0 2010-02-11
22
32
 
23
33
  * 2 major enhancement
24
- * enabled instance property definitions
25
- * Time is now natively parsed by json (no typecast)
34
+ * Enabled instance property definitions.
35
+ * Time is now natively parsed by json (no typecast).
26
36
 
27
37
  == 0.6.0 2010-02-11
28
38
 
29
39
  * 1 major enhancement
30
- * enabled ruby accessors in model
40
+ * Enabled ruby accessors in model.
31
41
 
32
42
  == 0.5.0 2010-02-11
33
43
 
34
44
  * 2 major enhancement
35
- * changed plugin into gem
36
- * using Rails columns to handle defaults and type casting
45
+ * Changed plugin into gem.
46
+ * Using Rails columns to handle defaults and type casting.
37
47
 
38
48
  == 0.4.0 2010-02-02
39
49
 
40
50
  * 1 major enhancement
41
- * initial plugin code
51
+ * Initial plugin code.
data/lib/property.rb CHANGED
@@ -5,15 +5,21 @@ require 'property/column'
5
5
  require 'property/behavior'
6
6
  require 'property/schema'
7
7
  require 'property/declaration'
8
+ require 'property/db'
9
+ require 'property/index'
8
10
  require 'property/serialization/json'
9
11
  require 'property/core_ext/time'
10
12
 
11
13
  module Property
12
- VERSION = '0.8.2'
14
+ VERSION = '0.9.0'
13
15
 
14
16
  def self.included(base)
15
17
  base.class_eval do
16
- include ::Property::Attribute
18
+ include Attribute
19
+ include Serialization::JSON
20
+ include Declaration
21
+ include Dirty
22
+ include Index
17
23
  end
18
24
  end
19
25
 
@@ -17,9 +17,6 @@ module Property
17
17
 
18
18
  base.class_eval do
19
19
  include InstanceMethods
20
- include Serialization::JSON
21
- include Declaration
22
- include Dirty
23
20
 
24
21
  store_properties_in self
25
22
 
@@ -17,6 +17,7 @@ module Property
17
17
  def initialize(name)
18
18
  @name = name
19
19
  @included_in_schemas = []
20
+ @group_indexes = []
20
21
  @accessor_module = build_accessor_module
21
22
  end
22
23
 
@@ -25,6 +26,24 @@ module Property
25
26
  @columns ||= {}
26
27
  end
27
28
 
29
+ # Return a list of index definitions in the form [type, key, proc_or_nil]
30
+ def indexes
31
+ columns.values.select do |c|
32
+ c.indexed?
33
+ end.map do |c|
34
+ if c.index == true
35
+ [c.type, c.name]
36
+ else
37
+ [c.type, c.name, c.index]
38
+ end
39
+ end + @group_indexes
40
+ end
41
+
42
+ # Return true if the Behavior contains the given column (property).
43
+ def has_column?(name)
44
+ column_names.include?(name)
45
+ end
46
+
28
47
  # Return the list of column names.
29
48
  def column_names
30
49
  columns.keys
@@ -64,6 +83,12 @@ module Property
64
83
  end
65
84
  end
66
85
 
86
+ # @internal
87
+ def add_index(type, proc)
88
+ # type, key, proc
89
+ @group_indexes << [type, nil, proc]
90
+ end
91
+
67
92
  private
68
93
  def build_accessor_module
69
94
  accessor_module = Module.new
@@ -95,6 +120,21 @@ module Property
95
120
  behavior.add_column(Property::Column.new(name, nil, klass, options))
96
121
  end
97
122
 
123
+ # This is used to create complex indexes with the following syntax:
124
+ #
125
+ # p.index(:text) do |r| # r = record
126
+ # {
127
+ # "high" => "gender:#{r.gender} age:#{r.age} name:#{r.name}",
128
+ # "name_#{r.lang}" => r.name, # multi-lingual index
129
+ # }
130
+ # end
131
+ #
132
+ # The first argument is the type (used to locate the table where the data will be stored) and the block
133
+ # will be yielded with the record and should return a hash of key => value pairs.
134
+ def index(type, &block)
135
+ behavior.add_index(type, block)
136
+ end
137
+
98
138
  alias actions class_eval
99
139
  end
100
140
  end
@@ -6,6 +6,8 @@ module Property
6
6
  # such as name, type and options. It is also used to typecast from strings to
7
7
  # the proper type (date, integer, float, etc).
8
8
  class Column < ::ActiveRecord::ConnectionAdapters::Column
9
+ attr_accessor :index
10
+
9
11
  SAFE_NAMES_REGEXP = %r{\A[a-zA-Z_]+\Z}
10
12
 
11
13
  def initialize(name, default, type, options={})
@@ -28,7 +30,7 @@ module Property
28
30
  end
29
31
 
30
32
  def indexed?
31
- @indexed
33
+ @index
32
34
  end
33
35
 
34
36
  def default_for(owner)
@@ -59,7 +61,7 @@ module Property
59
61
 
60
62
  private
61
63
  def extract_property_options(options)
62
- @indexed = options.delete(:indexed)
64
+ @index = options.delete(:index) || options.delete(:indexed)
63
65
  end
64
66
 
65
67
  def extract_default(default)
@@ -0,0 +1,52 @@
1
+ # FIXME: we should patch the connection adapters instead of having 'case, when' evaluated each time
2
+ # For example:
3
+ # module ActiveRecord
4
+ # module ConnectionAdapters
5
+ # class MysqlAdapter
6
+ # include Zena::Db::MysqlAdditions
7
+ # end
8
+ # end
9
+ # end
10
+
11
+
12
+ module Property
13
+
14
+ # This module is just a helper to fetch raw data from the database and could be removed in future versions of Rails
15
+ # if the framework provides these methods.
16
+ module Db
17
+ extend self
18
+
19
+ def adapter
20
+ connection.class.to_s[/ConnectionAdapters::(.*)Adapter/,1].downcase
21
+ end
22
+
23
+ def execute(*args)
24
+ ActiveRecord::Base.connection.execute(*args)
25
+ end
26
+
27
+ def connection
28
+ ActiveRecord::Base.connection
29
+ end
30
+
31
+ # Insert a list of values (multicolumn insert). The values should be properly escaped before
32
+ # being passed to this method.
33
+ def insert_many(table, columns, values)
34
+ values = values.compact.uniq
35
+ case adapter
36
+ when 'sqlite3'
37
+ pre_query = "INSERT INTO #{table} (#{columns.join(',')}) VALUES "
38
+ values.each do |v|
39
+ execute pre_query + "(#{v.join(',')})"
40
+ end
41
+ else
42
+ values = values.map {|v| "(#{v.join(',')})"}.join(', ')
43
+ execute "INSERT INTO #{table} (#{columns.map{|c| "`#{c}`"}.join(',')}) VALUES #{values}"
44
+ end
45
+ end
46
+
47
+ def fetch_attributes(attributes, table_name, sql)
48
+ sql = "SELECT #{attributes.map{|a| connection.quote_column_name(a)}.join(',')} FROM #{table_name} WHERE #{sql}"
49
+ connection.select_all(sql)
50
+ end
51
+ end # Db
52
+ end # Property
@@ -38,13 +38,18 @@ module Property
38
38
  schema.behave_like behavior
39
39
  end
40
40
 
41
- # Use this class method to declare properties that will be used in your models.
41
+ # Use this class method to declare properties and indexes that will be used in your models.
42
42
  # Example:
43
- # property.string 'phone', :default => ''
43
+ # property.string 'phone', :default => '', :indexed => true
44
44
  #
45
45
  # You can also use a block:
46
46
  # property do |p|
47
47
  # p.string 'phone', 'name', :default => ''
48
+ # p.index(:string) do |r|
49
+ # {
50
+ # "name_#{r.lang}" => r.name,
51
+ # }
52
+ # end
48
53
  # end
49
54
  def property(&block)
50
55
  schema.behavior.property(&block)
@@ -0,0 +1,131 @@
1
+ require 'versions/after_commit' # we need Versions gem's 'after_commit'
2
+
3
+ module Property
4
+
5
+ # Property::Declaration module is used to declare property definitions in a Class. The module
6
+ # also manages property inheritence in sub-classes.
7
+ module Index
8
+
9
+ def self.included(base)
10
+ base.class_eval do
11
+ extend ClassMethods
12
+ include InstanceMethods
13
+ before_save :property_index
14
+ before_destroy :property_index_destroy
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ end
20
+
21
+ module InstanceMethods
22
+
23
+ private
24
+ # Retrieve the current indexes for a given group (:string, :text, etc)
25
+ def get_indexes(group_name)
26
+ return {} if new_record?
27
+ res = {}
28
+ Property::Db.fetch_attributes(['key', 'value'], index_table_name(group_name), index_reader_sql).each do |row|
29
+ res[row['key']] = row['value']
30
+ end
31
+ res
32
+ end
33
+
34
+ def index_reader_sql
35
+ index_reader.map {|k, v| "#{k} = #{self.class.connection.quote(v)}"}.join(' AND ')
36
+ end
37
+
38
+ def index_reader
39
+ {index_foreign_key => self.id}
40
+ end
41
+
42
+ alias index_writer index_reader
43
+
44
+ def index_table_name(group_name)
45
+ "i_#{group_name}_#{self.class.table_name}"
46
+ end
47
+
48
+ def index_foreign_key
49
+ self.class.table_name.singularize.foreign_key
50
+ end
51
+
52
+ # This method prepares the index
53
+ def property_index
54
+ connection = self.class.connection
55
+ reader_sql = index_reader_sql
56
+ foreign_keys = nil
57
+ foreign_values = nil
58
+
59
+ schema.index_groups.each do |group_name, definitions|
60
+ old_indexes = get_indexes(group_name)
61
+ cur_indexes = {}
62
+ definitions.each do |key, proc|
63
+ if key
64
+ value = prop[key]
65
+ if !value.blank?
66
+ if proc
67
+ cur_indexes.merge!(proc.call(self))
68
+ else
69
+ cur_indexes[key] = value
70
+ end
71
+ end
72
+ else
73
+ cur_indexes.merge!(proc.call(self))
74
+ end
75
+ end
76
+
77
+ old_keys = old_indexes.keys
78
+ cur_keys = cur_indexes.keys
79
+
80
+ new_keys = cur_keys - old_keys
81
+ del_keys = old_keys - cur_keys
82
+ upd_keys = cur_keys & old_keys
83
+
84
+ after_commit do
85
+ table_name = index_table_name(group_name)
86
+
87
+ upd_keys.each do |key|
88
+ value = cur_indexes[key]
89
+ if value.blank?
90
+ del_keys << key
91
+ else
92
+ connection.execute "UPDATE #{table_name} SET value = #{connection.quote(cur_indexes[key])} WHERE #{reader_sql} AND key = #{connection.quote(key)}"
93
+ end
94
+ end
95
+
96
+ if !del_keys.empty?
97
+ connection.execute "DELETE FROM #{table_name} WHERE #{reader_sql} AND key IN (#{del_keys.map{|key| connection.quote(key)}.join(',')})"
98
+ end
99
+
100
+ new_keys.reject! {|k| cur_indexes[k].blank? }
101
+ if !new_keys.empty?
102
+ # we evaluate this now to have the id on record creation
103
+ foreign_keys ||= index_writer.keys
104
+ foreign_values ||= foreign_keys.map {|k| index_writer[k]}
105
+
106
+ Property::Db.insert_many(
107
+ table_name,
108
+ foreign_keys + ['key', 'value'],
109
+ new_keys.map do |key|
110
+ foreign_values + [connection.quote(key), connection.quote(cur_indexes[key])]
111
+ end
112
+ )
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ # Remove all index entries on destroy
119
+ def property_index_destroy
120
+ connection = self.class.connection
121
+ foreign_key = index_foreign_key
122
+ current_id = self.id
123
+ schema.index_groups.each do |group_name, definitions|
124
+ after_commit do
125
+ connection.execute "DELETE FROM #{index_table_name(group_name)} WHERE #{foreign_key} = #{current_id}"
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -55,6 +55,15 @@ module Property
55
55
  columns.keys
56
56
  end
57
57
 
58
+ # Return true if the schema has a property with the given name.
59
+ def has_column?(name)
60
+ name = name.to_s
61
+ [@behaviors].flatten.each do |behavior|
62
+ return true if behavior.has_column?(name)
63
+ end
64
+ false
65
+ end
66
+
58
67
  # Return column definitions from all included behaviors.
59
68
  def columns
60
69
  columns = {}
@@ -64,6 +73,17 @@ module Property
64
73
  columns
65
74
  end
66
75
 
76
+ # Return a hash with indexed types as keys and index definitions as values.
77
+ def index_groups
78
+ index_groups = {}
79
+ @behaviors.flatten.uniq.each do |b|
80
+ b.indexes.each do |list|
81
+ (index_groups[list.first] ||= []) << list[1..-1]
82
+ end
83
+ end
84
+ index_groups
85
+ end
86
+
67
87
  private
68
88
  def include_behavior(behavior)
69
89
  return if behaviors.include?(behavior)
data/property.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{property}
8
- s.version = "0.8.2"
8
+ s.version = "0.9.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Renaud Kern", "Gaspard Bucher"]
12
- s.date = %q{2010-02-16}
12
+ s.date = %q{2010-03-20}
13
13
  s.description = %q{Wrap model properties into a single database column and declare properties from within the model.}
14
14
  s.email = %q{gaspard@teti.ch}
15
15
  s.extra_rdoc_files = [
@@ -27,8 +27,10 @@ Gem::Specification.new do |s|
27
27
  "lib/property/behavior.rb",
28
28
  "lib/property/column.rb",
29
29
  "lib/property/core_ext/time.rb",
30
+ "lib/property/db.rb",
30
31
  "lib/property/declaration.rb",
31
32
  "lib/property/dirty.rb",
33
+ "lib/property/index.rb",
32
34
  "lib/property/properties.rb",
33
35
  "lib/property/schema.rb",
34
36
  "lib/property/serialization/json.rb",
@@ -42,6 +44,9 @@ Gem::Specification.new do |s|
42
44
  "test/unit/property/behavior_test.rb",
43
45
  "test/unit/property/declaration_test.rb",
44
46
  "test/unit/property/dirty_test.rb",
47
+ "test/unit/property/index_complex_test.rb",
48
+ "test/unit/property/index_foreign_test.rb",
49
+ "test/unit/property/index_simple_test.rb",
45
50
  "test/unit/property/validation_test.rb",
46
51
  "test/unit/serialization/json_test.rb",
47
52
  "test/unit/serialization/marshal_test.rb",
@@ -51,7 +56,7 @@ Gem::Specification.new do |s|
51
56
  s.rdoc_options = ["--charset=UTF-8"]
52
57
  s.require_paths = ["lib"]
53
58
  s.rubyforge_project = %q{property}
54
- s.rubygems_version = %q{1.3.5}
59
+ s.rubygems_version = %q{1.3.6}
55
60
  s.summary = %q{model properties wrap into a single database column}
56
61
  s.test_files = [
57
62
  "test/fixtures.rb",
@@ -61,6 +66,9 @@ Gem::Specification.new do |s|
61
66
  "test/unit/property/behavior_test.rb",
62
67
  "test/unit/property/declaration_test.rb",
63
68
  "test/unit/property/dirty_test.rb",
69
+ "test/unit/property/index_complex_test.rb",
70
+ "test/unit/property/index_foreign_test.rb",
71
+ "test/unit/property/index_simple_test.rb",
64
72
  "test/unit/property/validation_test.rb",
65
73
  "test/unit/serialization/json_test.rb",
66
74
  "test/unit/serialization/marshal_test.rb",
data/test/fixtures.rb CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  class Employee < ActiveRecord::Base
3
3
  include Property
4
- property.string 'first_name', :default => '', :indexed => true
5
- property.string 'last_name', :default => '', :indexed => true
4
+ property.string 'first_name', :default => '', :index => true
5
+ property.string 'last_name', :default => '', :index => true
6
6
  property.float 'age'
7
7
  end
8
8
 
@@ -46,25 +46,51 @@ end
46
46
  begin
47
47
  class PropertyMigration < ActiveRecord::Migration
48
48
  def self.down
49
- drop_table "employees"
50
- drop_table "versions"
49
+ drop_table 'employees'
50
+ drop_table 'versions'
51
+ drop_table 'string_index'
51
52
  end
53
+
52
54
  def self.up
53
- create_table "employees" do |t|
54
- t.string "type"
55
- t.text "properties"
55
+ create_table 'employees' do |t|
56
+ t.string 'type'
57
+ t.text 'properties'
56
58
  end
57
59
 
58
- create_table "versions" do |t|
60
+ create_table 'versions' do |t|
59
61
  t.integer 'employee_id'
60
- t.string "properties"
61
- t.string "title"
62
- t.string "comment"
62
+ t.string 'properties'
63
+ t.string 'title'
64
+ t.string 'comment'
65
+ t.string 'lang'
63
66
  t.timestamps
64
67
  end
65
68
 
66
- create_table "dummies" do |t|
67
- t.text "properties"
69
+ create_table 'dummies' do |t|
70
+ t.text 'properties'
71
+ end
72
+
73
+ # index strings in employees
74
+ create_table 'i_string_employees' do |t|
75
+ t.integer 'employee_id'
76
+ t.integer 'version_id'
77
+ t.string 'key'
78
+ t.string 'value'
79
+ end
80
+
81
+ # index integer in employees
82
+ create_table 'i_integer_employees' do |t|
83
+ t.integer 'employee_id'
84
+ t.integer 'version_id'
85
+ t.string 'key'
86
+ t.integer 'value'
87
+ end
88
+
89
+ # index text in employees
90
+ create_table 'i_text_employees' do |t|
91
+ t.integer 'employee_id'
92
+ t.string 'key'
93
+ t.text 'value'
68
94
  end
69
95
  end
70
96
  end
data/test/test_helper.rb CHANGED
@@ -7,14 +7,4 @@ require 'active_record'
7
7
  require 'property'
8
8
  require 'shoulda_macros/serialization'
9
9
  require 'fixtures'
10
-
11
- class Test::Unit::TestCase
12
-
13
- def assert_attribute(value, attr_name, object=subject)
14
- assert_equal value, object.send(attr_name)
15
- assert_equal value, object[attr_name]
16
- assert_equal value, object.attributes[attr_name]
17
- assert_equal value, object.properties=erties[attr_name]
18
- end
19
-
20
- end
10
+ require 'active_support/test_case'
@@ -52,11 +52,17 @@ class BehaviorTest < Test::Unit::TestCase
52
52
  assert_equal 10, column.default
53
53
  end
54
54
 
55
- should 'allow indexed option' do
56
- subject.property.string('rolodex', :indexed => true)
55
+ should 'allow index option' do
56
+ subject.property.string('rolodex', :index => true)
57
57
  column = subject.columns['rolodex']
58
58
  assert column.indexed?
59
59
  end
60
+
61
+ should 'return a list of indexes on indexes' do
62
+ subject.property.string('rolodex', :index => true)
63
+ subject.property.integer('foobar', :index => true)
64
+ assert_equal %w{integer string}, subject.indexes.map {|i| i[0].to_s }.sort
65
+ end
60
66
  end # A Behavior
61
67
 
62
68
  context 'Adding a behavior' do
@@ -13,8 +13,12 @@ class DeclarationTest < Test::Unit::TestCase
13
13
  assert_equal %w{age first_name language last_name}, @klass.schema.column_names.sort
14
14
  end
15
15
 
16
+ should 'see its property columns in schema' do
17
+ assert @klass.schema.has_column?('language')
18
+ end
19
+
16
20
  should 'not back-propagate definitions to parent' do
17
- assert !@klass.superclass.schema.columns.include?('language')
21
+ assert !@klass.superclass.schema.has_column?('language')
18
22
  end
19
23
 
20
24
  should 'inherit new definitions in parent' do
@@ -157,8 +161,8 @@ class DeclarationTest < Test::Unit::TestCase
157
161
  assert_equal 10, column.default
158
162
  end
159
163
 
160
- should 'allow indexed option' do
161
- subject.property.string('rolodex', :indexed => true)
164
+ should 'allow index option' do
165
+ subject.property.string('rolodex', :index => true)
162
166
  column = subject.schema.columns['rolodex']
163
167
  assert column.indexed?
164
168
  end
@@ -225,7 +229,7 @@ class DeclarationTest < Test::Unit::TestCase
225
229
  :foreign_key => 'employee_id'
226
230
  end
227
231
 
228
- Contact = Class.new(ActiveRecord::Base) do
232
+ class Contact < ActiveRecord::Base
229
233
  attr_accessor :assertion
230
234
  before_save :before_save_assertion
231
235
  set_table_name :employees
@@ -0,0 +1,153 @@
1
+ require 'test_helper'
2
+ require 'fixtures'
3
+
4
+ class IndexComplexTest < ActiveSupport::TestCase
5
+ class IndexedStringEmp < ActiveRecord::Base
6
+ set_table_name :i_string_employees
7
+ end
8
+
9
+ class IndexedIntegerEmp < ActiveRecord::Base
10
+ set_table_name :i_integer_employees
11
+ end
12
+
13
+ class IndexedTextEmp < ActiveRecord::Base
14
+ set_table_name :i_text_employees
15
+ end
16
+
17
+ # Complex index definition class
18
+ class Person < ActiveRecord::Base
19
+ include Property
20
+ set_table_name :employees
21
+
22
+ def save_with_raise
23
+ if name == 'raise'
24
+ raise Exception.new
25
+ else
26
+ save_without_raise
27
+ end
28
+ end
29
+ alias_method_chain :save, :raise
30
+
31
+ property do |p|
32
+ p.string 'name'
33
+ # only runs if 'age' is not blank
34
+ p.integer 'age', :index => Proc.new {|r| {'age' => r.age == 0 ? nil : r.age + 10}}
35
+ p.string 'gender'
36
+ p.string 'lang'
37
+
38
+ p.index(:text) do |r| # r = record
39
+ {
40
+ "high" => "gender:#{r.gender} age:#{r.age} name:#{r.name}",
41
+ "name_#{r.lang}" => r.name, # multi-lingual index
42
+ }
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'A schema from a class with complex index definitions' do
48
+ subject do
49
+ Person.schema
50
+ end
51
+
52
+ should 'return a Hash on index_groups' do
53
+ assert_kind_of Hash, subject.index_groups
54
+ end
55
+
56
+ should 'group indexes by type' do
57
+ assert_equal %w{integer text}, subject.index_groups.keys.map(&:to_s).sort
58
+ end
59
+ end
60
+
61
+ context 'A class with complex index definition' do
62
+ subject do
63
+ Person
64
+ end
65
+
66
+ context 'on record creation' do
67
+ should 'create index entries' do
68
+ assert_difference('IndexedTextEmp.count', 2) do
69
+ Person.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
70
+ end
71
+ end
72
+
73
+ should 'not create index entries for blank values' do
74
+ assert_difference('IndexedIntegerEmp.count', 0) do
75
+ Person.create('name' => 'Pavlov')
76
+ end
77
+ end
78
+
79
+ should 'store key and value pairs linked to the model' do
80
+ person = Person.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
81
+ high_index, name_index = IndexedTextEmp.all(:conditions => {:employee_id => person.id}, :order => 'key asc')
82
+ assert_equal 'high', high_index.key
83
+ assert_equal 'gender:M age:34 name:Juan', high_index.value
84
+ assert_equal 'name_es', name_index.key
85
+ assert_equal 'Juan', name_index.value
86
+ end
87
+
88
+ should 'execute index Proc to build value' do
89
+ person = Person.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
90
+ int_index = IndexedIntegerEmp.first(:conditions => {:employee_id => person.id})
91
+ assert_equal 44, int_index.value
92
+ end
93
+
94
+ should 'remove blank values built from proc execution' do
95
+ person = Person.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
96
+ assert_difference('IndexedIntegerEmp.count', -1) do
97
+ person.update_attributes('age' => 0)
98
+ end
99
+ end
100
+ end
101
+
102
+ context 'on record update' do
103
+ setup do
104
+ @person = Person.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
105
+ end
106
+
107
+ should 'update index entries' do
108
+ high_index, name_index = IndexedTextEmp.all(:conditions => {:employee_id => @person.id}, :order => 'key asc')
109
+ assert_difference('IndexedTextEmp.count', 0) do
110
+ @person.update_attributes('name' => 'Xavier')
111
+ end
112
+
113
+ high_index = IndexedTextEmp.find(high_index.id) # reload (make sure the record has been updated, not recreated)
114
+ name_index = IndexedTextEmp.find(name_index.id) # reload (make sure the record has been updated, not recreated)
115
+
116
+ assert_equal 'high', high_index.key
117
+ assert_equal 'gender:M age:34 name:Xavier', high_index.value
118
+ assert_equal 'name_es', name_index.key
119
+ assert_equal 'Xavier', name_index.value
120
+ end
121
+
122
+ context 'with key alterations' do
123
+ should 'remove and create new keys' do
124
+ high_index, name_index = IndexedTextEmp.all(:conditions => {:employee_id => @person.id}, :order => 'key asc')
125
+ assert_difference('IndexedTextEmp.count', 0) do
126
+ @person.update_attributes('lang' => 'en', 'name' => 'John')
127
+ end
128
+
129
+ assert IndexedTextEmp.find(high_index.id)
130
+ assert_nil IndexedTextEmp.find_by_id(name_index.id)
131
+
132
+ high_index, name_index = IndexedTextEmp.all(:conditions => {:employee_id => @person.id}, :order => 'key asc')
133
+
134
+ assert_equal 'high', high_index.key
135
+ assert_equal 'gender:M age:34 name:John', high_index.value
136
+ assert_equal 'name_en', name_index.key
137
+ assert_equal 'John', name_index.value
138
+ end
139
+ end
140
+ end
141
+
142
+ context 'on record destruction' do
143
+ should 'remove index entries' do
144
+ person = Person.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
145
+ assert_difference('IndexedTextEmp.count', -2) do
146
+ assert_difference('IndexedIntegerEmp.count', -1) do
147
+ person.destroy
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,232 @@
1
+ require 'test_helper'
2
+ require 'fixtures'
3
+
4
+ class IndexForeignTest < ActiveSupport::TestCase
5
+ class IndexedStringEmp < ActiveRecord::Base
6
+ set_table_name :i_string_employees
7
+ end
8
+
9
+ class IndexedIntegerEmp < ActiveRecord::Base
10
+ set_table_name :i_integer_employees
11
+ end
12
+
13
+ class IndexedTextEmp < ActiveRecord::Base
14
+ set_table_name :i_text_employees
15
+ end
16
+
17
+ class Version < ActiveRecord::Base
18
+ belongs_to :contact, :class_name => 'IndexForeignTest::Contact',
19
+ :foreign_key => 'employee_id'
20
+ end
21
+
22
+ class Contact < ActiveRecord::Base
23
+ set_table_name :employees
24
+
25
+ has_many :versions, :class_name => 'IndexForeignTest::Version'
26
+ def version
27
+ @version ||= begin
28
+ if new_record?
29
+ versions.build
30
+ else
31
+ Version.first(:conditions => ['employee_id = ?', self.id]) || versions.build
32
+ end
33
+ end
34
+ end
35
+
36
+ def new_version!
37
+ @version = versions.build
38
+ end
39
+
40
+ def lang=(l)
41
+ version.lang = l
42
+ end
43
+
44
+ include Property
45
+ store_properties_in :version
46
+
47
+ property do |p|
48
+ p.string 'name'
49
+ p.integer 'age', :indexed => true
50
+ p.string 'gender'
51
+
52
+ p.index(:string) do |r| # r = record
53
+ {
54
+ "high" => "gender:#{r.gender} age:#{r.age} name:#{r.name}",
55
+ "name_#{r.version.lang}" => r.name, # multi-lingual index
56
+ }
57
+ end
58
+ end
59
+
60
+ def index_reader
61
+ {'version_id' => version.id}
62
+ end
63
+
64
+ # Foreign index: we store the 'employee_id' in the index to get back directly to non-versioned class Contact (through employee_id key).
65
+ def index_writer
66
+ {'version_id' => version.id, 'employee_id' => self.id}
67
+ end
68
+ end
69
+
70
+ context 'A class with foreign index definition' do
71
+ subject do
72
+ Contact
73
+ end
74
+
75
+ context 'on record creation' do
76
+ should 'create index entries' do
77
+ assert_difference('IndexedStringEmp.count', 2) do
78
+ Contact.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
79
+ end
80
+ end
81
+
82
+ should 'store key and value pairs linked to the model' do
83
+ person = Contact.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
84
+ high_index, name_index = IndexedStringEmp.all(:conditions => {:version_id => person.version.id}, :order => 'key asc')
85
+ assert_equal 'high', high_index.key
86
+ assert_equal 'gender:M age:34 name:Juan', high_index.value
87
+ assert_equal 'name_es', name_index.key
88
+ assert_equal 'Juan', name_index.value
89
+ end
90
+
91
+ should 'store key and value pairs linked to the foreign model' do
92
+ person = Contact.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
93
+ high_index, name_index = IndexedStringEmp.all(:conditions => {:employee_id => person.id}, :order => 'key asc')
94
+ assert_equal 'high', high_index.key
95
+ assert_equal 'gender:M age:34 name:Juan', high_index.value
96
+ assert_equal 'name_es', name_index.key
97
+ assert_equal 'Juan', name_index.value
98
+ end
99
+ end
100
+
101
+ context 'on record update' do
102
+ setup do
103
+ @person = Contact.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
104
+ end
105
+
106
+ should 'update index entries' do
107
+ high_index, name_index = IndexedStringEmp.all(:conditions => {:employee_id => @person.id}, :order => 'key asc')
108
+ assert_difference('IndexedStringEmp.count', 0) do
109
+ @person.update_attributes('name' => 'Xavier')
110
+ end
111
+
112
+ high_index = IndexedStringEmp.find(high_index.id) # reload (make sure the record has been updated, not recreated)
113
+ name_index = IndexedStringEmp.find(name_index.id) # reload (make sure the record has been updated, not recreated)
114
+
115
+ assert_equal 'high', high_index.key
116
+ assert_equal 'gender:M age:34 name:Xavier', high_index.value
117
+ assert_equal 'name_es', name_index.key
118
+ assert_equal 'Xavier', name_index.value
119
+ end
120
+
121
+ context 'with key alterations' do
122
+ should 'remove and create new keys' do
123
+ high_index, name_index = IndexedStringEmp.all(:conditions => {:employee_id => @person.id}, :order => 'key asc')
124
+ assert_difference('IndexedStringEmp.count', 0) do
125
+ @person.update_attributes('lang' => 'en', 'name' => 'John')
126
+ end
127
+
128
+ assert IndexedStringEmp.find(high_index.id)
129
+ assert_nil IndexedStringEmp.find_by_id(name_index.id)
130
+
131
+ high_index, name_index = IndexedStringEmp.all(:conditions => {:employee_id => @person.id}, :order => 'key asc')
132
+
133
+ assert_equal 'high', high_index.key
134
+ assert_equal 'gender:M age:34 name:John', high_index.value
135
+ assert_equal 'name_en', name_index.key
136
+ assert_equal 'John', name_index.value
137
+ end
138
+ end
139
+ end
140
+
141
+ context 'on record update with a new version' do
142
+ should 'create new index entries' do
143
+ @person = Contact.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
144
+ high_index1, name_index1 = IndexedStringEmp.all(:conditions => {:version_id => @person.version.id}, :order => 'key asc')
145
+ @person.new_version!
146
+ assert_difference('IndexedStringEmp.count', 2) do
147
+ @person.update_attributes('name' => 'John', 'lang' => 'en')
148
+ end
149
+
150
+ high_index, name_index = IndexedStringEmp.all(:conditions => {:version_id => @person.version.id}, :order => 'key asc')
151
+ assert_not_equal high_index1.id, high_index.id
152
+ assert_not_equal name_index1.id, name_index.id
153
+
154
+ assert_equal 'high', high_index.key
155
+ assert_equal 'gender:M age:34 name:John', high_index.value
156
+ assert_equal 'name_en', name_index.key
157
+ assert_equal 'John', name_index.value
158
+ end
159
+
160
+ # ========== The context below is not really a test: it is used to example the index usage to sort
161
+ context 'in different languages' do
162
+ setup do
163
+ Contact.destroy_all
164
+ # People: Jean (John) and Jim
165
+ # sort order:
166
+ # fr: Jean, Jim
167
+ # en: Jim, John
168
+ @jean = Contact.create('name' => 'Jean', 'lang' => 'fr', 'gender' => 'M', 'age' => 34)
169
+ @jean.new_version!
170
+ @jean.update_attributes('name' => 'John', 'lang' => 'en')
171
+ @jim = Contact.create('name' => 'Jim', 'lang' => 'fr', 'gender' => 'M', 'age' => 17)
172
+ @jim.new_version!
173
+ @jim.update_attributes('name' => 'Jim', 'lang' => 'en')
174
+ end
175
+
176
+ should 'create index entries to sort multilingual values' do
177
+ people_fr = Contact.find(:all, :joins => "INNER JOIN i_string_employees AS ise ON ise.employee_id = employees.id AND ise.key = 'name_fr'",
178
+ :order => "ise.value asc")
179
+
180
+ people_en = Contact.find(:all, :joins => "INNER JOIN i_string_employees AS ise ON ise.employee_id = employees.id AND ise.key = 'name_en'",
181
+ :order => "ise.value asc")
182
+
183
+ assert_equal [@jean.id, @jim.id], people_fr.map {|r| r.id}
184
+ assert_equal [@jim.id, @jean.id], people_en.map {|r| r.id}
185
+ end
186
+ end
187
+
188
+ # ========== The context below is not really a test: it is used to example the index usage to sort
189
+ context 'in different languages with missing translations' do
190
+ setup do
191
+ Contact.destroy_all
192
+ # People: Jean (John) and Jim
193
+ # sort order:
194
+ # fr: Jean, Jim
195
+ # en: Jim, John
196
+ @jean = Contact.create('name' => 'Jean', 'lang' => 'fr', 'gender' => 'M', 'age' => 34)
197
+ @jean.new_version!
198
+ @jean.update_attributes('name' => 'John', 'lang' => 'en')
199
+ @jim = Contact.create('name' => 'Jim', 'lang' => 'en', 'gender' => 'M', 'age' => 17)
200
+ # no version for @jim in 'fr'
201
+ end
202
+
203
+ should 'create index entries to sort multilingual values' do
204
+ people_fr = Contact.find(:all, :joins => "INNER JOIN i_string_employees AS ise ON ise.employee_id = employees.id AND ise.key = 'name_fr'",
205
+ :order => "ise.value asc")
206
+
207
+ people_en = Contact.find(:all, :joins => "INNER JOIN i_string_employees AS ise ON ise.employee_id = employees.id AND ise.key = 'name_en'",
208
+ :order => "ise.value asc")
209
+
210
+ # This is what we would like to have (once we have found an SQL trick to get the record in 'en')
211
+ # assert_equal [@jean.id, @jim.id], people_fr.map {|r| r.id}
212
+
213
+ # But this is what we get
214
+ assert_equal [@jean.id], people_fr.map {|r| r.id}
215
+
216
+ assert_equal [@jim.id, @jean.id], people_en.map {|r| r.id}
217
+ end
218
+ end
219
+ end
220
+
221
+ context 'on record destruction' do
222
+ should 'remove index entries' do
223
+ person = Contact.create('name' => 'Juan', 'lang' => 'es', 'gender' => 'M', 'age' => 34)
224
+ assert_difference('IndexedStringEmp.count', -2) do
225
+ assert_difference('IndexedIntegerEmp.count', -1) do
226
+ person.destroy
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,145 @@
1
+ require 'test_helper'
2
+ require 'fixtures'
3
+
4
+ class IndexSimpleTest < ActiveSupport::TestCase
5
+ class IndexedStringEmp < ActiveRecord::Base
6
+ set_table_name :i_string_employees
7
+ end
8
+
9
+ class IndexedIntegerEmp < ActiveRecord::Base
10
+ set_table_name :i_integer_employees
11
+ end
12
+
13
+ # Simple index definition class
14
+ class Dog < ActiveRecord::Base
15
+ include Property
16
+ set_table_name :employees
17
+
18
+ def save_with_raise
19
+ if name == 'raise'
20
+ raise Exception.new
21
+ else
22
+ save_without_raise
23
+ end
24
+ end
25
+ alias_method_chain :save, :raise
26
+
27
+ property do |p|
28
+ p.string 'name', :index => true
29
+ p.integer 'age', :indexed => true # synonym
30
+ end
31
+ end
32
+
33
+ context 'A schema from a class with index definitions' do
34
+ subject do
35
+ Dog.schema
36
+ end
37
+
38
+ should 'return a Hash on index_groups' do
39
+ assert_kind_of Hash, subject.index_groups
40
+ end
41
+
42
+ should 'group indexes by type' do
43
+ assert_equal %w{integer string}, subject.index_groups.keys.map(&:to_s).sort
44
+ end
45
+ end
46
+
47
+ context 'A class with a simple index definition' do
48
+ subject do
49
+ Dog
50
+ end
51
+
52
+ context 'on record creation' do
53
+ should 'create index entries' do
54
+ assert_difference('IndexedStringEmp.count', 1) do
55
+ Dog.create('name' => 'Pavlov')
56
+ end
57
+ end
58
+
59
+ should 'not create index entries for blank values' do
60
+ assert_difference('IndexedIntegerEmp.count', 0) do
61
+ Dog.create('name' => 'Pavlov')
62
+ end
63
+ end
64
+
65
+ should 'store a key and value pair linked to the model' do
66
+ dog = Dog.create('name' => 'Pavlov')
67
+ index_string = IndexedStringEmp.first(:conditions => {:employee_id => dog.id})
68
+ assert_equal 'Pavlov', index_string.value
69
+ assert_equal 'name', index_string.key
70
+ end
71
+ end
72
+
73
+ context 'on record update' do
74
+ setup do
75
+ @dog = Dog.create('name' => 'Pavlov')
76
+ end
77
+
78
+ should 'update index entries' do
79
+ index_string = IndexedStringEmp.first(:conditions => {:employee_id => @dog.id})
80
+ assert_difference('IndexedStringEmp.count', 0) do
81
+ @dog.update_attributes('name' => 'Médor')
82
+ end
83
+
84
+ index_string = IndexedStringEmp.find(index_string.id)
85
+ assert_equal 'Médor', index_string.value
86
+ assert_equal 'name', index_string.key
87
+ end
88
+
89
+ should 'not create index entries for blank values' do
90
+ assert_difference('IndexedIntegerEmp.count', 0) do
91
+ @dog.update_attributes('name' => 'Médor')
92
+ end
93
+ end
94
+
95
+ should 'remove blank values' do
96
+ assert_difference('IndexedStringEmp.count', -1) do
97
+ @dog.update_attributes('name' => '')
98
+ end
99
+ end
100
+
101
+ should 'create new entries for new keys' do
102
+ assert_difference('IndexedIntegerEmp.count', 1) do
103
+ @dog.update_attributes('age' => 7)
104
+ end
105
+ end
106
+
107
+ should 'store a key and value pair linked to the model' do
108
+ @dog.update_attributes('age' => 7)
109
+ index_int = IndexedIntegerEmp.first(:conditions => {:employee_id => @dog.id})
110
+ assert_equal 7, index_int.value
111
+ assert_equal 'age', index_int.key
112
+ end
113
+
114
+ context 'that fails during save' do
115
+ setup do
116
+ @dog = Dog.create('name' => 'Pavlov')
117
+ end
118
+
119
+ should 'not alter indexes' do
120
+ assert_difference('IndexedIntegerEmp.count', 0) do
121
+ assert_raises(Exception) do
122
+ @dog.update_attributes('name' => 'raise')
123
+ end
124
+ end
125
+
126
+ index_string = IndexedStringEmp.first(:conditions => {:employee_id => @dog.id})
127
+ assert_equal 'Pavlov', index_string.value
128
+ assert_equal 'name', index_string.key
129
+ end
130
+
131
+ end
132
+ end
133
+
134
+ context 'on record destruction' do
135
+ should 'remove index entries' do
136
+ dog = Dog.create('name' => 'Pavlov', 'age' => 7)
137
+ assert_difference('IndexedStringEmp.count', -1) do
138
+ assert_difference('IndexedIntegerEmp.count', -1) do
139
+ dog.destroy
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -68,11 +68,11 @@ class ValidationTest < Test::Unit::TestCase
68
68
  end # When setting a property
69
69
 
70
70
  context 'On a class with default property values' do
71
- Cat = Class.new(ActiveRecord::Base) do
71
+ class Cat < ActiveRecord::Base
72
72
  attr_accessor :encoding
73
-
74
73
  set_table_name 'dummies'
75
- include Property::Attribute
74
+
75
+ include Property
76
76
  property do |p|
77
77
  p.string 'eat', :default => 'mouse'
78
78
  p.string 'name'
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: property
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 9
8
+ - 0
9
+ version: 0.9.0
5
10
  platform: ruby
6
11
  authors:
7
12
  - Renaud Kern
@@ -10,29 +15,33 @@ autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
17
 
13
- date: 2010-02-16 00:00:00 +01:00
18
+ date: 2010-03-20 00:00:00 +01:00
14
19
  default_executable:
15
20
  dependencies:
16
21
  - !ruby/object:Gem::Dependency
17
22
  name: shoulda
18
- type: :development
19
- version_requirement:
20
- version_requirements: !ruby/object:Gem::Requirement
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
21
25
  requirements:
22
26
  - - ">="
23
27
  - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
24
30
  version: "0"
25
- version:
31
+ type: :development
32
+ version_requirements: *id001
26
33
  - !ruby/object:Gem::Dependency
27
34
  name: activerecord
28
- type: :runtime
29
- version_requirement:
30
- version_requirements: !ruby/object:Gem::Requirement
35
+ prerelease: false
36
+ requirement: &id002 !ruby/object:Gem::Requirement
31
37
  requirements:
32
38
  - - ">="
33
39
  - !ruby/object:Gem::Version
40
+ segments:
41
+ - 0
34
42
  version: "0"
35
- version:
43
+ type: :runtime
44
+ version_requirements: *id002
36
45
  description: Wrap model properties into a single database column and declare properties from within the model.
37
46
  email: gaspard@teti.ch
38
47
  executables: []
@@ -53,8 +62,10 @@ files:
53
62
  - lib/property/behavior.rb
54
63
  - lib/property/column.rb
55
64
  - lib/property/core_ext/time.rb
65
+ - lib/property/db.rb
56
66
  - lib/property/declaration.rb
57
67
  - lib/property/dirty.rb
68
+ - lib/property/index.rb
58
69
  - lib/property/properties.rb
59
70
  - lib/property/schema.rb
60
71
  - lib/property/serialization/json.rb
@@ -68,6 +79,9 @@ files:
68
79
  - test/unit/property/behavior_test.rb
69
80
  - test/unit/property/declaration_test.rb
70
81
  - test/unit/property/dirty_test.rb
82
+ - test/unit/property/index_complex_test.rb
83
+ - test/unit/property/index_foreign_test.rb
84
+ - test/unit/property/index_simple_test.rb
71
85
  - test/unit/property/validation_test.rb
72
86
  - test/unit/serialization/json_test.rb
73
87
  - test/unit/serialization/marshal_test.rb
@@ -85,18 +99,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
85
99
  requirements:
86
100
  - - ">="
87
101
  - !ruby/object:Gem::Version
102
+ segments:
103
+ - 0
88
104
  version: "0"
89
- version:
90
105
  required_rubygems_version: !ruby/object:Gem::Requirement
91
106
  requirements:
92
107
  - - ">="
93
108
  - !ruby/object:Gem::Version
109
+ segments:
110
+ - 0
94
111
  version: "0"
95
- version:
96
112
  requirements: []
97
113
 
98
114
  rubyforge_project: property
99
- rubygems_version: 1.3.5
115
+ rubygems_version: 1.3.6
100
116
  signing_key:
101
117
  specification_version: 3
102
118
  summary: model properties wrap into a single database column
@@ -108,6 +124,9 @@ test_files:
108
124
  - test/unit/property/behavior_test.rb
109
125
  - test/unit/property/declaration_test.rb
110
126
  - test/unit/property/dirty_test.rb
127
+ - test/unit/property/index_complex_test.rb
128
+ - test/unit/property/index_foreign_test.rb
129
+ - test/unit/property/index_simple_test.rb
111
130
  - test/unit/property/validation_test.rb
112
131
  - test/unit/serialization/json_test.rb
113
132
  - test/unit/serialization/marshal_test.rb