property 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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