property 0.8.2 → 0.9.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 +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
|