repertoire-faceting 0.5.5 → 0.6.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/FAQ +23 -17
  3. data/INSTALL +52 -84
  4. data/LICENSE +1 -1
  5. data/README +213 -34
  6. data/TODO +20 -7
  7. data/ext/Makefile +24 -14
  8. data/ext/README.faceting +51 -0
  9. data/ext/bytea/bytea.sql +173 -0
  10. data/ext/bytea/faceting_bytea.control +6 -0
  11. data/ext/common/util.sql +35 -0
  12. data/ext/faceting--0.6.0.sql +251 -0
  13. data/ext/faceting_bytea--0.6.0.sql +207 -0
  14. data/ext/faceting_varbit--0.6.0.sql +198 -0
  15. data/ext/signature/faceting.control +6 -0
  16. data/ext/signature/signature.c +740 -0
  17. data/ext/{signature.o → signature/signature.o} +0 -0
  18. data/ext/{signature.so → signature/signature.so} +0 -0
  19. data/ext/signature/signature.sql +217 -0
  20. data/ext/varbit/faceting_varbit.control +7 -0
  21. data/ext/varbit/varbit.sql +164 -0
  22. data/{public → lib/assets}/images/repertoire-faceting/proportional_symbol.png +0 -0
  23. data/{public → lib/assets}/images/repertoire-faceting/spinner_sm.gif +0 -0
  24. data/{public → lib/assets}/javascripts/rep.faceting/context.js +2 -2
  25. data/{public → lib/assets}/javascripts/rep.faceting/ext/earth_facet.js +2 -4
  26. data/{public → lib/assets}/javascripts/rep.faceting/facet.js +1 -1
  27. data/{public → lib/assets}/javascripts/rep.faceting/facet_widget.js +3 -8
  28. data/{public → lib/assets}/javascripts/rep.faceting/nested_facet.js +1 -1
  29. data/{public → lib/assets}/javascripts/rep.faceting/results.js +1 -1
  30. data/{public → lib/assets}/javascripts/rep.faceting.js +5 -1
  31. data/{public → lib/assets}/javascripts/rep.protovis-facets.js +3 -3
  32. data/lib/assets/javascripts/rep.widgets/events.js +51 -0
  33. data/lib/assets/javascripts/rep.widgets/global.js +50 -0
  34. data/lib/assets/javascripts/rep.widgets/model.js +159 -0
  35. data/lib/assets/javascripts/rep.widgets/widget.js +213 -0
  36. data/lib/assets/javascripts/rep.widgets.js +14 -0
  37. data/{public → lib/assets}/stylesheets/rep.faceting.css +1 -1
  38. data/lib/repertoire-faceting/adapters/postgresql_adapter.rb +107 -48
  39. data/lib/repertoire-faceting/facets/abstract_facet.rb +43 -27
  40. data/lib/repertoire-faceting/facets/basic_facet.rb +23 -22
  41. data/lib/repertoire-faceting/facets/nested_facet.rb +50 -27
  42. data/lib/repertoire-faceting/model.rb +101 -65
  43. data/lib/repertoire-faceting/rails/engine.rb +8 -0
  44. data/lib/repertoire-faceting/rails/postgresql_adapter.rb +0 -1
  45. data/lib/repertoire-faceting/rails/relation.rb +0 -1
  46. data/lib/repertoire-faceting/railtie.rb +0 -1
  47. data/lib/repertoire-faceting/relation/calculations.rb +7 -2
  48. data/lib/repertoire-faceting/relation/query_methods.rb +17 -4
  49. data/lib/repertoire-faceting/routing.rb +2 -5
  50. data/lib/repertoire-faceting/tasks/all.rake +5 -4
  51. data/lib/repertoire-faceting/tasks/client.rake +2 -5
  52. data/lib/repertoire-faceting/version.rb +1 -1
  53. data/lib/repertoire-faceting.rb +2 -4
  54. data/{public → vendor/assets}/javascripts/google-earth-extensions.js +0 -0
  55. data/{public → vendor/assets}/javascripts/protovis.js +0 -0
  56. metadata +78 -78
  57. data/ext/README.signature +0 -33
  58. data/ext/signature.c +0 -740
  59. data/ext/signature.sql +0 -342
  60. data/ext/signature.sql.IN +0 -342
  61. data/ext/uninstall_signature.sql +0 -4
  62. data/ext/uninstall_signature.sql.IN +0 -4
  63. data/lib/repertoire-faceting/adapters/abstract_adapter.rb +0 -18
  64. data/lib/repertoire-faceting/relation/spawn_methods.rb +0 -26
