property 1.0.0 → 1.1.0

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.
data/History.txt CHANGED
@@ -1,3 +1,11 @@
1
+ == 1.1.0 2010-07-22
2
+
3
+ * Major enhancements
4
+ * Storing original role in column.
5
+ * Using 'idx_' as prefix by default instead of 'i_'.
6
+ * Pluralizing index table names.
7
+ * Added 'index_group' setting to use with Proc indexes on properties.
8
+
1
9
  == 1.0.0 2010-05-27
2
10
 
3
11
  * Major enhancements
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'pathname'
2
2
  $LOAD_PATH.unshift((Pathname(__FILE__).dirname + 'lib').expand_path)
3
3
 
4
- require 'property'
4
+ require 'property/version'
5
5
  require 'rake'
6
6
  require 'rake/testtask'
7
7
 
@@ -15,6 +15,7 @@ module Property
15
15
  if type.kind_of?(Class)
16
16
  @klass = type
17
17
  end
18
+
18
19
  super(name, default, type, options)
19
20
  extract_property_options(options)
20
21
  end
@@ -44,10 +45,16 @@ module Property
44
45
  end
45
46
  end
46
47
 
48
+ # Return the class related to the type of property stored (String, Integer, etc).
47
49
  def klass
48
50
  @klass || super
49
51
  end
50
52
 
53
+ # Return the role in which the column was originally defined.
54
+ def role
55
+ @role
56
+ end
57
+
51
58
  # Property type used instead of 'type' when column is stored
52
59
  alias ptype type
53
60
 
@@ -62,16 +69,23 @@ module Property
62
69
  end
63
70
  end
64
71
 
72
+ # Return the Proc to use to build index (usually nil).
73
+ def index_proc
74
+ @index_proc
75
+ end
76
+
65
77
  private
66
78
  def extract_property_options(options)
67
79
  @index = options.delete(:index) || options.delete(:indexed)
80
+ @role = options.delete(:role)
68
81
  if @index == true
69
- @index = ptype
70
- end
71
-
72
- if @index.blank?
82
+ @index = (options.delete(:index_group) || ptype).to_s
83
+ elsif @index.kind_of?(Proc)
84
+ @index_proc = @index
85
+ @index = (options.delete(:index_group) || ptype).to_s
86
+ elsif @index.blank?
73
87
  @index = nil
74
- elsif @index.kind_of?(Symbol)
88
+ else
75
89
  @index = @index.to_s
76
90
  end
77
91
  end
data/lib/property/db.rb CHANGED
@@ -28,24 +28,33 @@ module Property
28
28
  ActiveRecord::Base.connection
29
29
  end
30
30
 
31
+ def quote(value)
32
+ connection.quote(value)
33
+ end
34
+
31
35
  # Insert a list of values (multicolumn insert). The values should be properly escaped before
32
36
  # being passed to this method.
33
37
  def insert_many(table, columns, values)
34
- values = values.compact.uniq
38
+ values = values.compact.uniq.map do |list|
39
+ list.map {|e| quote(e)}
40
+ end
41
+
42
+ columns = columns.map {|e| connection.quote_column_name(e)}.join(',')
43
+
35
44
  case adapter
36
45
  when 'sqlite3'
37
- pre_query = "INSERT INTO #{table} (#{columns.join(',')}) VALUES "
46
+ pre_query = "INSERT INTO #{table} (#{columns}) VALUES "
38
47
  values.each do |v|
39
48
  execute pre_query + "(#{v.join(',')})"
40
49
  end
41
50
  else
42
51
  values = values.map {|v| "(#{v.join(',')})"}.join(', ')
43
- execute "INSERT INTO #{table} (#{columns.map{|c| "`#{c}`"}.join(',')}) VALUES #{values}"
52
+ execute "INSERT INTO #{table} (#{columns}) VALUES #{values}"
44
53
  end
45
54
  end
46
55
 
47
56
  def fetch_attributes(attributes, table_name, sql)
48
- sql = "SELECT #{attributes.map{|a| connection.quote_column_name(a)}.join(',')} FROM #{table_name} WHERE #{sql}"
57
+ sql = "SELECT #{attributes.map {|a| connection.quote_column_name(a)}.join(',')} FROM #{table_name} WHERE #{sql}"
49
58
  connection.select_all(sql)
