freelancing-god-thinking-sphinx 1.1.12 → 1.1.14
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/README.textile +1 -0
- data/lib/thinking_sphinx.rb +2 -1
- data/lib/thinking_sphinx/active_record.rb +7 -3
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +1 -1
- data/lib/thinking_sphinx/attribute.rb +32 -29
- data/lib/thinking_sphinx/deltas.rb +9 -6
- data/lib/thinking_sphinx/deltas/datetime_delta.rb +1 -1
- data/lib/thinking_sphinx/deltas/default_delta.rb +3 -3
- data/lib/thinking_sphinx/deploy/capistrano.rb +66 -64
- data/lib/thinking_sphinx/facet.rb +57 -20
- data/lib/thinking_sphinx/facet_collection.rb +12 -13
- data/lib/thinking_sphinx/field.rb +3 -1
- data/lib/thinking_sphinx/index.rb +37 -350
- data/lib/thinking_sphinx/index/builder.rb +255 -232
- data/lib/thinking_sphinx/property.rb +29 -2
- data/lib/thinking_sphinx/search.rb +4 -84
- data/lib/thinking_sphinx/search/facets.rb +98 -0
- data/lib/thinking_sphinx/source.rb +150 -0
- data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
- data/lib/thinking_sphinx/source/sql.rb +124 -0
- data/lib/thinking_sphinx/tasks.rb +1 -1
- data/spec/unit/thinking_sphinx/active_record_spec.rb +10 -8
- data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -11
- data/spec/unit/thinking_sphinx/facet_spec.rb +232 -0
- data/spec/unit/thinking_sphinx/field_spec.rb +18 -9
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +347 -1
- data/spec/unit/thinking_sphinx/index_spec.rb +22 -27
- data/spec/unit/thinking_sphinx/search_spec.rb +31 -4
- data/spec/unit/thinking_sphinx/source_spec.rb +156 -0
- data/tasks/testing.rb +7 -15
- metadata +8 -2
data/README.textile
CHANGED
data/lib/thinking_sphinx.rb
CHANGED
|
@@ -18,6 +18,7 @@ require 'thinking_sphinx/class_facet'
|
|
|
18
18
|
require 'thinking_sphinx/facet_collection'
|
|
19
19
|
require 'thinking_sphinx/field'
|
|
20
20
|
require 'thinking_sphinx/index'
|
|
21
|
+
require 'thinking_sphinx/source'
|
|
21
22
|
require 'thinking_sphinx/rails_additions'
|
|
22
23
|
require 'thinking_sphinx/search'
|
|
23
24
|
require 'thinking_sphinx/deltas'
|
|
@@ -36,7 +37,7 @@ module ThinkingSphinx
|
|
|
36
37
|
module Version #:nodoc:
|
|
37
38
|
Major = 1
|
|
38
39
|
Minor = 1
|
|
39
|
-
Tiny =
|
|
40
|
+
Tiny = 14
|
|
40
41
|
|
|
41
42
|
String = [Major, Minor, Tiny].join('.')
|
|
42
43
|
end
|
|
@@ -66,7 +66,7 @@ module ThinkingSphinx
|
|
|
66
66
|
return unless ThinkingSphinx.define_indexes?
|
|
67
67
|
|
|
68
68
|
self.sphinx_indexes ||= []
|
|
69
|
-
index = Index.
|
|
69
|
+
index = ThinkingSphinx::Index::Builder.generate(self, &block)
|
|
70
70
|
|
|
71
71
|
self.sphinx_indexes << index
|
|
72
72
|
unless ThinkingSphinx.indexed_models.include?(self.name)
|
|
@@ -151,7 +151,9 @@ module ThinkingSphinx
|
|
|
151
151
|
self.sphinx_indexes.select { |ts_index|
|
|
152
152
|
ts_index.model == self
|
|
153
153
|
}.each_with_index do |ts_index, i|
|
|
154
|
-
index.sources
|
|
154
|
+
index.sources += ts_index.sources.collect { |source|
|
|
155
|
+
source.to_riddle_for_core(offset, i)
|
|
156
|
+
}
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
index
|
|
@@ -163,7 +165,9 @@ module ThinkingSphinx
|
|
|
163
165
|
index.path = File.join(ThinkingSphinx::Configuration.instance.searchd_file_path, index.name)
|
|
164
166
|
|
|
165
167
|
self.sphinx_indexes.each_with_index do |ts_index, i|
|
|
166
|
-
index.sources
|
|
168
|
+
index.sources += ts_index.sources.collect { |source|
|
|
169
|
+
source.to_riddle_for_delta(offset, i)
|
|
170
|
+
} if ts_index.delta?
|
|
167
171
|
end
|
|
168
172
|
|
|
169
173
|
index
|
|
@@ -11,7 +11,7 @@ module ThinkingSphinx
|
|
|
11
11
|
|
|
12
12
|
def concatenate(clause, separator = ' ')
|
|
13
13
|
clause.split(', ').collect { |field|
|
|
14
|
-
"COALESCE(CAST(#{field} as varchar), '')"
|
|
14
|
+
field[/COALESCE/] ? field : "COALESCE(CAST(#{field} as varchar), '')"
|
|
15
15
|
}.join(" || '#{separator}' || ")
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -9,7 +9,7 @@ module ThinkingSphinx
|
|
|
9
9
|
# associations. Which can get messy. Use Index.link!, it really helps.
|
|
10
10
|
#
|
|
11
11
|
class Attribute < ThinkingSphinx::Property
|
|
12
|
-
attr_accessor :
|
|
12
|
+
attr_accessor :query_source
|
|
13
13
|
|
|
14
14
|
# To create a new attribute, you'll need to pass in either a single Column
|
|
15
15
|
# or an array of them, and some (optional) options.
|
|
@@ -67,15 +67,17 @@ module ThinkingSphinx
|
|
|
67
67
|
# If you're creating attributes for latitude and longitude, don't forget
|
|
68
68
|
# that Sphinx expects these values to be in radians.
|
|
69
69
|
#
|
|
70
|
-
def initialize(columns, options = {})
|
|
70
|
+
def initialize(source, columns, options = {})
|
|
71
71
|
super
|
|
72
72
|
|
|
73
|
-
@type
|
|
74
|
-
@
|
|
75
|
-
@crc
|
|
73
|
+
@type = options[:type]
|
|
74
|
+
@query_source = options[:source]
|
|
75
|
+
@crc = options[:crc]
|
|
76
76
|
|
|
77
|
-
@type
|
|
78
|
-
@type
|
|
77
|
+
@type ||= :multi unless @query_source.nil?
|
|
78
|
+
@type = :integer if @type == :string && @crc
|
|
79
|
+
|
|
80
|
+
source.attributes << self
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
# Get the part of the SELECT clause related to this attribute. Don't forget
|
|
@@ -88,16 +90,16 @@ module ThinkingSphinx
|
|
|
88
90
|
return nil unless include_as_association?
|
|
89
91
|
|
|
90
92
|
clause = @columns.collect { |column|
|
|
91
|
-
column_with_prefix(column)
|
|
93
|
+
part = column_with_prefix(column)
|
|
94
|
+
type == :string ? adapter.convert_nulls(part) : part
|
|
92
95
|
}.join(', ')
|
|
93
96
|
|
|
94
|
-
separator = all_ints? ? ',' : ' '
|
|
97
|
+
separator = all_ints? || @crc ? ',' : ' '
|
|
95
98
|
|
|
96
|
-
clause = adapter.concatenate(clause, separator) if concat_ws?
|
|
97
|
-
clause = adapter.group_concatenate(clause, separator) if is_many?
|
|
98
99
|
clause = adapter.cast_to_datetime(clause) if type == :datetime
|
|
99
|
-
clause = adapter.convert_nulls(clause) if type == :string
|
|
100
100
|
clause = adapter.crc(clause) if @crc
|
|
101
|
+
clause = adapter.concatenate(clause, separator) if concat_ws?
|
|
102
|
+
clause = adapter.group_concatenate(clause, separator) if is_many?
|
|
101
103
|
|
|
102
104
|
"#{clause} AS #{quote_column(unique_name)}"
|
|
103
105
|
end
|
|
@@ -114,7 +116,7 @@ module ThinkingSphinx
|
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
def include_as_association?
|
|
117
|
-
! (type == :multi && (
|
|
119
|
+
! (type == :multi && (query_source == :query || query_source == :ranged_query))
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
# Returns the configuration value that should be used for
|
|
@@ -168,12 +170,23 @@ module ThinkingSphinx
|
|
|
168
170
|
object.send(column.__name)
|
|
169
171
|
end
|
|
170
172
|
|
|
173
|
+
def all_ints?
|
|
174
|
+
@columns.all? { |col|
|
|
175
|
+
klasses = @associations[col].empty? ? [@model] :
|
|
176
|
+
@associations[col].collect { |assoc| assoc.reflection.klass }
|
|
177
|
+
klasses.all? { |klass|
|
|
178
|
+
column = klass.columns.detect { |column| column.name == col.__name.to_s }
|
|
179
|
+
!column.nil? && column.type == :integer
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
171
184
|
private
|
|
172
185
|
|
|
173
186
|
def source_value(offset)
|
|
174
187
|
if is_string?
|
|
175
|
-
"#{
|
|
176
|
-
elsif
|
|
188
|
+
"#{query_source.to_s.dasherize}; #{columns.first.__name}"
|
|
189
|
+
elsif query_source == :ranged_query
|
|
177
190
|
"ranged-query; #{query offset} #{query_clause}; #{range_query}"
|
|
178
191
|
else
|
|
179
192
|
"query; #{query offset}"
|
|
@@ -223,17 +236,6 @@ FROM #{quote_table_name assoc.table}
|
|
|
223
236
|
concat_ws? && all_ints?
|
|
224
237
|
end
|
|
225
238
|
|
|
226
|
-
def all_ints?
|
|
227
|
-
@columns.all? { |col|
|
|
228
|
-
klasses = @associations[col].empty? ? [@model] :
|
|
229
|
-
@associations[col].collect { |assoc| assoc.reflection.klass }
|
|
230
|
-
klasses.all? { |klass|
|
|
231
|
-
column = klass.columns.detect { |column| column.name == col.__name.to_s }
|
|
232
|
-
!column.nil? && column.type == :integer
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
end
|
|
236
|
-
|
|
237
239
|
def type_from_database
|
|
238
240
|
klass = @associations.values.flatten.first ?
|
|
239
241
|
@associations.values.flatten.first.reflection.klass : @model
|
|
@@ -254,9 +256,10 @@ FROM #{quote_table_name assoc.table}
|
|
|
254
256
|
else
|
|
255
257
|
raise <<-MESSAGE
|
|
256
258
|
|
|
257
|
-
Cannot automatically map
|
|
258
|
-
type (integer, float, boolean, datetime, string as ordinal).
|
|
259
|
-
explicitly convert the column's value in your define_index
|
|
259
|
+
Cannot automatically map attribute #{unique_name} in #{@model.name} to an
|
|
260
|
+
equivalent Sphinx type (integer, float, boolean, datetime, string as ordinal).
|
|
261
|
+
You could try to explicitly convert the column's value in your define_index
|
|
262
|
+
block:
|
|
260
263
|
has "CAST(column AS INT)", :type => :integer, :as => :column
|
|
261
264
|
MESSAGE
|
|
262
265
|
end
|
|
@@ -4,20 +4,23 @@ require 'thinking_sphinx/deltas/datetime_delta'
|
|
|
4
4
|
|
|
5
5
|
module ThinkingSphinx
|
|
6
6
|
module Deltas
|
|
7
|
-
def self.parse(index
|
|
8
|
-
delta_option =
|
|
7
|
+
def self.parse(index)
|
|
8
|
+
delta_option = index.local_options.delete(:delta)
|
|
9
9
|
case delta_option
|
|
10
10
|
when TrueClass, :default
|
|
11
|
-
DefaultDelta.new index,
|
|
11
|
+
DefaultDelta.new index, index.local_options
|
|
12
12
|
when :delayed
|
|
13
|
-
DelayedDelta.new index,
|
|
13
|
+
DelayedDelta.new index, index.local_options
|
|
14
14
|
when :datetime
|
|
15
|
-
DatetimeDelta.new index,
|
|
15
|
+
DatetimeDelta.new index, index.local_options
|
|
16
16
|
when FalseClass, nil
|
|
17
17
|
nil
|
|
18
18
|
else
|
|
19
|
+
if delta_option.is_a?(String)
|
|
20
|
+
delta_option = Kernel.const_get(delta_option)
|
|
21
|
+
end
|
|
19
22
|
if delta_option.ancestors.include?(ThinkingSphinx::Deltas::DefaultDelta)
|
|
20
|
-
delta_option.new index,
|
|
23
|
+
delta_option.new index, index.local_options
|
|
21
24
|
else
|
|
22
25
|
raise "Unknown delta type"
|
|
23
26
|
end
|
|
@@ -39,7 +39,7 @@ module ThinkingSphinx
|
|
|
39
39
|
|
|
40
40
|
def clause(model, toggled)
|
|
41
41
|
if toggled
|
|
42
|
-
"#{model.quoted_table_name}.#{
|
|
42
|
+
"#{model.quoted_table_name}.#{model.connection.quote_column_name(@column.to_s)}" +
|
|
43
43
|
" > #{adapter.time_difference(@threshold)}"
|
|
44
44
|
else
|
|
45
45
|
nil
|
|
@@ -39,12 +39,12 @@ module ThinkingSphinx
|
|
|
39
39
|
|
|
40
40
|
def reset_query(model)
|
|
41
41
|
"UPDATE #{model.quoted_table_name} SET " +
|
|
42
|
-
"#{
|
|
43
|
-
"WHERE #{
|
|
42
|
+
"#{model.connection.quote_column_name(@column.to_s)} = #{adapter.boolean(false)} " +
|
|
43
|
+
"WHERE #{model.connection.quote_column_name(@column.to_s)} = #{adapter.boolean(true)}"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def clause(model, toggled)
|
|
47
|
-
"#{model.quoted_table_name}.#{
|
|
47
|
+
"#{model.quoted_table_name}.#{model.connection.quote_column_name(@column.to_s)}" +
|
|
48
48
|
" = #{adapter.boolean(toggled)}"
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -1,80 +1,82 @@
|
|
|
1
|
-
|
|
2
|
-
namespace :
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
Capistrano::Configuration.instance(:must_exist).load do
|
|
2
|
+
namespace :thinking_sphinx do
|
|
3
|
+
namespace :install do
|
|
4
|
+
desc "Install Sphinx by source"
|
|
5
|
+
task :sphinx do
|
|
6
|
+
with_postgres = false
|
|
7
|
+
run "which pg_config" do |channel, stream, data|
|
|
8
|
+
with_postgres = !(data.nil? || data == "")
|
|
9
|
+
end
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
args = []
|
|
12
|
+
if with_postgres
|
|
13
|
+
run "pg_config --pkgincludedir" do |channel, stream, data|
|
|
14
|
+
args << "--with-pgsql=#{data}"
|
|
15
|
+
end
|
|
14
16
|
end
|
|
15
|
-
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
commands = <<-CMD
|
|
19
|
+
wget -q http://www.sphinxsearch.com/downloads/sphinx-0.9.8.1.tar.gz >> sphinx.log
|
|
20
|
+
tar xzvf sphinx-0.9.8.1.tar.gz
|
|
21
|
+
cd sphinx-0.9.8.1
|
|
22
|
+
./configure #{args.join(" ")}
|
|
23
|
+
make
|
|
24
|
+
sudo make install
|
|
25
|
+
rm -rf sphinx-0.9.8.1 sphinx-0.9.8.1.tar.gz
|
|
26
|
+
CMD
|
|
27
|
+
run commands.split(/\n\s+/).join(" && ")
|
|
28
|
+
end
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
desc "Install Thinking Sphinx as a gem from GitHub"
|
|
31
|
+
task :ts do
|
|
32
|
+
sudo "gem install freelancing-god-thinking-sphinx --source http://gems.github.com"
|
|
33
|
+
end
|
|
32
34
|
end
|
|
33
|
-
end
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
desc "Generate the Sphinx configuration file"
|
|
37
|
+
task :configure do
|
|
38
|
+
rake "thinking_sphinx:configure"
|
|
39
|
+
end
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
desc "Index data"
|
|
42
|
+
task :index do
|
|
43
|
+
rake "thinking_sphinx:index"
|
|
44
|
+
end
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
desc "Start the Sphinx daemon"
|
|
47
|
+
task :start do
|
|
48
|
+
configure
|
|
49
|
+
rake "thinking_sphinx:start"
|
|
50
|
+
end
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
desc "Stop the Sphinx daemon"
|
|
53
|
+
task :stop do
|
|
54
|
+
configure
|
|
55
|
+
rake "thinking_sphinx:stop"
|
|
56
|
+
end
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
desc "Stop and then start the Sphinx daemon"
|
|
59
|
+
task :restart do
|
|
60
|
+
stop
|
|
61
|
+
start
|
|
62
|
+
end
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
desc "Stop, re-index and then start the Sphinx daemon"
|
|
65
|
+
task :rebuild do
|
|
66
|
+
stop
|
|
67
|
+
index
|
|
68
|
+
start
|
|
69
|
+
end
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
desc "Add the shared folder for sphinx files for the production environment"
|
|
72
|
+
task :shared_sphinx_folder, :roles => :web do
|
|
73
|
+
sudo "mkdir -p #{shared_path}/db/sphinx/production"
|
|
74
|
+
end
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
def rake(*tasks)
|
|
77
|
+
tasks.each do |t|
|
|
78
|
+
run "cd #{current_path} && rake #{t} RAILS_ENV=production"
|
|
79
|
+
end
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module ThinkingSphinx
|
|
2
2
|
class Facet
|
|
3
|
-
attr_reader :
|
|
3
|
+
attr_reader :property
|
|
4
4
|
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
5
|
+
def initialize(property)
|
|
6
|
+
@property = property
|
|
7
7
|
|
|
8
|
-
if
|
|
8
|
+
if property.columns.length != 1
|
|
9
9
|
raise "Can't translate Facets on multiple-column field or attribute"
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -19,29 +19,62 @@ module ThinkingSphinx
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def name
|
|
23
|
-
reference.unique_name
|
|
24
|
-
end
|
|
25
|
-
|
|
26
22
|
def self.attribute_name_for(name)
|
|
27
23
|
name.to_s == 'class' ? 'class_crc' : "#{name}_facet"
|
|
28
24
|
end
|
|
29
25
|
|
|
26
|
+
def self.attribute_name_from_value(name, value)
|
|
27
|
+
case value
|
|
28
|
+
when String
|
|
29
|
+
attribute_name_for(name)
|
|
30
|
+
when Array
|
|
31
|
+
if value.all? { |val| val.is_a?(Integer) }
|
|
32
|
+
name
|
|
33
|
+
else
|
|
34
|
+
attribute_name_for(name)
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
name
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.translate?(property)
|
|
42
|
+
return true if property.is_a?(Field)
|
|
43
|
+
|
|
44
|
+
case property.type
|
|
45
|
+
when :string
|
|
46
|
+
true
|
|
47
|
+
when :integer, :boolean, :datetime, :float
|
|
48
|
+
false
|
|
49
|
+
when :multi
|
|
50
|
+
!property.all_ints?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def name
|
|
55
|
+
property.unique_name
|
|
56
|
+
end
|
|
57
|
+
|
|
30
58
|
def attribute_name
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
if translate?
|
|
60
|
+
Facet.attribute_name_for(@property.unique_name)
|
|
61
|
+
else
|
|
62
|
+
@property.unique_name.to_s
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def translate?
|
|
67
|
+
Facet.translate?(@property)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def type
|
|
71
|
+
@property.is_a?(Field) ? :string : @property.type
|
|
37
72
|
end
|
|
38
73
|
|
|
39
74
|
def value(object, attribute_value)
|
|
40
|
-
return translate(object, attribute_value) if
|
|
75
|
+
return translate(object, attribute_value) if translate?
|
|
41
76
|
|
|
42
|
-
case @
|
|
43
|
-
when :string
|
|
44
|
-
translate(object, attribute_value)
|
|
77
|
+
case @property.type
|
|
45
78
|
when :datetime
|
|
46
79
|
Time.at(attribute_value)
|
|
47
80
|
when :boolean
|
|
@@ -61,11 +94,15 @@ module ThinkingSphinx
|
|
|
61
94
|
column.__stack.each { |method|
|
|
62
95
|
object = object.send(method)
|
|
63
96
|
}
|
|
64
|
-
object.
|
|
97
|
+
if object.is_a?(Array)
|
|
98
|
+
object.collect { |item| item.send(column.__name) }
|
|
99
|
+
else
|
|
100
|
+
object.send(column.__name)
|
|
101
|
+
end
|
|
65
102
|
end
|
|
66
103
|
|
|
67
104
|
def column
|
|
68
|
-
@
|
|
105
|
+
@property.columns.first
|
|
69
106
|
end
|
|
70
107
|
end
|
|
71
108
|
end
|