@@ -3,54 +3,70 @@ require 'active_support/core_ext/object/blank'
3
3
  module Repertoire
4
4
  module Faceting
5
5
  module Facets #:nodoc:
6
-
6
+
7
7
  # Abstract interface all facet implementations must fulfil. At minimum, implementors should
8
8
  # define self.claim?(), signature(), and drill() to create a new kind of facet.
9
9
  #
10
- # For indexing support, implement create_index to create your index table, detect its presence
11
- # in signature() and drill(), and act accordingly.
10
+ # For indexing support, implement create_index(), refresh_index(), and drop_index(). Detect the
11
+ # index's presence in signature() and drill(), and act accordingly.
12
+ #
13
+ # N.B. Facet instances can assume they have been mixed in with an ActiveRecord::Relation and
14
+ # Repertoire::Faceting::Model (see the mixin() method below). Think of them as scoped
15
+ # relations that identify an attribute of the model dataset.
12
16
  #
13
17
  # See BasicFacet and NestedFacet for examples of facet implementations.
18
+ #
14
19
  module AbstractFacet
15
-
20
+
16
21
  attr_accessor :facet_name
17
-
22
+
18
23
  # Return true if this facet implementation can index this ActiveRecord relation. If multiple
19
- # implementations claim a facet, the one that is laoded last wins.
24
+ # implementations claim a facet, the one that is laoded last wins.
20
25
  def self.claim?(relation)
21
- raise 'Please implement claim? for your facet'
26
+ raise "Please implement claim? for your facet"
22
27
  end
23
28
 
24
29
  # Return an arel expression for the signature of all models matching the given current refinement state for
25
- # this facet (and ignoring all others).
30
+ # this facet (and ignoring all others). Signatures should be constructed from the column returned by
31
+ # faceting_id().
26
32
  def signature(state)
27
- raise 'Please implement signature for your facet'
33
+ raise "Please implement signature for your facet"
28
34
  end
29
-
30
- # Return an arel expression for state, signature pairs from which one can refine the current state of this facet
31
- # (ignoring all others)
35
+
36
+ # Return an arel expression for (state, signature) pairs from which one can refine the current state of this facet
37
+ # (ignoring all others). Signatures should be constructed from the column returned by faceting_id().
32
38
  def drill(state)
33
- raise 'Please implement drill for your facet'
39
+ raise "Please implement drill for your facet"
34
40
  end
35
41
 
36
- # Return an arel expression describing this facet's index table, or raise an exception if indexing not supported
37
- #
38
- # Implementations should build signatures using the faceting_id column passed in, which may not exist yet on the
39
- # model table itself.
40
- def create_index(faceting_id)
41
- raise 'Facet #{facet_name} does not support indexing'
42
+ # Create this facet's index table, or raise an exception if indexing not supported. Signatures should be
43
+ # constructed from the column returned by faceting_id().
44
+ def create_index
45
+ raise "Facet #{facet_name} does not support indexing"
42
46
  end
43
47
 
44
- protected
48
+ # Refresh this facet's index, or raise an exception if it is not indexed
49
+ def refresh_index
50
+ raise "Facet #{facet_name} is not indexed" unless facet_indexed?
51
+ connection.refresh_materialized_view(facet_index_name)
52
+ end
45
53
 
46
- # Return the facet's index table name
47
- def facet_index_table
48
- connection.facet_table_name(table_name, facet_name).to_sym
54
+ # Drop this facet's index, or raise an exception if it is not indexed
55
+ def drop_index
56
+ raise "Facet #{facet_name} is not indexed" unless facet_indexed?
57
+ connection.drop_materialized_view(facet_index_name)
49
58
  end
