bmg 0.17.8 → 0.18.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -3
  3. data/README.md +236 -57
  4. data/lib/bmg.rb +6 -0
  5. data/lib/bmg/algebra.rb +1 -0
  6. data/lib/bmg/algebra/shortcuts.rb +14 -0
  7. data/lib/bmg/operator/allbut.rb +27 -0
  8. data/lib/bmg/operator/autosummarize.rb +27 -4
  9. data/lib/bmg/operator/autowrap.rb +27 -0
  10. data/lib/bmg/operator/constants.rb +7 -0
  11. data/lib/bmg/operator/extend.rb +7 -0
  12. data/lib/bmg/operator/group.rb +1 -0
  13. data/lib/bmg/operator/image.rb +41 -2
  14. data/lib/bmg/operator/join.rb +1 -0
  15. data/lib/bmg/operator/matching.rb +1 -0
  16. data/lib/bmg/operator/not_matching.rb +1 -0
  17. data/lib/bmg/operator/page.rb +2 -7
  18. data/lib/bmg/operator/project.rb +3 -2
  19. data/lib/bmg/operator/rename.rb +12 -5
  20. data/lib/bmg/operator/restrict.rb +1 -0
  21. data/lib/bmg/operator/rxmatch.rb +1 -0
  22. data/lib/bmg/operator/summarize.rb +2 -17
  23. data/lib/bmg/operator/transform.rb +39 -1
  24. data/lib/bmg/operator/union.rb +1 -0
  25. data/lib/bmg/reader.rb +1 -0
  26. data/lib/bmg/reader/csv.rb +29 -10
  27. data/lib/bmg/reader/excel.rb +23 -4
  28. data/lib/bmg/reader/text_file.rb +56 -0
  29. data/lib/bmg/relation.rb +28 -0
  30. data/lib/bmg/relation/empty.rb +4 -0
  31. data/lib/bmg/relation/in_memory.rb +10 -1
  32. data/lib/bmg/relation/materialized.rb +6 -0
  33. data/lib/bmg/relation/spied.rb +6 -1
  34. data/lib/bmg/sequel/relation.rb +5 -0
  35. data/lib/bmg/sql/relation.rb +2 -3
  36. data/lib/bmg/summarizer.rb +29 -1
  37. data/lib/bmg/summarizer/avg.rb +3 -3
  38. data/lib/bmg/summarizer/by_proc.rb +41 -0
  39. data/lib/bmg/summarizer/distinct.rb +36 -0
  40. data/lib/bmg/summarizer/multiple.rb +46 -0
  41. data/lib/bmg/summarizer/percentile.rb +79 -0
  42. data/lib/bmg/support.rb +1 -0
  43. data/lib/bmg/support/ordering.rb +20 -0
  44. data/lib/bmg/support/tuple_algebra.rb +6 -0
  45. data/lib/bmg/support/tuple_transformer.rb +14 -6
  46. data/lib/bmg/version.rb +2 -2
  47. data/lib/bmg/writer.rb +16 -0
  48. data/lib/bmg/writer/csv.rb +0 -11
  49. data/lib/bmg/writer/xlsx.rb +68 -0
  50. data/tasks/test.rake +9 -2
  51. metadata +36 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 600739b827185e0d02252824b7b5616662a378b1c907a632ba695536e76d631e
4
- data.tar.gz: f327e31860ef19e0ab8177ebd0e3d0c927c003cb25b8e48af202145cdf1ceece
3
+ metadata.gz: 1e208e1eb958222f5ffac079296fc7279f221dddf6ed8165f5004b0a90b08c4b
4
+ data.tar.gz: 6e9e2509ae9ce0d277d226d55d29274b895a6b31dabfe9ac387d4be7fd97be4a
5
5
  SHA512:
6
- metadata.gz: 2cd09c0006211c6db5d220ee2371669bd3e29bf32c71eae9fb9ba2c4698392ed172412bbd71c9eded5df34172ab469aa6d20a4117ef263955ed73e8a128cf856
7
- data.tar.gz: 8777307569c78001741a597b3ecd9974aa1ee94780495d86f97af38313dcd185c9516d43b9c1635cae5e57606c7422d3e6b3f4889be0b2adcdfeb9f2c30dee24
6
+ metadata.gz: d142fd93326193529359a02c9e14936ffc43dd52ceab4b44e7d890b82cc0df4561e555002f0e6c12e53f6956a4541140be5214a40745ac54e1dc046bdbd4b735
7
+ data.tar.gz: 6fbbd1ad533a0beee8faf783e07c7cedcc2535ef9edf1b601f973a30a513e5ee6d00f0031c8486f4460ccb912acdbad623c86ca649d4664fdc24c63c3f5de693
data/Gemfile CHANGED
@@ -1,5 +1,2 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
-
4
- # gem "predicate", github: "enspirit/predicate", branch: "placeholders"
5
- # gem "predicate", path: "../predicate"
data/README.md CHANGED
@@ -1,16 +1,30 @@
1
1
  # Bmg, a relational algebra (Alf's successor)!
2
2
 
