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
@@ -0,0 +1,159 @@
1
+ /*
2
+ * Repertoire abstract ajax model, for use with widget framework
3
+ *
4
+ * Copyright (c) 2009 MIT Hyperstudio
5
+ * Christopher York, 11/2009
6
+ *
7
+ * Requires jquery 1.3.2+
8
+ * Support: Firefox 3+ & Safari 4+. IE emphatically not supported.
9
+ */
10
+
11
+ //
12
+ // Abstract model for ajax widgets. This class includes convenience methods for listening to
13
+ // events on models, for jquery ajax calls, and for param encoding in Merb's style.
14
+ //
15
+ // Handles:
16
+ // - change publication and observing
17
+ // - url/query-string construction
18
+ //
19
+ // Usage. The model provides a facade to your ajax webservice, and may hold state for a widget.
20
+ // In some cases (e.g. working with large server-side datasets), it may be appropriate to
21
+ // cache data in the model and fetch it as needed from the webservice.
22
+ //
23
+ // Using the ajax param encoders. These convenience methods help you construct the url and
24
+ // assemble params for an ajax call. It's not required to use them, although they make life
25
+ // easier.
26
+ //
27
+ // self.items = function(callback) {
28
+ // var url = self.default_url(['projects', 'results']);
29
+ // self.fetch(params, url, 'html', callback);
30
+ // }
31
+ //
32
+ repertoire.model = function(options) {
33
+ // this object is an abstract class
34
+ var self = {};
35
+
36
+ // default: no options specified
37
+ options = options || {};
38
+
39
+ // mixin event handler functionality
40
+ repertoire.events(self);
41
+
42
+ //
43
+ // Update the data model given the current state
44
+ //
45
+ // You may either pre-process the state and write your own webservice access methods
46
+ // or use self.fetch for a generic webservice, e.g.
47
+ //
48
+ // self.update = function(state, callback) {
49
+ // var url = self.default_url(['projects', 'results']);
50
+ // self.fetch(state, url, 'html', callback);
51
+ // }
52
+ //
53
+ self.update = function(state, callback) {
54
+ throw "Abstract function: redefine self.update() in your data model."
55
+ };
56
+
57
+ //
58
+ // Utility method to issue an ajax HTTP GET to fetch data from a webservice
59
+ //
60
+ // params: params to send as http query line
61
+ // url: url of webservice to access
62
+ // type: type of data returned (e.g. 'json', 'html')
63
+ // callback: function to call with returned data
64
+ //
65
+ self.fetch = function(params, url, type, callback, $elem, async) {
66
+ if (async == null)
67
+ async = true;
68
+
69
+ var spinnerClass = options.spinner || 'loading';
70
+ if ($elem)
71
+ $elem.addClass(spinnerClass);
72
+ $.ajax({
73
+ async: async,
74
+ url: url,
75
+ data: self.to_query_string(params),
76
+ type: 'GET',
77
+ dataType: type,
78
+ // content negotiation problems may require:
79
+ /* beforeSend: function(xhr) { xhr.setRequestHeader("Accept", "application/json") } */
80
+ success: callback,
81
+ error: function(request, textStatus, errorThrown) {
82
+ if ($elem)
83
+ $elem.html(options.error || 'Could not load');
84
+ },
85
+ complete: function () {
86
+ if ($elem)
87
+ $elem.removeClass(spinnerClass);
88
+ }
89
+ });
90
+ };
91
+
92
+ //
93
+ // Format a webservice url, preferring options.url if possible
94
+ //
95
+ self.default_url = function(default_parts, ext) {
96
+ var path_prefix = options.path_prefix || '';
97
+ var parts = default_parts.slice();
98
+
99
+ parts.unshift(path_prefix);
100
+ url = options.url || parts.join('/')
101
+
102
+ if (ext)
103
+ url += '.' + ext;
104
+
105
+ return url;
106
+ }
107
+
108
+ //
109
+ // Convert a structure of params to a URL query string suitable for use in an HTTP GET request, compliant with Merb's format.
110
+ //
111
+ // An example:
112
+ //
113
+ // Merb::Parse.params_to_query_string(:filter => {:year => [1593, 1597], :genre => ['Tragedy', 'Comedy'] }, :search => 'William')
114
+ // ==> "filter[genre][]=Tragedy&filter[genre][]=Comedy&filter[year][]=1593&filter[year][]=1597&search=William"
115
+ //
116
+ self.to_query_string = function(value, prefix) {
117
+ var vs = [];
118
+ prefix = prefix || '';
119
+ if (value instanceof Array) {
120
+ jQuery.each(value, function(i, v) {
121
+ vs.push(self.to_query_string(v, prefix + '[]'));
122
+ });
123
+ return vs.join('&');
124
+ } else if (typeof(value) == "object") {
125
+ jQuery.each(value, function(k, v) {
126
+ vs.push(self.to_query_string(v, (prefix.length > 0) ? (prefix + '[' + encodeURIComponent(k) + ']') : encodeURIComponent(k)));
127
+ });
128
+ // minor addition to merb: discard empty value lists { e.g. discipline: [] }
129
+ vs = array_filter(vs, function(x) { return x !== ""; });
130
+ return vs.join('&');
131
+ } else {
132
+ return prefix + '=' + encodeURIComponent(value);
133
+ }
134
+ };
135
+
136
+ // Apparently IE doesn't support the filter function? -DD via Brett
137
+ var array_filter = function (thisArray, fun) {
138
+ var len = thisArray.length;
139
+ if (typeof fun != "function")
140
+ throw new TypeError();
141
+
142
+ var res = new Array();
143
+ var thisp = arguments[1];
144
+
145
+ for (var i = 0; i < len; i++) {
146
+ if (i in thisArray) {
147
+ var val = thisArray[i]; // in case fun mutates this
148
+ if (fun.call(thisp, val, i, thisArray))
149
+ res.push(val);
150
+ }
151
+ }
152
+
153
+ return res;
154
+ };
155
+
156
+
157
+ // end of model factory function
158
+ return self;
159
+ }
@@ -0,0 +1,213 @@
1
+ /*
2
+ * Repertoire abstract ajax widget
3
+ *
4
+ * Copyright (c) 2009 MIT Hyperstudio
5
+ * Christopher York, 09/2009
6
+ *
7
+ * Requires jquery 1.3.2+
8
+ * Support: Firefox 3+ & Safari 4+. IE emphatically not supported.
9
+ */
10
+
11
+ //
12
+ // Abstract class for ajax widgets
13
+ //
14
+ // Handles:
15
+ // - url/query-string construction
16
+ // - data assembly for sending to webservice
17
+ // - ui event delegation hooks
18
+ // - hooks for injecting custom behaviour
19
+ //
20
+ // Options on all subclassed widgets:
21
+ //
22
+ // url - provide a url to over-ride the widget's default
23
+ // spinner - css class to add to widget during ajax loads
24
+ // error - text to display if ajax load fails
25
+ // injectors - additional jquery markup to inject into widget (see FAQ)
26
+ // handlers - additional jquery event handlers to add to widget (see FAQ)
27
+ // state - additional pre-processing for params sent to webservice (see FAQ)
28
+ //
29
+ // Sub-classes are required to over-ride one method: self.render(). If you wish to
30
+ // use a data model, store a subclass of rep.wigets/model in your widget.
31
+ //
32
+ repertoire.widget = function(selector, options) {
33
+ // this object is an abstract class
34
+ var self = {};
35
+
36
+ // private variables
37
+ var $widget = $(selector);
38
+ options = options || {};
39
+
40
+ // mix in event handling functionality
41
+ repertoire.events(self, $widget);
42
+
43
+ // to run by hand after sub-classes have evaluated
44
+ self.initialize = function() {
45
+ // register any custom handlers
46
+ if (options.handlers !== undefined)
47
+ register_handlers(options.handlers);
48
+
49
+ // load once at beginning
50
+ self.refresh();
51
+ }
52
+
53
+ //
54
+ // Refresh model and render into widget
55
+ //
56
+ // Integrates state and markup injectors
57
+ //
58
+ self.refresh = function() {
59
+ var callback;
60
+
61
+ // pass to custom state processor
62
+ if (options.state !== undefined)
63
+ options.state(self);
64
+
65
+ callback = function() {
66
+ // render the widget
67
+ var markup = self.render.apply(self, arguments);
68
+
69
+ // inject any custom markup
70
+ if (options.injectors !== undefined)
71
+ // TODO. figure out how to send all args to injectors
72
+ process_injectors(markup, options.injectors, arguments[0]);
73
+
74
+ // paint markup into the dom
75
+ if (markup)
76
+ $widget.html(markup);
77
+ };
78
+
79
+ // start rendering
80
+ self.reload(callback);
81
+ };
82
+
83
+ //
84
+ // Render and return markup for this widget.
85
+ //
86
+ // Three forms are possible:
87
+ //
88
+ // (1) Basic... just return a string or jquery object
89
+ //
90
+ // self.render = function() {
91
+ // return 'Hello world!';
92
+ // };
93
+ //
94
+ // (2) If you just want to tweak the superclass' view:
95
+ //
96
+ // var template_fn = self.render; // idiom to access super.render()
97
+ // self.render = function() {
98
+ // var markup = template_fn();
99
+ // return $(markup).find('.title').html('New Title');
100
+ // };
101
+ //
102
+ // (3) If you want to modify the DOM in place, do so
103
+ // and return nothing.
104
+ //
105
+ self.render = function() {
106
+ return $('<div class="rep"/>'); // namespace for all other widget css is 'rep'
107
+ };
108
+
109
+
110
+ //
111
+ // A hook for use when your widget must render the results of an ajax callback. Put
112
+ // the ajax call in self.reload(). Its results will be passed to self.render().
113
+ //
114
+ // self.reload = function(callback) {
115
+ // $.get('http://www.nytimes.com', callback);
116
+ // };
117
+ //
118
+ // self.render = function(daily_news) {
119
+ // $(daily_news).find('title').text(); // widget's view is the title of the new york times
120
+ // }
121
+ //
122
+ // N.B. In real-world cases, the call in self.reload should be to your
123
+ // data model class, which serves as an ajax api facade.
124
+ //
125
+ self.reload = function(callback) {
126
+ callback();
127
+ }
128
+
129
+ //
130
+ // Register a handler for dom events on this widget. Call with an event selector and a standard jquery event
131
+ // handler function, e.g.
132
+ //
133
+ // self.handler('click.toggle_value!.rep .facet .value', function() { ... });
134
+ //
135
+ // Note the syntax used to identify a handler's event, namespace, and the jquery selector: '<event.namespace>!<target>'.
136
+ // Both event and namespace are optional - leave them out to register a click handler with a unique namespace.
137
+ //
138
+ // N.B. This method is intended only for protected use within a widget and its subclasses, since it depends
139
+ // on the view implementation.
140
+ //
141
+ self.handler = function(event_selector, fn) {
142
+ event_selector = parse_event_selector(event_selector);
143
+ var event = event_selector[0],
144
+ selector = event_selector[1]; // why doesn't JS support array decomposition?!?
145
+
146
+ // bind new handler
147
+ $widget.bind(event, function (e) {
148
+ var $el = $(e.target);
149
+ var result = false;
150
+ // walk up dom tree for selector
151
+ while ($el.length > 0) {
152
+ if ($el.is(selector)) {
153
+ result = fn.apply($el[0], [e]);
154
+ if (result === false)
155
+ e.preventDefault();
156
+ return;
157
+ }
158
+ $el = $el.parent();
159
+ }
160
+ });
161
+ }
162
+
163
+ // PRIVATE
164
+
165
+ // register a collection of event handlers
166
+ function register_handlers(handlers) {
167
+ $.each(handlers, function(selector, handler) {
168
+ // register each handler
169
+ self.handler(selector, function() {
170
+ // bind self as an argument for the custom handler
171
+ return handler.apply(this, [self]);
172
+ });
173
+ });
174
+ }
175
+
176
+ // inject custom markup into widget
177
+ function process_injectors($markup, injectors, data) {
178
+ // workaround for jquery find not matching top element
179
+ $wrapped = $("<div/>").append($markup);
180
+
181
+ $.each(injectors, function(selector, injector) {
182
+ var $elems = $wrapped.find(selector);
183
+ if ($elems.length > 0) {
184
+ injector.apply($elems, [ self, data ]);
185
+ }
186
+ });
187
+ }
188
+
189
+ // parse an event name and selector spec
190
+ function parse_event_selector(event_selector) {
191
+ var s = event_selector.split('!');
192
+ var event, selector;
193
+
194
+ if (s.length === 2) {
195
+ event = s[0], selector = s[1];
196
+ } else if (s.length === 1) {
197
+ event = 'click', selector = s[0];
198
+ } else {
199
+ throw "Could not parse event selector: " + event_selector;
200
+ }
201
+
202
+ if (event.indexOf('.')<0) {
203
+ // create a default namespace from selector or random number
204
+ namespace = selector.replace(/[^a-zA-z0-9]/g, '') || new Date().getTime();
205
+ event = event + '.' + namespace;
206
+ }
207
+
208
+ return [event, selector];
209
+ }
210
+
211
+ // end of widget factory function
212
+ return self;
213
+ };
@@ -0,0 +1,14 @@
1
+ /*
2
+ * Repertoire abstract ajax widget
3
+ *
4
+ * Copyright (c) 2009 MIT Hyperstudio
5
+ * Christopher York, 09/2009
6
+ *
7
+ * Requires jquery 1.3.2+
8
+ * Support: Firefox 3+ & Safari 4+. IE emphatically not supported.
9
+ */
10
+
11
+ //= require rep.widgets/global
12
+ //= require rep.widgets/widget
13
+ //= require rep.widgets/events
14
+ //= require rep.widgets/model
@@ -14,5 +14,5 @@
14
14
  .rep .facet .values .value .label { position: relative; left: 0.5em;}