50
59
 
51
60
  # Returns true if the facet's index table exists
52
- def indexed?
53
- connection.indexed_facets(table_name).map(&:to_sym).include?(facet_name)
61
+ def facet_indexed?
62
+ indexed_facets.map(&:to_sym).include?(facet_name)
63
+ end
64
+
65
+ protected
66
+
67
+ # Return a facet's index table name
68
+ def facet_index_name
69
+ connection.facet_table_name(table_name, facet_name)
54
70
  end
55
71
 
56
72
  def self.implementations
@@ -70,10 +86,10 @@ module Repertoire
70
86
  # mix in facet implementation and configure
71
87
  relation.singleton_class.send(:include, impls.last)
72
88
  relation.facet_name = name
73
-
89
+
74
90
  relation
75
91
  end
76
-
92
+
77
93
  end
78
94
  end
79
95
  end
@@ -1,8 +1,8 @@
1
1
  module Repertoire #:nodoc:
2
2
  module Faceting #:nodoc:
3
3
  module Facets #:nodoc:
4
-
5
- # Implementation of AbstractFacet for non-nested, single-valued facets. By default,
4
+
5
+ # Implementation of AbstractFacet for non-nested, single-valued facets. By default,
6
6
  # all facets that have a single group column will follow this behavior.
7
7
  #
8
8
  # See Repertoire::Faceting::Model::ClassMethods for usage.
@@ -10,56 +10,57 @@ module Repertoire #:nodoc:
10
10
  module BasicFacet
11
11
  include AbstractFacet
12
12
  include Arel
13
-
13
+
14
14
  def self.claim?(relation)
15
15
  relation.group_values.size == 1
16
16
  end
17
-
17
+
18
18
  def signature(state)
19
- return read_index(state, true) if indexed?
19
+ return read_index(state, true) if facet_indexed?
20
20
  col = group_values.first
21
21
  rel = only(:where, :joins)
22
22
  rel = rel.where(in_clause(col, state)) unless state.empty?
23
- rel.select("signature(#{table_name}.#{faceting_id})").arel
23
+ rel.select("facet.signature(#{table_name}.#{faceting_id})").arel
24
24
  end
25
-
25
+
26
26
  def drill(state)
27
- return read_index(state, false) if indexed?
28
- col = group_values.first
27
+ return read_index(state, false) if facet_indexed?
28
+ col = group_values.first
29
29
  rel = only(:where, :joins, :group)
30
30
  rel = rel.where(in_clause(col, state)) unless state.empty?
31
- rel.select(["#{col} AS #{facet_name}", "signature(#{table_name}.#{faceting_id})"]).arel
31
+ rel.select(["#{col} AS #{facet_name}", "facet.signature(#{table_name}.#{faceting_id})"]).arel
32
32
  end
33
-
34
- def create_index(faceting_id)
33
+
34
+ def create_index
35
35
  col = group_values.first
36
- sql = only(:where, :joins, :group).select(["#{col} AS #{facet_name}", "signature(#{table_name}.#{faceting_id})"]).to_sql
37
-
38
- connection.recreate_table(facet_index_table, sql)
36
+ rel = only(:where, :joins, :group)
37
+ sql = rel.select(["#{col} AS #{facet_name}", "facet.signature(#{table_name}.#{faceting_id})"]).to_sql
38
+
39
+ connection.create_materialized_view(facet_index_name, sql)
39
40
  end
40
41
 
41
42
  private
42
-
43
+
43
44
  def in_clause(col, values)
44
45
  # ActiveRecord unhelpfully scatters wrong table names in predicates...
45
46
  values = values.map { |v| connection.quote(v) }
46
47
  "#{col} IN (#{values.join(', ')})"
47
48
  end
48
-
49
+
49
50
  def read_index(state, aggregate)
