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 +24 -14
- data/lib/property.rb +8 -2
- data/lib/property/attribute.rb +0 -3
- data/lib/property/behavior.rb +40 -0
- data/lib/property/column.rb +4 -2
- data/lib/property/db.rb +52 -0
- data/lib/property/declaration.rb +7 -2
- data/lib/property/index.rb +131 -0
- data/lib/property/schema.rb +20 -0
- data/property.gemspec +11 -3
- data/test/fixtures.rb +39 -13
- data/test/test_helper.rb +1 -11
- data/test/unit/property/behavior_test.rb +8 -2
- data/test/unit/property/declaration_test.rb +8 -4
- data/test/unit/property/index_complex_test.rb +153 -0
- data/test/unit/property/index_foreign_test.rb +232 -0
- data/test/unit/property/index_simple_test.rb +145 -0
- data/test/unit/property/validation_test.rb +3 -3
- metadata +32 -13
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
|
-
*
|
5
|
-
*
|
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
|
-
*
|
12
|
-
*
|
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
|
-
*
|
18
|
-
*
|
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
|
-
*
|
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
|
-
*
|
40
|
+
* Enabled ruby accessors in model.
|
31
41
|
|
32
42
|
== 0.5.0 2010-02-11
|
33
43
|
|
34
44
|
* 2 major enhancement
|
35
|
-
*
|
36
|
-
*
|
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
|
-
*
|
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.
|
14
|
+
VERSION = '0.9.0'
|
13
15
|
|
14
16
|
def self.included(base)
|
15
17
|
base.class_eval do
|
16
|
-
include
|
18
|
+
include Attribute
|
19
|
+
include Serialization::JSON
|
20
|
+
include Declaration
|
21
|
+
include Dirty
|
22
|
+
include Index
|
17
23
|
end
|
18
24
|
end
|
19
25
|
|
data/lib/property/attribute.rb
CHANGED
data/lib/property/behavior.rb
CHANGED
@@ -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
|
data/lib/property/column.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
@
|
64
|
+
@index = options.delete(:index) || options.delete(:indexed)
|
63
65
|
end
|
64
66
|
|
65
67
|
def extract_default(default)
|
data/lib/property/db.rb
ADDED
@@ -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
|
data/lib/property/declaration.rb
CHANGED
@@ -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
|
data/lib/property/schema.rb
CHANGED
@@ -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
|
+
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-
|
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.
|
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 => '', :
|
5
|
-
property.string 'last_name', :default => '', :
|
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
|
50
|
-
drop_table
|
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
|
54
|
-
t.string
|
55
|
-
t.text
|
55
|
+
create_table 'employees' do |t|
|
56
|
+
t.string 'type'
|
57
|
+
t.text 'properties'
|
56
58
|
end
|
57
59
|
|
58
|
-
create_table
|
60
|
+
create_table 'versions' do |t|
|
59
61
|
t.integer 'employee_id'
|
60
|
-
t.string
|
61
|
-
t.string
|
62
|
-
t.string
|
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
|
67
|
-
t.text
|
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
|
56
|
-
subject.property.string('rolodex', :
|
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.
|
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
|
161
|
-
subject.property.string('rolodex', :
|
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
|
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
|
71
|
+
class Cat < ActiveRecord::Base
|
72
72
|
attr_accessor :encoding
|
73
|
-
|
74
73
|
set_table_name 'dummies'
|
75
|
-
|
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
|
-
|
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-
|
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
|
-
|
19
|
-
|
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
|
-
|
31
|
+
type: :development
|
32
|
+
version_requirements: *id001
|
26
33
|
- !ruby/object:Gem::Dependency
|
27
34
|
name: activerecord
|
28
|
-
|
29
|
-
|
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
|
-
|
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.
|
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
|