50
59
  end
51
60
  end # Db
@@ -3,6 +3,7 @@ module Property
3
3
  # Property::Declaration module is used to declare property definitions in a Class. The module
4
4
  # also manages property inheritence in sub-classes.
5
5
  module Index
6
+ KEY = Property::Db.connection.quote_column_name('key')
6
7
 
7
8
  def self.included(base)
8
9
  base.class_eval do
@@ -14,16 +15,25 @@ module Property
14
15
  end
15
16
 
16
17
  module ClassMethods
18
+
19
+ # Return the table name for the given index group. Produces something like 'idx_pages_strings'.
20
+ def index_table_name(group_name)
21
+ "idx_#{table_name}_#{group_name}s"
22
+ end
17
23
  end
18
24
 
19
25
  module InstanceMethods
20
26
 
27
+ def rebuild_index!
28
+ property_index
29
+ end
30
+
21
31
  private
22
32
  # Retrieve the current indices for a given group (:string, :text, etc)
23
33
  def get_indices(group_name)
24
34
  return {} if new_record?
25
35
  res = {}
26
- Property::Db.fetch_attributes(['key', 'value'], index_table_name(group_name), index_reader_sql(group_name)).each do |row|
36
+ Property::Db.fetch_attributes(%w{key value}, index_table_name(group_name), index_reader_sql(group_name)).each do |row|
27
37
  res[row['key']] = row['value']
28
38
  end
29
39
  res
@@ -34,13 +44,13 @@ module Property
34
44
  if k == :with
35
45
  v.map do |subk, subv|
36
46
  if subv.kind_of?(Array)
37
- "`#{subk}` IN (#{subv.map {|ssubv| connection.quote(ssubv)}.join(',')})"
47
+ "#{subk} IN (#{subv.map {|ssubv| connection.quote(ssubv)}.join(',')})"
38
48
  else
39
- "`#{subk}` = #{self.class.connection.quote(subv)}"
49
+ "#{subk} = #{self.class.connection.quote(subv)}"
40
50
  end
41
51
  end.join(' AND ')
42
52
  else
43
- "`#{k}` = #{self.class.connection.quote(v)}"
53
+ "#{k} = #{self.class.connection.quote(v)}"
44
54
  end
45
55
  end.join(' AND ')
46
56
  end
@@ -54,7 +64,7 @@ module Property
54
64
  end
55
65
 
56
66
  def index_table_name(group_name)
57
- "i_#{group_name}_#{self.class.table_name}"
67
+ self.class.index_table_name(group_name)
58
68
  end
59
69
 
60
70
  def index_foreign_key
@@ -90,12 +100,11 @@ module Property
90
100
  end
91
101
 
92
102
  values = new_keys.map do |key|
93
- [connection.quote(key), connection.quote(cur_indices[key])]
103
+ [key, cur_indices[key]]
94
104
  end
95
105
 
96
106
  res = []
97
107
  foreign_values.each do |list|
98
- list = list.map {|k| connection.quote(k)}
99
108
  values.each do |value|
100
109
  res << (list + value)
101
110
  end
@@ -127,12 +136,12 @@ module Property
127
136
 
128
137
  # Update an index entry
129
138
  def update_index(group_name, key, value)
130
- self.class.connection.execute "UPDATE #{index_table_name(group_name)} SET `value` = #{connection.quote(value)} WHERE #{index_reader_sql(group_name)} AND `key` = #{connection.quote(key)}"
139
+ self.class.connection.execute "UPDATE #{index_table_name(group_name)} SET value = #{connection.quote(value)} WHERE #{index_reader_sql(group_name)} AND #{KEY} = #{connection.quote(key)}"
131
140
  end
132
141
 
133
142
  # Delete a list of indices (value became blank).
134
143
  def delete_indices(group_name, keys)