50
- index = Arel::Table.new(facet_index_table)
51
+ index = Arel::Table.new(facet_index_name)
51
52
  rel = SelectManager.new Table.engine
52
-
53
+
53
54
  rel.from index
54
55
  rel.where(index[facet_name].in(state)) unless state.empty?
55
-
56
+
56
57
  if aggregate
57
- rel.project('collect(signature) AS signature')
58
+ rel.project('facet.collect(signature) AS signature')
58
59
  else
59
60
  rel.project(index[facet_name], index[:signature])
60
61
  end
61
62
  end
62
-
63
+
63
64
  end
64
65
  end
65
66
  end
@@ -1,7 +1,7 @@
1
1
  module Repertoire
2
2
  module Faceting
3
3
  module Facets
4
-
4
+
5
5
  # Implementation of AbstractFacet for facets whose values fall into a nested taxonomy.
6
6
  # By default, all facets that group several columns will follow this behavior.
7
7
  #
@@ -10,66 +10,90 @@ module Repertoire
10
10
  module NestedFacet
11
11
  include AbstractFacet
12
12
  include Arel
13
-
13
+
14
14
  def self.claim?(relation)
15
15
  relation.group_values.size > 1
16
16
  end
17
-
17
+
18
18
  def signature(state)
19
- return read_index(state, true) if indexed?
19
+ return read_index(state, true) if facet_indexed?
20
20
  rel = only(:where, :joins)
21
21
  bind_nest(group_values, state) do |expr, val|
22
22
  rel = rel.where("#{expr} = #{connection.quote(val)}")
23
23
  end
24
- rel.select("signature(#{table_name}.#{faceting_id})").arel
24
+ rel.select("facet.signature(#{table_name}.#{faceting_id})").arel
25
25
  end
26
-
26
+
27
27
  def drill(state)
28
- return read_index(state, false) if indexed?
28
+ return read_index(state, false) if facet_indexed?
29
29
  rel = only(:where, :joins)
30
30
  grp = bind_nest(group_values, state) do |expr, val|
31
31
  rel = rel.where("#{expr} = #{connection.quote(val)}")
32
32
  end
33
- rel.group(grp).select(["#{grp.last} AS #{facet_name}", "signature(#{table_name}.#{faceting_id})"]).arel
33
+ rel.group(grp).select(["#{grp.last} AS #{facet_name}", "facet.signature(#{table_name}.#{faceting_id})"]).arel
34
34
  end
35
-
36
- def create_index(faceting_id)
37
- rel = only(:where, :joins, :group)
38
- group_values.zip(columns).each do |expr, col|
39
- rel = rel.select("#{expr} AS #{col}")
35
+
36
+ def create_index
37
+ levels = group_values.length
38
+
39
+ # Construct expressions at each grouping level, right pad with nil
40
+ expr_drills = (0..levels-1).map do |i|
41
+ group_values[0..i] + (i+1..levels-1).collect { "NULL" }
42
+ end
43
+
44
+ # Construct indexes for each drill level
45
+ queries = []
46
+ expr_drills.each_with_index do |exprs, level|
47
+ rel = only(:where, :joins)
48
+ exprs.zip(columns).each do |expr, col|
49
+ rel = rel.select("#{expr} AS #{col}")
50
+ end
51
+ rel = rel.group(columns[0..level])
52
+
53
+ queries << rel.select(["#{level+1} AS level", "facet.signature(#{table_name}.#{faceting_id})"]).to_sql
40
54
  end
41
- sql = rel.select(["#{group_values.size} AS level", "signature(#{table_name}.#{faceting_id})"]).to_sql
42
-
43
- connection.recreate_table(facet_index_table, sql)
44
- connection.expand_nesting(facet_index_table)
55
+
56
+ # Root of tree
57
+ empty_cols = columns.map { |col| "NULL AS #{col}"}
58
+ queries << only(:where).select(empty_cols + ["0 AS level", "facet.signature(#{table_name}.#{faceting_id})"]).to_sql
59
+
60
+ # Give the fullest index first (i.e. leaves of the tree), so the database
61
+ # can resolve types before encountering any NULL values (i.e. values of
62
+ # indeterminate type)
63
+ queries = queries.reverse
64
+
65
+ # The full index table is union of indices at each drill level
66
+ sql = queries.join(" UNION ")
67
+
68
+ connection.create_materialized_view(facet_index_name, sql)
45
69
  end