3
+ [![Build Status](https://travis-ci.com/enspirit/bmg.svg?branch=master)](https://travis-ci.com/enspirit/bmg)
4
+
3
5
  Bmg is a relational algebra implemented as a ruby library. It implements the
4
6
  [Relation as First-Class Citizen](http://www.try-alf.org/blog/2013-10-21-relations-as-first-class-citizen)
5
- paradigm contributed with Alf a few years ago.
6
-
7
- Like Alf, Bmg can be used to query relations in memory, from various files,
8
- SQL databases, and any data sources that can be seen as serving relations.
9
- Cross data-sources joins are supported, as with Alf.
10
-
11
- Unlike Alf, Bmg does not make any core ruby extension and exposes the
12
- object-oriented syntax only (not Alf's functional one). Bmg implementation is
13
- also much simpler, and make its easier to implement user-defined relations.
7
+ paradigm contributed with [Alf](http://www.try-alf.org/) a few years ago.
8
+
9
+ Bmg can be used to query relations in memory, from various files, SQL databases,
10
+ and any data source that can be seen as serving relations. Cross data-sources
11
+ joins are supported, as with Alf. For differences with Alf, see a section
12
+ further down this README.
13
+
14
+ ## Outline
15
+
16
+ * [Example](#example)
17
+ * [Where are base relations coming from?](#where-are-base-relations-coming-from)
18
+ * [Memory relations](#memory-relations)
19
+ * [Connecting to SQL databases](#connecting-to-sql-databases)
20
+ * [Reading files (csv, excel, text)](#reading-files-csv-excel-text)
21
+ * [Your own relations](#your-own-relations)
22
+ * [List of supported operators](#supported-operators)
23
+ * [How is this different?](#how-is-this-different)
24
+ * [... from similar libraries](#-from-similar-libraries)
25
+ * [... from Alf](#-from-alf)
26
+ * [Contribute](#contribute)
27
+ * [License](#license)
14
28
 
15
29
  ## Example
16
30
 
@@ -27,7 +41,7 @@ suppliers = Bmg::Relation.new([
27
41
  ])
28
42
 
29
43
  by_city = suppliers
30
- .restrict(Predicate.neq(status: 30))
44
+ .exclude(status: 30)
31
45
  .extend(upname: ->(t){ t[:name].upcase })
32
46
  .group([:sid, :name, :status], :suppliers_in)
33
47
 
@@ -35,76 +49,158 @@ puts JSON.pretty_generate(by_city)
35
49
  # [{...},...]
36
50
  ```
37
51
 
38
- ## Connecting to a SQL database
52
+ ## Where are base relations coming from?
53
+
54
+ Bmg sees relations as sets/enumerable of symbolized Ruby hashes. The following
55
+ sections show you how to get them in the first place, to enter Relationland.
56
+
57
+ ### Memory relations
58
+
59
+ If you have an Array of Hashes -- in fact any Enumerable -- you can easily get
60
+ a Relation using either `Bmg::Relation.new` or `Bmg.in_memory`.
61
+
62
+ ```ruby
63
+ # this...
64
+ r = Bmg::Relation.new [{id: 1}, {id: 2}]
65
+
66
+ # is the same as this...
67
+ r = Bmg.in_memory [{id: 1}, {id: 2}]
68
+
69
+ # entire algebra is available on `r`
70
+ ```
71
+
72
+ ### Connecting to SQL databases
39
73
 
40
- Bmg requires `sequel >= 3.0` to connect to SQL databases.
74
+ Bmg currently requires `sequel >= 3.0` to connect to SQL databases. You also
75
+ need to require `bmg/sequel`.
41
76
 
42
77
  ```ruby
43
78
  require 'sqlite3'
44
79
  require 'bmg'
45
80
  require 'bmg/sequel'
81
+ ```
46
82
 
47
- DB = Sequel.connect("sqlite://suppliers-and-parts.db")
83
+ Then `Bmg.sequel` serves relations for tables of your SQL database:
48
84
 
85
+ ```ruby
86
+ DB = Sequel.connect("sqlite://suppliers-and-parts.db")
49
87
  suppliers = Bmg.sequel(:suppliers, DB)
88
+ ```
89
+
90
+ The entire algebra is available on those relations. As long as you keep using
91
+ operators that can be translated to SQL, results remain SQL-able:
50
92
 
93
+ ```ruby
51
94
  big_suppliers = suppliers
52
- .restrict(Predicate.neq(status: 30))
95
+ .exclude(status: 30)
96
+ .project([:sid, :name])
53
97
 
54
98
  puts big_suppliers.to_sql
55
- # SELECT `t1`.`sid`, `t1`.`name`, `t1`.`status`, `t1`.`city` FROM `suppliers` AS 't1' WHERE (`t1`.`status` != 30)
99
+ # SELECT `t1`.`sid`, `t1`.`name` FROM `suppliers` AS 't1' WHERE (`t1`.`status` != 30)
100
+ ```
56
101
 
57
- puts JSON.pretty_generate(big_suppliers)
58
- # [{...},...]
102
+ Operators not translatable to SQL are available too (such as `group` below).
103
+ Bmg fallbacks to memory operators for them, but remains capable of pushing some
104
+ operators down the tree as illustrated below (the restriction on `:city` is
105
+ pushed to the SQL server):
106
+
107
+ ```ruby
108
+ Bmg.sequel(:suppliers, sequel_db)
109
+ .project([:sid, :name, :city])
110
+ .group([:sid, :name], :suppliers_in)
111
+ .restrict(city: ["Paris", "London"])
112
+ .debug
113
+
114
+ # (group
115
+ # (sequel SELECT `t1`.`sid`, `t1`.`name`, `t1`.`city` FROM `suppliers` AS 't1' WHERE (`t1`.`city` IN ('Paris', 'London')))
116
+ # [:sid, :name, :status]
117
+ # :suppliers_in
118
+ # {:array=>false})
59
119
  ```
60
120
 
61
- ## How is this different from similar libraries?
121
+ ### Reading files (csv, excel, text)
62
122
 
63
- 1. The libraries you probably know (Sequel, Arel, SQLAlchemy, Korma, jOOQ,
64
- etc.) do not implement a genuine relational algebra: their support for
65
- chaining relational operators is limited (yielding errors or wrong SQL
66
- queries). Bmg **always** allows chaining operators. If it does not, it's
67
- a bug. In other words, the following query is 100% valid:
123
+ Bmg provides simple adapters to read files and reach Relationland as soon as
124
+ possible.
68
125
 
69
- relation
70
- .restrict(...) # aka where
71
- .union(...)
72
- .summarize(...) # aka group by
73
- .restrict(...)
126
+ #### CSV files
74
127
 
75
- 2. Bmg supports in memory relations, json relations, csv relations, SQL
76
- relations and so on. It's not tight to SQL generation, and supports
77
- queries accross multiple data sources.
128
+ ```ruby
129
+ csv_options = { col_sep: ",", quote_char: '"' }
130
+ r = Bmg.csv("path/to/a/file.csv", csv_options)
131
+ ```
78
132
 
79
- 3. Bmg makes a best effort to optimize queries, simplifying both generated
80
- SQL code (low-level accesses to datasources) and in-memory operations.
133
+ Options are directly transmitted to `::CSV.new`, check ruby's standard
134
+ library.
81
135
 
82
- 4. Bmg supports various *structuring* operators (group, image, autowrap,
83
- autosummarize, etc.) and allows building 'non flat' relations.
136
+ #### Excel files
84
137
 
85
- ## How is this different from Alf?
138
+ You will need to add [`roo`](https://github.com/roo-rb/roo) to your Gemfile to
139
+ read `.xls` and `.xlsx` files with Bmg.
86
140
 
87
- 1. Bmg's implementation is much simpler than Alf, and uses no ruby core
88
- extention.
141
+ ```ruby
142
+ roo_options = { skip: 1 }
143
+ r = Bmg.excel("path/to/a/file.xls", roo_options)
144
+ ```
89
145
 
90
- 2. We are confident using Bmg in production. Systematic inspection of query
91
- plans is suggested though. Alf was a bit too experimental to be used on
92
- (critical) production systems.
146
+ Options are directly transmitted to `Roo::Spreadsheet.open`, check roo's
147
+ documentation.
93
148
 
94
- 2. Alf exposes a functional syntax, command line tool, restful tools and
95
- many more. Bmg is limited to the core algebra, main Relation abstraction
96
- and SQL generation.
149
+ #### Text files
97
150
 
98
- 3. Bmg is less strict regarding conformance to relational theory, and
99
- may actually expose non relational features (such as support for null,
100
- left_join operator, etc.). Sharp tools hurt, use them with great care.
151
+ There is also a straightforward way to read text files and convert lines to
152
+ tuples.
101
153
 
102
- 4. Bmg does not yet implement all operators documented on try-alf.org, even
103
- if we plan to eventually support them all.
154
+ ```ruby
155
+ r = Bmg.text_file("path/to/a/file.txt")
156
+ r.type.attrlist
157
+ # => [:line, :text]
158
+ ```
104
159
 
105
- 5. Bmg has a few additional operators that prove very useful on real
106
- production use cases: prefix, suffix, autowrap, autosummarize, left_join,
107
- rxmatch, etc.
160
+ Without options tuples will have `:line` and `:text` attributes, the former
161
+ being the line number (starting at 1) and the latter being the line itself
162
+ (stripped).
163
+
164
+ The are a couple of options (see `Bmg::Reader::Textfile`). The most useful one
165
+ is the use a of a Regexp with named captures to automatically extract
166
+ attributes:
167
+
168
+ ```ruby
169
+ r = Bmg.text_file("path/to/a/file.txt", parse: /GET (?<url>([^\s]+))/)
170
+ r.type.attrlist
171
+ # => [:line, :url]
172
+ ```
173
+
174
+ In this scenario, non matching lines are skipped. The `:line` attribute keeps
175
+ being used to have at least one candidate key (so to speak).
176
+
177
+ ### Your own relations
178
+
179
+ As noted earlier, Bmg has a simple relation interface where you only have to
180
+ provide an iteration of symbolized tuples.
181
+
182
+ ```ruby
183
+ class MyRelation
184
+ include Bmg::Relation
185
+
186
+ def each
187
+ yield(id: 1, name: "Alf", year: 2014)
188
+ yield(id: 2, name: "Bmg", year: 2018)
189
+ end
190
+ end
191
+
192
+ MyRelation.new
193
+ .restrict(Predicate.gt(:year, 2015))
194
+ .allbut([:year])
195
+ ```
196
+
197
+ As shown, creating adapters on top of various data source is straighforward.
198
+ Adapters can also participate to query optimization (such as pushing
199
+ restrictions down the tree) by overriding the underscored version of operators
200
+ (e.g. `_restrict`).
201
+
202
+ Have a look at `Bmg::Algebra` for the protocol and `Bmg::Sql::Relation` for an
203
+ example. Keep in touch with the team if you need some help.
108
204
 
109
205
  ## Supported operators
110
206
 
@@ -114,8 +210,10 @@ r.autowrap(split: '_') # structure a flat relation, split:
114
210
  r.autosummarize([:a, :b, ...], x: :sum) # (experimental) usual summarizers supported
115
211
  r.constants(x: 12, ...) # add constant attributes (sometimes useful in unions)
116
212
  r.extend(x: ->(t){ ... }, ...) # add computed attributes
213
+ r.exclude(predicate) # shortcut for restrict(!predicate)
117
214
  r.group([:a, :b, ...], :x) # relation-valued attribute from attributes
118
215
  r.image(right, :x, [:a, :b, ...]) # relation-valued attribute from another relation
216
+ r.images({:x => r1, :y => r2}, [:a, ...]) # shortcut over image(r1, :x, ...).image(r2, :y, ...)
119
217
  r.join(right, [:a, :b, ...]) # natural join on a join key
120
218
  r.join(right, :a => :x, :b => :y, ...) # natural join after right reversed renaming
121
219
  r.left_join(right, [:a, :b, ...], {...}) # left join with optional default right tuple
@@ -137,14 +235,95 @@ t.transform(&:to_s) # similar, but Proc-driven
137
235
  t.transform(:foo => :upcase, ...) # specific-attrs tranformation
138
236
  t.transform([:to_s, :upcase]) # chain-transformation
139
237
  r.union(right) # relational union
238
+ r.where(predicate) # alias for restrict(predicate)
140
239
  ```
141
240
 
142
- ## Who is behind Bmg?
241
+ ## How is this different?
242
+
243
+ ### ... from similar libraries?
244
+
245
+ 1. The libraries you probably know (Sequel, Arel, SQLAlchemy, Korma, jOOQ,
246
+ etc.) do not implement a genuine relational algebra. Their support for
247
+ chaining relational operators is thus limited (restricting your expression
248
+ power and/or raising errors and/or outputting wrong or counterintuitive
249
+ SQL code). Bmg **always** allows chaining operators. If it does not, it's
250
+ a bug.
251
+
252
+ For instance the expression below is 100% valid in Bmg. The last where
253
+ clause applies to the result of the summarize (while SQL requires a `HAVING`
254
+ clause, or a `SELECT ... FROM (SELECT ...) r`).
255
+
256
+ ```ruby
257
+ relation
258
+ .where(...)
259
+ .union(...)
260
+ .summarize(...) # aka group by
261
+ .where(...)
262
+ ```
263
+
264
+ 2. Bmg supports in memory relations, json relations, csv relations, SQL
265
+ relations and so on. It's not tight to SQL generation, and supports
266
+ queries accross multiple data sources.
267
+
268
+ 3. Bmg makes a best effort to optimize queries, simplifying both generated
269
+ SQL code (low-level accesses to datasources) and in-memory operations.
270
+
271
+ 4. Bmg supports various *structuring* operators (group, image, autowrap,
272
+ autosummarize, etc.) and allows building 'non flat' relations.
273
+
274
+ 5. Bmg can use full ruby power when that helps (e.g. regular expressions in
275
+ WHERE clauses or ruby code in EXTEND clauses). This may prevent Bmg from
276
+ delegating work to underlying data sources (e.g. SQL server) and should
277
+ therefore be used with care though.
278
+
279
+ ### ... from Alf?
280
+
281
+ If you use Alf (or used it in the past), below are the main differences between
282
+ Bmg and Alf. Bmg has NOT been written to be API-compatible with Alf and will
283
+ probably never be.
284
+
285
+ 1. Bmg's implementation is much simpler than Alf and uses no ruby core
286
+ extention.
287
+
288
+ 2. We are confident using Bmg in production. Systematic inspection of query
289
+ plans is advised though. Alf was a bit too experimental to be used on
290
+ (critical) production systems.
291
+
292
+ 3. Alf exposes a functional syntax, command line tool, restful tools and
293
+ many more. Bmg is limited to the core algebra, main Relation abstraction
294
+ and SQL generation.
143
295
 
144
- Bernard Lambeau (bernard@klaro.cards) is Alf & Bmg main engineer & maintainer.
296
+ 4. Bmg is less strict regarding conformance to relational theory, and
297
+ may actually expose non relational features (such as support for null,
298
+ left_join operator, etc.). Sharp tools hurt, use them with care.
299
+
300
+ 5. Unlike Alf::Relation instances of Bmg::Relation capture query-trees, not
301
+ values. Currently two instances `r1` and `r2` are not equal even if they
302
+ define the same mathematical relation. As a consequence joining on
303
+ relation-valued attributes does not work as expected in Bmg until further
304
+ notice.
305
+
306
+ 6. Bmg does not implement all operators documented on try-alf.org, even if
307
+ we plan to eventually support most of them.
308
+
309
+ 7. Bmg has a few additional operators that prove very useful on real
310
+ production use cases: prefix, suffix, autowrap, autosummarize, left_join,
311
+ rxmatch, etc.
312
+
313
+ 8. Bmg optimizes queries and compiles them to SQL on the fly, while Alf was
314
+ building an AST internally first. Strictly speaking this makes Bmg less
315
+ powerful than Alf since optimizations cannot be turned off for now.
316
+
317
+ ## Contribute
318
+
319
+ Please use github issues and pull requests for all questions, bug reports,
320
+ and contributions. Don't hesitate to get in touch with us with an early code
321
+ spike if you plan to add non trivial features.
322
+
323
+ ## Licence
324
+
325
+ This software is distributed by Enspirit SRL under a MIT Licence. Please
326
+ contact Bernard Lambeau (blambeau@gmail.com) with any question.
145
327
 
146
328
  Enspirit (https://enspirit.be) and Klaro App (https://klaro.cards) are both
147
329
  actively using and contributing to the library.
148
-
149
- Feel free to contact us for help, ideas and/or contributions. Please use github
150
- issues and pull requests if possible if code is involved.
data/lib/bmg.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'path'
2
2
  require 'predicate'
3
3
  require 'forwardable'
4
+ require 'set'
4
5
  module Bmg
5
6
 
6
7
  def in_memory(enumerable, type = Type::ANY)
@@ -8,6 +9,11 @@ module Bmg
8
9
  end
9
10
  module_function :in_memory
10
11
 
12
+ def text_file(path, options = {}, type = Type::ANY)
13
+ Reader::TextFile.new(type, path, options).spied(main_spy)
14
+ end
15
+ module_function :text_file
16
+
11
17
  def csv(path, options = {}, type = Type::ANY)
12
18
  Reader::Csv.new(type, path, options).spied(main_spy)
13
19
  end
data/lib/bmg/algebra.rb CHANGED
@@ -174,6 +174,7 @@ module Bmg
174
174
 
175
175
  def transform(transformation = nil, options = {}, &proc)
176
176
  transformation, options = proc, (transformation || {}) unless proc.nil?
177
+ return self if transformation.is_a?(Hash) && transformation.empty?
177
178
  _transform(self.type.transform(transformation, options), transformation, options)
178
179
  end
179
180
 
@@ -2,6 +2,14 @@ module Bmg
2
2
  module Algebra
3
3
  module Shortcuts
4
4
 
5
+ def where(predicate)
6
+ restrict(predicate)
7
+ end
8
+
9
+ def exclude(predicate)
10
+ restrict(!Predicate.coerce(predicate))
11
+ end
12
+
5
13
  def rxmatch(attrs, matcher, options = {})
6
14
  predicate = attrs.inject(Predicate.contradiction){|p,a|
7
15
  p | Predicate.match(a, matcher, options)
@@ -31,6 +39,12 @@ module Bmg
31
39
  self.image(right.rename(renaming), as, on.keys, options)
32
40
  end
33
41
 
42
+ def images(rights, on = [], options = {})
43
+ rights.each_pair.inject(self){|memo,(as,right)|
44
+ memo.image(right, as, on, options)
45
+ }
46
+ end
47
+
34
48
  def join(right, on = [])
35
49
  return super unless on.is_a?(Hash)
36
50
  renaming = Hash[on.map{|k,v| [v,k] }]
@@ -30,6 +30,7 @@ module Bmg
30
30
  public
31
31
 
32
32
  def each
33
+ return to_enum unless block_given?
33
34
  seen = {}
34
35
  @operand.each do |tuple|
35
36
  allbuted = tuple_allbut(tuple)
@@ -63,12 +64,38 @@ module Bmg
63
64
 
64
65
  protected ### optimization
65
66
 
67
+ def _allbut(type, butlist)
68
+ operand.allbut(self.butlist|butlist)
69
+ end
70
+
71
+ def _matching(type, right, on)
72
+ # Always possible to push the matching, since by construction
73
+ # `on` can only use attributes that have not been trown away,
74
+ # hence they exist on `operand` too.
75
+ operand.matching(right, on).allbut(butlist)
76
+ end
77
+
78
+ def _page(type, ordering, page_index, options)
79
+ return super unless self.preserving_key?
80
+ operand.page(ordering, page_index, options).allbut(butlist)
81
+ end
82
+
83
+ def _project(type, attrlist)
84
+ operand.project(attrlist)
85
+ end
86
+
66
87
  def _restrict(type, predicate)
67
88
  operand.restrict(predicate).allbut(butlist)
68
89
  end
69
90
 
70
91
  protected ### inspect
71
92
 
93
+ def preserving_key?
94
+ operand.type.knows_keys? && operand.type.keys.find{|k|
95
+ (k & butlist).empty?
96
+ }
97
+ end
98
+
72
99
  def args
73
100
  [ butlist ]
74
101
  end