135
- self.class.connection.execute "DELETE FROM #{index_table_name(group_name)} WHERE #{index_reader_sql(group_name)} AND `key` IN (#{keys.map{|key| connection.quote(key)}.join(',')})"
144
+ self.class.connection.execute "DELETE FROM #{index_table_name(group_name)} WHERE #{index_reader_sql(group_name)} AND #{KEY} IN (#{keys.map{|key| connection.quote(key)}.join(',')})"
136
145
  end
137
146
 
138
147
  # This method prepares the index
@@ -202,7 +211,7 @@ module Property
202
211
  if group_name.kind_of?(Class)
203
212
  group_name.delete_property_index(self)
204
213
  else
205
- connection.execute "DELETE FROM #{index_table_name(group_name)} WHERE `#{foreign_key}` = #{current_id}"
214
+ connection.execute "DELETE FROM #{index_table_name(group_name)} WHERE #{foreign_key} = #{current_id}"
206
215
  end
207
216
  end
208
217
  end
@@ -39,13 +39,7 @@ module Property
39
39
  columns.values.select do |c|
40
40
  c.indexed?
41
41
  end.map do |c|
42
- if c.index == true
43
- [c.type.to_sym, c.name]
44
- elsif c.index.kind_of?(Proc)
45
- [c.type.to_sym, c.name, c.index]
46
- else
47
- [c.index, c.name]
48
- end
42
+ [c.index, c.name, c.index_proc]
49
43
  end + @group_indices
50
44
  end
51
45
 
@@ -132,7 +126,7 @@ module Property
132
126
  options = args.extract_options!
133
127
  column_names = args.flatten
134
128
  default = options.delete(:default)
135
- column_names.each { |name| role.add_column(Property::Column.new(name, default, '#{column_type}', options)) }
129
+ column_names.each { |name| role.add_column(Property::Column.new(name, default, '#{column_type}', options.merge(:role => role))) }
136
130
  end
137
131
  EOV
138
132
  end
@@ -141,7 +135,7 @@ module Property
141
135
  # p.serialize 'pet', Dog
142
136
  def serialize(name, klass, options = {})
143
137
  Property.validate_property_class(klass)
144
- role.add_column(Property::Column.new(name, nil, klass, options))
138
+ role.add_column(Property::Column.new(name, nil, klass, options.merge(:role => role)))
145
139
  end
146
140
 
147
141
  # This is used to create complex indices with the following syntax:
@@ -21,18 +21,22 @@ module Property
21
21
 
22
22
  def self.included(base)
23
23
  Property.validators << Validator
24
+ base.extend Encoder
24
25
  end
25
26
 
26
- # Encode properties with JSON
27
- def encode_properties(properties)
28
- properties.to_json
29
- end
30
-
31
- # Decode Marshal encoded properties
32
- def decode_properties(string)
33
- ::JSON.parse(string)
34
- end
27
+ module Encoder
28
+ # Encode properties with JSON
29
+ def encode_properties(properties)
30
+ properties.to_json
31
+ end
35
32
 
33
+ # Decode Marshal encoded properties
34
+ def decode_properties(string)
35
+ ::JSON.parse(string)
36
+ end
37
+ end # Encoder
38
+ include Encoder
39
+ extend Encoder
36
40
  end # JSON
37
41
  end # Serialization
38
42
  end # Property
@@ -7,18 +7,25 @@ module Property
7
7
  # * no corruption risk if the version of Marshal changes
8
8
  # * it can be accessed by other languages then ruby
9
9
  module Marshal
10
-
11
- # Encode properties with Marhsal
12
- def encode_properties(properties)
13
- # we limit dump depth to 0 (object only: no instance variables)
14
- # we have to protect Marshal from serializing instance variables by making a copy
15
- [::Marshal::dump(Properties[properties])].pack('m*')
10
+ def self.included(base)
11
+ base.extend Encoder
16
12
  end
17
13
 
18
- # Decode Marshal encoded properties
19
- def decode_properties(string)
20
- ::Marshal::load(string.unpack('m')[0])
21
- end
14
+ module Encoder
15
+ # Encode properties with Marhsal
16
+ def encode_properties(properties)
17
+ # we limit dump depth to 0 (object only: no instance variables)
18
+ # we have to protect Marshal from serializing instance variables by making a copy
19
+ [::Marshal::dump(Properties[properties])].pack('m*')
20
+ end
21
+
22
+ # Decode Marshal encoded properties
23
+ def decode_properties(string)
24
+ ::Marshal::load(string.unpack('m')[0])
25
+ end
26
+ end # Encoder
27
+ include Encoder
28
+ extend Encoder
22
29
 