46
-
70
+
47
71
  private
48
72
 
49
73
  def columns
50
- (1..group_values.size).map { |i| "#{facet_name}#{i}"}
74
+ (1..group_values.size).map { |i| "#{facet_name}_#{i}"}
51
75
  end
52
-
76
+
53
77
  # Iterates over all the columns that have state in turn, and returns
54
78
  # a grouping of the columns one level further nested
55
79
  def bind_nest(cols, state, &block)
56
80
  level = state.size
57
81
  grp = cols[0..level] # advance one nest step
58
-
82
+
59
83
  if level > 0
60
- cols[0..level-1].zip(state).each do |col, val|
84
+ cols[0..level-1].zip(state).each do |col, val|
61
85
  yield(col, val)
62
86
  end
63
87
  end
64
88
  grp << "NULL::TEXT" if level >= cols.size
65
-
89
+
66
90
  grp
67
91
  end
68
-
92
+
69
93
  def read_index(state, aggregate)
70
- index = Table.new(facet_index_table)
94
+ index = Table.new(facet_index_name)
71
95
  rel = SelectManager.new Table.engine
72
-
96
+
73
97
  rel.from index
74
98
  grp = bind_nest(columns, state) do |col, val|
75
99
  rel.where(index[col].eq(val))
@@ -84,7 +108,6 @@ module Repertoire
84
108
  rel.project("#{grp.last} AS #{facet_name}", index[:signature])
85
109
  end
86
110
  end
87
-
88
111
  end
89
112
  end
90
113
  end
@@ -2,15 +2,18 @@ module Repertoire
2
2
  module Faceting
3
3
  module Model #:nodoc:
4
4
  extend ActiveSupport::Concern
5
-
5
+
6
6
  SIGNATURE_WASTAGE_THRESHOLD = 0.15
7
7
  DEFAULT_SIGNATURE_COLUMN = 'id'
8
8
  PACKED_SIGNATURE_COLUMN = '_packed_id'
9
-
9
+
10
10
  included do |base|
11
- base.singleton_class.delegate :refine, :minimum, :nils, :reorder, :to_sql, :to => :scoped
11
+ base.singleton_class.delegate :refine, :minimum, :nils, :reorder, :to_sql, :to => :scoped_all
12
+
13
+ base.class_attribute(:facets)
14
+ base.facets = {}
12
15
  end
13
-
16
+
14
17
  #
15
18
  # == Facet declarations
16
19
  #
@@ -28,12 +31,12 @@ module Repertoire
28
31
  # end
29
32
  #
30
33
  # Implicitly, any facet declaration is an SQL aggregate that divides the attribute values into discrete
31
- # groups. When no relation is provided, /model/.group(/facet name/) is assumed by default. So the
34
+ # groups. When no relation is provided, /model/.group(/facet name/) is assumed by default. So the
32
35
  # discipline facet declaration above is equivalent to
33
36
  #
34
37
  # facet :discipline, group(:discipline)
35
38
  #
36
- # and the grouping on degree could be left out. You can use this behavior to construct a facet
39
+ # and the grouping on degree could be left out. You can use this behavior to construct a facet
37
40
  # from differently-named columns:
38
41
  #
39
42
  # facet :balloon_color, group(:color)
@@ -80,13 +83,13 @@ module Repertoire
80
83
  # To incorporate refinements on other facets on this model, use refine:
81
84
  #
82
85
  # Nobelist.refine(:nobel_year => 2001, :degree => 'Ph.D.').count(:discipline)
83
- #
86
+ #
84
87
  # If you provide multiple values for a simple facet refinement, they are interpreted as a logical "or":
85
88
  #