15
15
  .rep .facet .values .value .label:hover,
16
16
  .rep .facet .nesting_level:hover { cursor: pointer; text-decoration: underline; }
17
- .loading .spinner { background: url(../images/repertoire-faceting/spinner_sm.gif) no-repeat left center; float:right; min-height:15px; min-width:15px;}
17
+ .loading .spinner { background: url(/assets/repertoire-faceting/spinner_sm.gif) no-repeat left center; float:right; min-height:15px; min-width:15px;}
18
18
  .loading .mask { background: rgba(255, 255, 255, 0.6); position:absolute; top:0; left:0; width: 100%; height: 100%; }
@@ -3,7 +3,7 @@ require 'active_support/ordered_hash'
3
3
  module Repertoire
4
4
  module Faceting
5
5
  module PostgreSQLColumn #:nodoc:
6
-
6
+
7
7
  # TODO. still not clear how ActiveRecord adapters support adding custom SQL data types...
8
8
  # feels like a monkey-patch, but there's no documented way to accomplish this simple task
9
9
  def simplified_type(field_type)
@@ -12,82 +12,141 @@ module Repertoire
12
12
  when 'signature'
13
13
  :string
14
14
  else
15
- super
15
+ super
16
16
  end
17
17
  end
18
-
18
+
19
19
  end