23
30
  end # Marshal
24
31
  end # Serialization
@@ -3,15 +3,23 @@ module Property
3
3
  # Use YAML to encode properties. This method is the slowest of all
4
4
  # and you should use JSON if you haven't got good reasons not to.
5
5
  module YAML
6
- # Encode properties with YAML
7
- def encode_properties(properties)
8
- ::YAML.dump(properties)
6
+ def self.included(base)
7
+ base.extend Encoder
9
8
  end
10
9
 
11
- # Decode properties from YAML
12
- def decode_properties(string)
13
- ::YAML::load(string)
14
- end
10
+ module Encoder
11
+ # Encode properties with Marhsal
12
+ def encode_properties(properties)
13
+ ::YAML.dump(properties)
14
+ end
15
+
16
+ # Decode Marshal encoded properties
17
+ def decode_properties(string)
18
+ ::YAML::load(string)
19
+ end
20
+ end # Encoder
21
+ include Encoder
22
+ extend Encoder
15
23
 
16
24
  end # Yaml
17
25
  end # Serialization
@@ -69,7 +69,7 @@ module Property
69
69
  @original_columns = {}
70
70
  stored_columns.each do |column|
71
71
  @original_columns[column.name] = column
72
- add_column(Property::Column.new(column.name, column.default, column.ptype, column.options))
72
+ add_column(Property::Column.new(column.name, column.default, column.ptype, column.options.merge(:role => self)))
73
73
  end
74
74
  end
75
75
 
@@ -0,0 +1,3 @@
1
+ module Property
2
+ VERSION = '1.1.0'
3
+ end
data/lib/property.rb CHANGED
@@ -13,8 +13,6 @@ require 'property/core_ext/time'
13
13
  require 'property/base'
14
14
 
15
15
  module Property
16
- VERSION = '1.0.0'
17
-
18
16
  def self.included(base)
19
17
  base.class_eval do