86
89
  # Nobelist.refine(:nobel_year => [2000, 2001]) # => 'WHERE name IN (2000, 2001)'
87
90
  #
88
91
  # In the case of a nested facet, multiple values identify levels in the taxonomy:
89
- #
92
+ #
90
93
  # Nobelist.refine(:birth_place => [ 'Ukraine', 'Kiev' ]).count(:nobel_year)
91
94
  #
92
95
  # Refinements are integrated into result queries automatically:
@@ -96,9 +99,9 @@ module Repertoire
96
99
  # == Index access
97
100
  #
98
101
  # As you will have noted already, facet counts and queries are quite similar to their ActiveRecord/SQL
99
- # counterparts. Behind the scenes, the Repertoire faceting code re-writes your query.
102
+ # counterparts. Behind the scenes, the Repertoire faceting code re-writes your query.
100
103
  #
101
- # Facets defined on associations are joined and limited automatically, and facet indices in the database
104
+ # Facets defined on associations are joined and limited automatically, and facet indices in the database
102
105
  # are used wherever possible rather than querying the model table.
103
106
  #
104
107
  # == Facet registration
@@ -112,114 +115,147 @@ module Repertoire
112
115
  # See AbstractFacet for more details.
113
116
  #
114
117
  module ClassMethods
115
-
118
+
116
119
  # Declare a facet by name
117
120
  def facet(name, rel=nil)
118
121
  name = name.to_sym
119
122
 
120
123
  # default: group by column with facet name, order by count descending
121
- rel ||= scoped
124
+ rel ||= scoped_all
122
125
  rel = rel.group(name) if rel.group_values.empty?
123
126
  rel = rel.order(["count DESC", "#{name} ASC"]) if rel.order_values.empty?
124
127
 
125
128
  # locate facet implementation that can handle relation
126
129
  facets[name] = Facets::AbstractFacet.mixin(name, rel)
127
130
  end
128
-
129
- # Accessor for the facet definitions
130
- def facets
131
- read_inheritable_attribute(:facets) || write_inheritable_attribute(:facets, {})
132
- end
133
-
131
+
134
132
  # Is there a facet by this name?
135
133
  def facet?(name)
136
134
  facets.key?(name.to_sym)
137
135
  end
138
-
136
+
139
137
  # All defined facets by name
140
138
  def facet_names
141
139
  facets.keys
142
140
  end
143
-
144
- # Drops any unused facet indices, updates its packed ids, then recreates
145
- # indices for the facets with the provided names. If no names are provided,
141
+
142
+ # Returns a list of the facets that currently have indices declared
143
+ def indexed_facets
144
+ connection.indexed_facets(table_name)
145
+ end
146
+
147
+ # Drops any unused facet indices, updates its packed ids, then recreates
148
+ # indices for the facets with the provided names. If no names are provided,
146
149
  # then the existing facet indices are refreshed.
147
150
  #
148
- # If a signature id column name is provided, it will be used to build the
149
- # bitset indices. Otherwise the indexer will add or remove a new packed
151
+ # If a signature id column name is provided, it will be used to build the
152
+ # bitset indices. Otherwise the indexer will add or remove a new packed
150
153
  # id column as appropriate.
151
154
  #
152
155
  # Examples:
153
156
  #
154
157
  # === Refresh existing facet indices
155
158
  #
156
- # Nobelist.update_indexed_facets
159
+ # Nobelist.index_facets
157
160
  #
158
161
  # === Adjust which facets are indexed
159
162
  #
160
- # Nobelist.update_indexed_facets([:degree, :nobel_year])
163
+ # Nobelist.index_facets([:degree, :nobel_year])
161
164
  #
162
165
  # === Drop all facet indices, but add/remove packed id as necessary
163
166
  #
164
- # Nobelist.update_indexed_facets([])
167
+ # Nobelist.index_facets([])
165
168
  #
166
- # === Drop absolutely everything, force manual faceting using 'id'
169
+ # === Drop absolutely everything, force manual faceting using 'id'
167
170
  # column
168
171
  #