20
-
20
+
21
21
  module PostgreSQLAdapter #:nodoc:
22
-
23
- # Creates (or recreates) the packed id column on a given table
24
- def renumber_table(table_name, faceting_id, wastage)
25
- sql = "SELECT renumber_table('#{table_name}', '#{faceting_id}', #{wastage})"
26
- execute(sql)
22
+
23
+ #
24
+ # Methods for accessing faceting APIs (as PostgreSQL extensions)
25
+ #
26
+
27
+ # Returns the available in-database faceting APIs (only when installed as an extension)
28
+ def facet_api_bindings
29
+ @api_bindings ||= select_values "SELECT name FROM pg_available_extensions WHERE name LIKE 'faceting%'"
27
30
  end
28
31
 
29
- # Returns the scatter quotient of the given id column
30
- def signature_wastage(table_name, faceting_id)
31
- sql = "SELECT signature_wastage('#{table_name}', '#{faceting_id}')"
32
- result = select_value(sql)
33
- Float(result)
32
+ # Returns the currently active faceting API binding (only when installed as an extension)
33
+ def current_facet_api_binding
34
+ @current_api_binding ||= select_value "SELECT extname FROM pg_extension WHERE extname LIKE 'faceting%';"
34
35
  end
35
-
36
- # Creates (recreates) a table with the specified select statement
37
- def recreate_table(table_name, sql)
38
- sql = "SELECT recreate_table('#{table_name}', $$#{sql}$$)"
39
- execute(sql)
36
+
37
+ #
38
+ # Methods for access to facet indices
39
+ #
40
+
41
+ def facet_schema
42
+ # TODO. Not clear how to get the schema associated with a PostgreSQL extension from the
43
+ # system tables. So we limit to loading into a schema named 'facet' for now.
44
+ 'facet'
40
45
  end