20
18
  include Attribute
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 = "1.0.0"
8
+ s.version = "1.1.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-05-27}
12
+ s.date = %q{2010-07-22}
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 = [
@@ -43,7 +43,9 @@ Gem::Specification.new do |s|
43
43
  "lib/property/serialization/yaml.rb",
44
44
  "lib/property/stored_column.rb",
45
45
  "lib/property/stored_role.rb",
46
+ "lib/property/version.rb",
46
47
  "property.gemspec",
48
+ "test/database.rb",
47
49
  "test/fixtures.rb",
48
50
  "test/shoulda_macros/index.rb",
49
51
  "test/shoulda_macros/role.rb",
@@ -51,6 +53,7 @@ Gem::Specification.new do |s|
51
53
  "test/test_helper.rb",
52
54
  "test/unit/property/attribute_test.rb",
53
55
  "test/unit/property/base_test.rb",
56
+ "test/unit/property/column_test.rb",
54
57
  "test/unit/property/declaration_test.rb",
55
58
  "test/unit/property/dirty_test.rb",
56
59
  "test/unit/property/index_complex_test.rb",
@@ -71,13 +74,15 @@ Gem::Specification.new do |s|
71
74
  s.rubygems_version = %q{1.3.6}
72
75
  s.summary = %q{model properties wrap into a single database column}
73
76
  s.test_files = [
74
- "test/fixtures.rb",
77
+ "test/database.rb",
78
+ "test/fixtures.rb",
75
79
  "test/shoulda_macros/index.rb",
76
80
  "test/shoulda_macros/role.rb",
77
81
  "test/shoulda_macros/serialization.rb",
78
82
  "test/test_helper.rb",
79
83
  "test/unit/property/attribute_test.rb",
80
84
  "test/unit/property/base_test.rb",
85
+ "test/unit/property/column_test.rb",
81
86
  "test/unit/property/declaration_test.rb",
82
87
  "test/unit/property/dirty_test.rb",
83
88
  "test/unit/property/index_complex_test.rb",
data/test/database.rb ADDED
@@ -0,0 +1,103 @@
1
+
2
+ begin
3
+ class PropertyMigration < ActiveRecord::Migration
4
+ def self.down
5
+ drop_table 'employees'
6
+ drop_table 'versions'
7
+ drop_table 'string_index'
8
+ end
9
+
10
+ def self.up
11
+ create_table 'employees' do |t|
12
+ t.string 'type'
13
+ t.text 'properties'
14
+ end
15
+
16
+ create_table 'versions' do |t|
17
+ t.integer 'employee_id'
18
+ t.string 'properties'
19
+ t.string 'title'
20
+ t.string 'comment'
21
+ t.string 'lang'
22
+ t.timestamps
23
+ end
24
+
25
+ create_table 'dummies' do |t|
26
+ t.text 'properties'
27
+ end
28
+
29
+ # index strings in employees
30
+ create_table 'idx_employees_strings' do |t|
31
+ t.integer 'employee_id'
32
+ t.integer 'version_id'
33
+ t.string 'key'
34
+ t.string 'value'
35
+ end
36
+
37
+ # multilingual index strings in employees
38
+ create_table 'idx_employees_ml_strings' do |t|
39
+ t.integer 'employee_id'
40
+ t.integer 'version_id'
41
+ t.string 'lang'
42
+ t.integer 'site_id'
43
+ t.string 'key'
44
+ t.string 'value'
45
+ end
46
+
47
+ # index strings in employees
48
+ create_table 'idx_employees_specials' do |t|
49
+ t.integer 'id'
50
+ t.integer 'employee_id'
51
+ t.string 'key'
52
+ t.string 'value'
53
+ end
54
+
55
+ # index integer in employees
56
+ create_table 'idx_employees_integers' do |t|
57
+ t.integer 'employee_id'
58
+ t.integer 'version_id'
59
+ t.string 'key'
60
+ t.integer 'value'
61
+ end
62
+
63
+ # index text in employees
64
+ create_table 'idx_employees_texts' do |t|
65
+ t.integer 'employee_id'
66
+ t.string 'key'
67
+ t.text 'value'
68
+ end
69
+
70
+ # custom or legacy index table
71
+ create_table 'contacts' do |t|
72
+ t.integer 'employee_id'
73
+ t.string 'name'
74
+ t.string 'other_name'
75
+ end
76
+
77
+ # Database stored role
78
+ create_table 'roles' do |t|
79
+ t.integer 'id'
80
+ t.string 'name'
81
+ end
82
+
83
+ create_table 'columns' do |t|
84
+ t.integer 'id'
85
+ t.integer 'role_id'
86
+ t.string 'name'
87
+ # Property Type
88
+ t.string 'ptype'
89
+ # Indexed (we store an integer so that we can have multiple index types)
90
+ t.string 'index'
91
+ end
92
+ end
93
+ end
94
+
95
+ ActiveRecord::Base.establish_connection(:adapter=>'sqlite3', :database=>':memory:')
96
+ log_path = Pathname(__FILE__).dirname + '../log/test.log'
97
+ Dir.mkdir(log_path.dirname) unless File.exist?(log_path.dirname)
98
+ ActiveRecord::Base.logger = Logger.new(File.open(log_path, 'wb'))
99
+ ActiveRecord::Migration.verbose = false
100
+ #PropertyMigration.migrate(:down)
101
+ PropertyMigration.migrate(:up)
102
+ ActiveRecord::Migration.verbose = true
103
+ end
data/test/fixtures.rb CHANGED
@@ -28,122 +28,42 @@ class Version < ActiveRecord::Base
28
28
  end
29
29
 
30
30
  # To test custom class serialization
31
- class Dog
31
+ class Cat
32
32
  attr_accessor :name, :toy
33
33
  def self.json_create(data)
34
- Dog.new(data['name'], data['toy'])
34
+ Cat.new(data['name'], data['toy'])
35
35
  end
36
+
36
37
  def initialize(name, toy)
37
38
  @name, @toy = name, toy
38
39
  end
40
+
39
41
  def to_json(*args)
40
42
  { 'json_class' => self.class.to_s,
41
43
  'name' => @name, 'toy' => @toy
42
44
  }.to_json(*args)
43
45
  end
46
+
44
47
  def ==(other)
45
- other.kind_of?(Dog) && @name == other.name && @toy == other.toy
48
+ other.kind_of?(Cat) && @name == other.name && @toy == other.toy
46
49
  end
47
50
  end
48
51
 
49
- begin
50
- class PropertyMigration < ActiveRecord::Migration
51
- def self.down
52
- drop_table 'employees'
53
- drop_table 'versions'
54
- drop_table 'string_index'
55
- end
56
-
57
- def self.up
58
- create_table 'employees' do |t|
59
- t.string 'type'
60
- t.text 'properties'
61
- end
62
-
63
- create_table 'versions' do |t|
64
- t.integer 'employee_id'
65
- t.string 'properties'
66
- t.string 'title'
67
- t.string 'comment'
68
- t.string 'lang'
69
- t.timestamps
70
- end
71
-
72
- create_table 'dummies' do |t|
73
- t.text 'properties'
74
- end
75
-
76
- # index strings in employees
77
- create_table 'i_string_employees' do |t|
78
- t.integer 'employee_id'
79
- t.integer 'version_id'
80
- t.string 'key'
81
- t.string 'value'
82
- end
83
-
84
- # multilingual index strings in employees
85
- create_table 'i_ml_string_employees' do |t|
86
- t.integer 'employee_id'
87
- t.integer 'version_id'
88
- t.string 'lang'
89
- t.integer 'site_id'
90
- t.string 'key'
91
- t.string 'value'
92
- end
93
-
94
- # index strings in employees
95
- create_table 'i_special_employees' do |t|
96
- t.integer 'employee_id'
97
- t.string 'key'
98
- t.string 'value'
99
- end
100
-
101
- # index integer in employees
102
- create_table 'i_integer_employees' do |t|
103
- t.integer 'employee_id'
104
- t.integer 'version_id'
105
- t.string 'key'
106
- t.integer 'value'
107
- end
108
-
109
- # index text in employees
110
- create_table 'i_text_employees' do |t|
111
- t.integer 'employee_id'
112
- t.string 'key'
113
- t.text 'value'
114
- end
115
-
116
- # custom or legacy index table
117
- create_table 'contacts' do |t|
118
- t.integer 'employee_id'
119
- t.string 'name'
120
- t.string 'other_name'
121
- end
122
-
123
- # Database stored role
124
- create_table 'roles' do |t|
125
- t.integer 'id'
126
- t.string 'name'
127
- end
128
-
129
- create_table 'columns' do |t|
130
- t.integer 'id'
131
- t.integer 'role_id'
132
- t.string 'name'
133
- # Property Type
134
- t.string 'ptype'
135
- # Indexed (we store an integer so that we can have multiple index types)
136
- t.string 'index'
137
- end
138
- end
139
- end
140
52
 
141
- ActiveRecord::Base.establish_connection(:adapter=>'sqlite3', :database=>':memory:')
142
- log_path = Pathname(__FILE__).dirname + '../log/test.log'
143
- Dir.mkdir(log_path.dirname) unless File.exist?(log_path.dirname)
144
- ActiveRecord::Base.logger = Logger.new(File.open(log_path, 'wb'))
145
- ActiveRecord::Migration.verbose = false
146
- #PropertyMigration.migrate(:down)
147
- PropertyMigration.migrate(:up)
148
- ActiveRecord::Migration.verbose = true
149
- end
53
+ class IdxEmployeesString < ActiveRecord::Base
54
+ end
55
+
56
+ class IdxEmployeesString < ActiveRecord::Base
57
+ end
58
+
59
+ class IdxEmployeesMlString < ActiveRecord::Base
60
+ end
61
+
62
+ class IdxEmployeesInteger < ActiveRecord::Base
63
+ end
64
+
65
+ class IdxEmployeesText < ActiveRecord::Base
66
+ end
67
+
68
+ class IdxEmployeesSpecial < ActiveRecord::Base
69
+ end