169
- # Nobelist.udpate_indexed_facets([], 'id')
172
+ # Nobelist.index_facets([], 'id')
170
173
  #
171
- def update_indexed_facets(facet_names=nil, signature_column=nil)
174
+ def index_facets(next_indexes=nil, next_faceting_id=nil)
172
175
  # default: update existing facets
173
- indexed_facets = connection.indexed_facets(table_name)
174
- facet_names ||= indexed_facets
175
-
176
+ current_indexes = indexed_facets
177
+ next_indexes ||= current_indexes
178
+
179
+ # sanity checks
180
+ current_indexes = current_indexes.map { |name| name.to_sym }
181
+ next_indexes = next_indexes.map { |name| name.to_sym }
182
+ (current_indexes | next_indexes).each do
183
+ |name| raise QueryError, "Unknown facet #{name}" unless facet?(name)
184
+ end
185
+
176
186
  # determine best column for signature bitsets, unless set manually
177
- signature_column ||= if signature_wastage('id') < SIGNATURE_WASTAGE_THRESHOLD
187
+ next_faceting_id ||= if signature_wastage(DEFAULT_SIGNATURE_COLUMN) < SIGNATURE_WASTAGE_THRESHOLD
178
188
  DEFAULT_SIGNATURE_COLUMN
179
189
  else
180
190
  PACKED_SIGNATURE_COLUMN
181
191
  end
182
-
192
+
193
+ # default behavior: no changes to packed id column
194
+ drop_packed_id = create_packed_id = false
195
+
196
+ # default behavior: adjust facet indexes
197
+ drop_list = current_indexes - next_indexes
198
+ refresh_list = next_indexes & current_indexes
199
+ create_list = next_indexes - current_indexes
200
+
201
+ # adding or removing a packed id column
202
+ if next_faceting_id != faceting_id
203
+ drop_packed_id = (next_faceting_id == DEFAULT_SIGNATURE_COLUMN)
204
+ create_packed_id = (next_faceting_id != DEFAULT_SIGNATURE_COLUMN)
205
+ end
206
+
207
+ # special case: repacking an existing packed id column
208
+ if next_faceting_id == faceting_id && next_faceting_id != DEFAULT_SIGNATURE_COLUMN
209
+ drop_packed_id = create_packed_id = (signature_wastage > SIGNATURE_WASTAGE_THRESHOLD)
210
+ end
211
+
212
+ # changing item ids invalidates all existing facet indices
213
+ if drop_packed_id || create_packed_id
214
+ drop_list, refresh_list, create_list = [ current_indexes, [], next_indexes ]
215
+ end
216
+
183
217
  connection.transaction do
184
- # drop old facet indices
185
- indexed_facets.each do |name|
186
- table = connection.facet_table_name(table_name, name)
187
- connection.drop_table(table)
188
- end
189
-
190
- # create or remove packed signature column as necessary
191
- ensure_numbering(signature_column)
192
-
193
- # re-create the facet indices
194
- facet_names.each do |name|
195
- name = name.to_sym
196
- raise "Unknown facet #{name}" unless facet?(name)
197
- facets[name].create_index(signature_column)
198
- end
218
+ # adjust faceting id column
219
+ connection.remove_column(table_name, PACKED_SIGNATURE_COLUMN) if drop_packed_id
220
+ connection.add_column(table_name, PACKED_SIGNATURE_COLUMN, "SERIAL") if create_packed_id
221
+ @faceting_id = next_faceting_id
222
+
223
+ # adjust facet indices
224
+ drop_list.each { |name| facets[name].drop_index }
225
+ refresh_list.each { |name| facets[name].refresh_index }
226
+ create_list.each { |name| facets[name].create_index }
199
227
  end
200
-
228
+
229
+ # TODO. in a nested transaction, this would need to fire after the final commit...
201
230
  reset_column_information
231
+
202
232
  end
203
-
233
+
234
+ # Over-rides reset_column_information in ActiveRecord::ModelSchema
235
+ def reset_column_information
236
+ @faceting_id = nil
237
+ super
238
+ end
239
+
204
240
  # Returns the name of the id column to use for constructing bitset signatures