41
-
42
- # Load PostgreSQL native bitset type into current database
43
- def load_faceting
44
- sql = File.read(Repertoire::Faceting::MODULE_PATH + '/ext/signature.sql')
45
- unload_faceting
46
- execute(sql)
46
+
47
+ def facet_table_name(model_name, name)
48
+ "#{facet_schema}.#{model_name}_#{name}_index"
47
49
  end
48
-
49
- # Unloads PostgreSQL native bitset type
50
- def unload_faceting
51
- execute("DROP TYPE IF EXISTS signature CASCADE")
50
+
51
+ def indexed_facets(model_name)
52
+ sql = "SELECT matviewname FROM pg_matviews WHERE schemaname = 'facet'"
53
+ tables = select_values(sql)
54
+
55
+ tables.grep(/#{model_name}_(\w+)_(\d*)?index/) { $1 }
52
56
  end
53
-
54
- # Expands nested faceting for the specified table (once)
55
- def expand_nesting(table_name)
56
- sql = "SELECT expand_nesting('#{table_name}')"
57
- execute(sql)
57
+
58
+ #
59
+ # Methods for managing packed id columns on models
60
+ #
61
+
62
+ def signature_wastage(table_name, faceting_id)
63
+ sql = "SELECT #{facet_schema}.wastage(#{faceting_id}) FROM #{table_name}"
64
+ result = select_value(sql)
65
+ Float(result)
58
66
  end
59
-
67
+
68
+ #
69
+ # Methods for running facet value counts
70
+ #
71
+
60
72
  def population(facet, masks, signatures)
61
73
  # Would be nice to use Arel here... but recent versions (~ 2.0.1) have removed the property of closure under
62
74
  # composition (e.g. joining two select managers / sub-selects)... why?!?
63
- sigs = [ 'facet.signature' ]
75
+ sigs = [ 'fct.signature' ]
64
76
  exprs = masks.map{|mask| "(#{mask.to_sql})"}
65
77
  sigs << 'mask.signature' unless masks.empty?
66
-
67
- sql = "SELECT facet.#{facet.facet_name}, count(#{ sigs.join(' & ')}) "
68
- sql += "FROM (#{signatures.to_sql}) AS facet "
69
- sql += ", (SELECT (#{exprs.join(' & ')}) AS signature) AS mask " unless masks.empty?
78
+
79
+ bit_and = " OPERATOR(#{facet_schema}.&) "
80
+
81
+ sql = "SELECT fct.#{facet.facet_name}, #{facet_schema}.count(#{ sigs.join(bit_and) }) "
82
+ sql += "FROM (#{signatures.to_sql}) AS fct "
83
+ sql += ", (SELECT (#{exprs.join(bit_and)}) AS signature) AS mask " unless masks.empty?
70
84
  sql += "ORDER BY #{facet.order_values.join(', ')} " if facet.order_values.present?
71
85
  sql += "OFFSET #{facet.offset_value} " if facet.offset_value.present?
72
86
  sql += "LIMIT #{facet.limit_value} " if facet.limit_value.present?
73
-
87
+
74
88
  # run query and type cast
75
89
  results = query(sql)
76
90
  results = results.map { |key, count| [ key, count.to_i] }
77
91
  results = ActiveSupport::OrderedHash[results]
78
-
92
+
79
93
  # minimums and nils
80
- results = results.reject { |key, count| count < (facet.minimum_value || 1) }
81
- results.delete(nil) if facet.nils_value == false
82
-
94
+ results.reject! { |key, count| count < (facet.minimum_value || 1) }
95
+ results.delete(nil) if facet.nils_value == :exclude
96
+
83
97
  results
84
98
  end
85
-
99
+
86
100
  def mask_members_sql(masks, table_name, faceting_id)
101
+ bit_and = " OPERATOR(#{facet_schema}.&) "
87
102
  exprs = masks.map { |mask| "(#{mask.to_sql})" }
88
- "INNER JOIN members(#{exprs.join(' & ')}) AS _refinements_id ON (#{table_name}.#{faceting_id} = _refinements_id)"
103
+ "INNER JOIN #{facet_schema}.members(#{exprs.join(bit_and)}) AS _refinements_id ON (#{table_name}.#{faceting_id} = _refinements_id)"
89
104
  end
90
-
105
+
106
+
107
+ module MigrationMethods #:nodoc:
108
+
109
+ #
110
+ # Methods used in creating, updating, and removing facet indices
111
+ #
112
+
113
+ def create_materialized_view(view_name, sql)
114
+ sql = "CREATE MATERIALIZED VIEW #{quote_table_name(view_name)} AS #{sql}"
115
+ execute(sql)
116
+ end
117
+
118
+ def refresh_materialized_view(view_name)
119
+ sql = "REFRESH MATERIALIZED VIEW #{quote_table_name(view_name)}"
120
+ execute(sql)
121
+ end
122
+
123
+ def drop_materialized_view(view_name)
124
+ sql = "DROP MATERIALIZED VIEW #{quote_table_name(view_name)} CASCADE"
125
+ execute(sql)
126
+ end
127
+
128
+ # Returns the named PostgreSQL API binding sql; for shared hosts where you cannot build extensions
129
+ def faceting_api_sql(api_name, schema_name)
130
+ path = Repertoire::Faceting::MODULE_PATH
131
+ version = Repertoire::Faceting::VERSION
132
+ api_name = api_name.to_sym
133
+ file_name = "#{path}/ext/faceting_#{api_name}--#{version}.sql"
134
+
135
+ raise "Use 'CREATE EXTENSION faceting' to load the default facet api" if api_name == :signature
136
+ raise "Currently, the faceting API must install into a schema named 'facet'" unless schema_name == facet_schema
137
+
138
+ # TODO This approach allows production deploys to skip a "rake db:extensions:install" step when installing
139
+ # to Heroku. In practice, this eases deployment significantly. But shelling out to make during a
140
+ # Rails migration feels inelegant.
141
+ system "cd #{path}/ext; make" unless File.exist?(file_name)
142
+ raise "No API binding available for #{api_name}" unless File.exist?(file_name)
143
+
144
+ File.read(file_name).gsub(/@extschema@/, facet_schema)
145
+ end
146
+ end
147
+
148
+ include MigrationMethods
149
+
91
150
  end
92
151
  end
93
152
  end