205
241
  # over this model.
206
242
  def faceting_id
207
- [PACKED_SIGNATURE_COLUMN, DEFAULT_SIGNATURE_COLUMN].detect { |c| column_names.include?(c) }
243
+ @faceting_id ||= [PACKED_SIGNATURE_COLUMN, DEFAULT_SIGNATURE_COLUMN].detect { |c| column_names.include?(c) }
208
244
  end
209
-
210
- def signature_wastage(col=nil)
211
- col ||= faceting_id
212
- connection.signature_wastage(table_name, col)
245
+
246
+ # Returns the proportion of wasted slots in 0..max(id)
247
+ def signature_wastage(signature_column = nil)
248
+ signature_column ||= faceting_id
249
+ connection.signature_wastage(table_name, signature_column)
213
250
  end
214
-
215
- def ensure_numbering(signature_column)
216
- if signature_column == DEFAULT_SIGNATURE_COLUMN
217
- connection.remove_column(table_name, PACKED_SIGNATURE_COLUMN) if column_names.include?(PACKED_SIGNATURE_COLUMN)
218
- else
219
- connection.renumber_table(table_name, PACKED_SIGNATURE_COLUMN, SIGNATURE_WASTAGE_THRESHOLD)
220
- end
251
+
252
+ # Once clients have migrated to Rails 4, delete and replace with 'all' where this is called
253
+ #
254
+ # c.f. http://stackoverflow.com/questions/18198963/with-rails-4-model-scoped-is-deprecated-but-model-all-cant-replace-it
255
+ def scoped_all
256
+ where(nil)
221
257
  end
222
-
258
+
223
259
  end
224
260
  end
225
261
  end
@@ -0,0 +1,8 @@
1
+ require 'rails'
2
+
3
+ module Repertoire
4
+ module Faceting
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -7,7 +7,6 @@ module ActiveRecord #:nodoc: all
7
7
  end
8
8
 
9
9
  class PostgreSQLAdapter
10
- include Repertoire::Faceting::AbstractAdapter
11
10
  include Repertoire::Faceting::PostgreSQLAdapter
12
11
  end
13
12
  end
@@ -4,7 +4,6 @@ require 'active_record'
4
4
  module ActiveRecord #:nodoc: all
5
5
  class Relation
6
6
  include Repertoire::Faceting::Relation::QueryMethods
7
- include Repertoire::Faceting::Relation::SpawnMethods
8
7
  include Repertoire::Faceting::Relation::Calculations
9
8
  end
10
9
  end
@@ -1,4 +1,3 @@
1
- require 'repertoire-faceting'
2
1
  require 'rails'
3
2
 
4
3
  module Repertoire
@@ -9,9 +9,14 @@ module Repertoire
9
9
  def count(name = nil, options = {})
10
10
  if name.present? && @klass.facet?(name)
11
11
  name = name.to_sym
12
- facet = @klass.facets[name].merge(self)
12
+ parent = @klass.facets[name]
13
+ facet = parent.merge(self)
13
14
  state = refine_value[name] || []
14
15
  signatures = facet.drill(state)
16
+
17
+ facet.minimum_value = self.minimum_value || parent.minimum_value
18
+ facet.nils_value = self.nils_value || parent.nils_value
19
+
15
20
  connection.population(facet, masks, signatures)
16
21
  else
17
22
  super
@@ -39,7 +44,7 @@ module Repertoire
39
44
  base = except(:order, :limit, :offset)
40
45
  masks = []
41
46
 
42
- masks << base.only(:where, :join).select("signature(#{table_name}.#{faceting_id})").arel if base.where_values.present?
47
+ masks << base.only(:where, :join).select("facet.signature(#{table_name}.#{faceting_id})").arel if base.where_values.present?
43
48
  refine_value.each do |name, values|
44
49
  raise QueryError, "Unkown facet #{name} on #{klass.name}" unless @klass.facet?(name)
45
50
  masks << @klass.facets[name].signature(values)