alf 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (187) hide show
  1. data/CHANGELOG.md +89 -0
  2. data/Gemfile.lock +6 -1
  3. data/README.md +35 -21
  4. data/TODO.md +0 -5
  5. data/alf.gemspec +2 -0
  6. data/alf.noespec +6 -4
  7. data/bin/alf +9 -13
  8. data/examples/{autonum.alf → operators/autonum.alf} +0 -0
  9. data/examples/{cities.rash → operators/cities.rash} +0 -0
  10. data/examples/{clip.alf → operators/clip.alf} +0 -0
  11. data/examples/{compact.alf → operators/compact.alf} +0 -0
  12. data/examples/{database.alf → operators/database.alf} +1 -1
  13. data/examples/{defaults.alf → operators/defaults.alf} +0 -0
  14. data/examples/{extend.alf → operators/extend.alf} +0 -0
  15. data/examples/{group.alf → operators/group.alf} +0 -0
  16. data/examples/{intersect.alf → operators/intersect.alf} +0 -0
  17. data/examples/{join.alf → operators/join.alf} +0 -0
  18. data/examples/operators/matching.alf +2 -0
  19. data/examples/{minus.alf → operators/minus.alf} +0 -0
  20. data/examples/operators/not_matching.alf +2 -0
  21. data/examples/{nulls.rash → operators/nulls.rash} +0 -0
  22. data/examples/{parts.rash → operators/parts.rash} +0 -0
  23. data/examples/{project.alf → operators/project.alf} +0 -0
  24. data/examples/{pseudo-with.alf → operators/pseudo-with.alf} +0 -0
  25. data/examples/{quota.alf → operators/quota.alf} +0 -0
  26. data/examples/operators/rank.alf +4 -0
  27. data/examples/{rename.alf → operators/rename.alf} +0 -0
  28. data/examples/{restrict.alf → operators/restrict.alf} +0 -0
  29. data/examples/{schema.yaml → operators/schema.yaml} +0 -0
  30. data/examples/{sort.alf → operators/sort.alf} +0 -0
  31. data/examples/{summarize.alf → operators/summarize.alf} +0 -0
  32. data/examples/{suppliers.rash → operators/suppliers.rash} +0 -0
  33. data/examples/{supplies.rash → operators/supplies.rash} +0 -0
  34. data/examples/{ungroup.alf → operators/ungroup.alf} +0 -0
  35. data/examples/{union.alf → operators/union.alf} +0 -0
  36. data/examples/{unwrap.alf → operators/unwrap.alf} +0 -0
  37. data/examples/{wrap.alf → operators/wrap.alf} +0 -0
  38. data/lib/alf.rb +837 -62
  39. data/lib/alf/loader.rb +2 -1
  40. data/lib/alf/text.rb +160 -0
  41. data/lib/alf/version.rb +1 -1
  42. data/lib/alf/yaml.rb +24 -0
  43. data/spec/integration/__database__/group.alf +3 -0
  44. data/spec/integration/__database__/parts.rash +6 -0
  45. data/spec/integration/__database__/suppliers.rash +5 -0
  46. data/spec/integration/__database__/supplies.rash +12 -0
  47. data/spec/integration/command/alf/alf_e.cmd +1 -0
  48. data/spec/integration/command/alf/alf_e.stdout +4 -0
  49. data/spec/integration/command/alf/alf_env.cmd +1 -0
  50. data/spec/integration/command/alf/alf_env.stdout +5 -0
  51. data/spec/integration/command/alf/alf_implicit.alf +1 -0
  52. data/spec/integration/command/alf/alf_implicit_exec.cmd +1 -0
  53. data/spec/integration/command/alf/alf_implicit_exec.stdout +4 -0
  54. data/spec/integration/command/alf/alf_r.cmd +1 -0
  55. data/spec/integration/command/alf/alf_r.stdout +5 -0
  56. data/spec/integration/command/alf/alf_version.cmd +1 -0
  57. data/spec/integration/command/alf/alf_version.stdout +2 -0
  58. data/spec/integration/command/alf/alf_yaml.cmd +1 -0
  59. data/spec/integration/command/alf/alf_yaml.stdout +22 -0
  60. data/spec/integration/command/alf/rel.rash +1 -0
  61. data/spec/integration/command/autonum/autonum_0.cmd +1 -0
  62. data/spec/integration/command/autonum/autonum_0.stdout +9 -0
  63. data/spec/integration/command/autonum/autonum_1.cmd +1 -0
  64. data/spec/integration/command/autonum/autonum_1.stdout +9 -0
  65. data/spec/integration/command/clip/clip_0.cmd +1 -0
  66. data/spec/integration/command/clip/clip_0.stdout +9 -0
  67. data/spec/integration/command/clip/clip_1.cmd +1 -0
  68. data/spec/integration/command/clip/clip_1.stdout +9 -0
  69. data/spec/integration/command/compact/compact_0.cmd +1 -0
  70. data/spec/integration/command/compact/compact_0.stdout +9 -0
  71. data/spec/integration/command/defaults/defaults_0.cmd +1 -0
  72. data/spec/integration/command/defaults/defaults_0.stdout +9 -0
  73. data/spec/integration/command/defaults/defaults_1.cmd +1 -0
  74. data/spec/integration/command/defaults/defaults_1.stdout +9 -0
  75. data/spec/integration/command/extend/extend_0.cmd +1 -0
  76. data/spec/integration/command/extend/extend_0.stdout +16 -0
  77. data/spec/integration/command/group/group_0.cmd +1 -0
  78. data/spec/integration/command/group/group_0.stdout +32 -0
  79. data/spec/integration/command/group/group_1.cmd +1 -0
  80. data/spec/integration/command/group/group_1.stdout +32 -0
  81. data/spec/integration/command/intersect/intersect_0.cmd +1 -0
  82. data/spec/integration/command/intersect/intersect_0.stdout +9 -0
  83. data/spec/integration/command/join/join_0.cmd +1 -0
  84. data/spec/integration/command/join/join_0.stdout +16 -0
  85. data/spec/integration/command/matching/matching_0.cmd +1 -0
  86. data/spec/integration/command/matching/matching_0.stdout +8 -0
  87. data/spec/integration/command/minus/minus_0.cmd +1 -0
  88. data/spec/integration/command/minus/minus_0.stdout +4 -0
  89. data/spec/integration/command/not-matching/not-matching_0.cmd +1 -0
  90. data/spec/integration/command/not-matching/not-matching_0.stdout +5 -0
  91. data/spec/integration/command/project/project_0.cmd +1 -0
  92. data/spec/integration/command/project/project_0.stdout +9 -0
  93. data/spec/integration/command/project/project_1.cmd +1 -0
  94. data/spec/integration/command/project/project_1.stdout +9 -0
  95. data/spec/integration/command/quota/quota_0.cmd +1 -0
  96. data/spec/integration/command/quota/quota_0.stdout +16 -0
  97. data/spec/integration/command/rank/rank_1.cmd +1 -0
  98. data/spec/integration/command/rank/rank_1.stdout +10 -0
  99. data/spec/integration/command/rank/rank_2.cmd +1 -0
  100. data/spec/integration/command/rank/rank_2.stdout +10 -0
  101. data/spec/integration/command/rank/rank_3.cmd +1 -0
  102. data/spec/integration/command/rank/rank_3.stdout +10 -0
  103. data/spec/integration/command/rank/rank_4.cmd +1 -0
  104. data/spec/integration/command/rank/rank_4.stdout +6 -0
  105. data/spec/integration/command/rank/rank_5.cmd +1 -0
  106. data/spec/integration/command/rank/rank_5.stdout +6 -0
  107. data/spec/integration/command/rename/rename_0.cmd +1 -0
  108. data/spec/integration/command/rename/rename_0.stdout +9 -0
  109. data/spec/integration/command/restrict/restrict_0.cmd +1 -0
  110. data/spec/integration/command/restrict/restrict_0.stdout +6 -0
  111. data/spec/integration/command/restrict/restrict_1.cmd +1 -0
  112. data/spec/integration/command/restrict/restrict_1.stdout +6 -0
  113. data/spec/integration/command/show/show_base.cmd +1 -0
  114. data/spec/integration/command/show/show_base.stdout +9 -0
  115. data/spec/integration/command/show/show_conflictual.cmd +1 -0
  116. data/spec/integration/command/show/show_conflictual.stdout +5 -0
  117. data/spec/integration/command/show/show_rash.cmd +1 -0
  118. data/spec/integration/command/show/show_rash.stdout +5 -0
  119. data/spec/integration/command/show/show_rash_2.cmd +1 -0
  120. data/spec/integration/command/show/show_rash_2.stdout +5 -0
  121. data/spec/integration/command/show/show_yaml.cmd +1 -0
  122. data/spec/integration/command/show/show_yaml.stdout +22 -0
  123. data/spec/integration/command/sort/sort_0.cmd +1 -0
  124. data/spec/integration/command/sort/sort_0.stdout +9 -0
  125. data/spec/integration/command/sort/sort_1.cmd +1 -0
  126. data/spec/integration/command/sort/sort_1.stdout +9 -0
  127. data/spec/integration/command/summarize/summarize_0.cmd +1 -0
  128. data/spec/integration/command/summarize/summarize_0.stdout +8 -0
  129. data/spec/integration/command/ungroup/ungroup_0.cmd +1 -0
  130. data/spec/integration/command/ungroup/ungroup_0.stdout +16 -0
  131. data/spec/integration/command/union/union_0.cmd +1 -0
  132. data/spec/integration/command/union/union_0.stdout +9 -0
  133. data/spec/integration/command/unwrap/unwrap_0.cmd +1 -0
  134. data/spec/integration/command/unwrap/unwrap_0.stdout +9 -0
  135. data/spec/integration/command/wrap/wrap_0.cmd +1 -0
  136. data/spec/integration/command/wrap/wrap_0.stdout +9 -0
  137. data/spec/integration/semantics/test_join.alf +9 -0
  138. data/spec/integration/{src → semantics}/test_minus.alf +0 -0
  139. data/spec/integration/{src → semantics}/test_project.alf +0 -0
  140. data/spec/integration/semantics/test_rank.alf +34 -0
  141. data/spec/integration/test_command.rb +36 -0
  142. data/spec/integration/test_examples.rb +11 -22
  143. data/spec/integration/test_semantics.rb +40 -0
  144. data/spec/regression/alf_file/__FILE__.alf +2 -0
  145. data/spec/regression/alf_file/suppliers.rash +5 -0
  146. data/spec/regression/alf_file/test___FILE__.rb +17 -0
  147. data/spec/regression/heading/test_heading_with_date.rb +12 -0
  148. data/spec/regression/lispy/test_compile.rb +14 -0
  149. data/spec/regression/relation/test_relation_with_date.rb +12 -0
  150. data/spec/regression/restrict/test_restrict_with_keywords.rb +17 -0
  151. data/spec/shared/a_value.rb +12 -0
  152. data/spec/shared/an_operator_class.rb +35 -0
  153. data/spec/spec_helper.rb +12 -34
  154. data/spec/unit/assumptions/test_file.rb +17 -0
  155. data/spec/unit/{test_assumptions.rb → assumptions/test_instance_eval.rb} +1 -1
  156. data/spec/unit/assumptions/test_scoping.rb +29 -0
  157. data/spec/unit/environment/test_folder.rb +6 -1
  158. data/spec/unit/operator/relational/matching/test_hash_based.rb +60 -0
  159. data/spec/unit/operator/relational/not_matching/test_hash_based.rb +37 -0
  160. data/spec/unit/operator/relational/test_rank.rb +50 -0
  161. data/spec/unit/operator/test_relational.rb +3 -0
  162. data/spec/unit/reader/test_alf_file.rb +7 -4
  163. data/spec/unit/reader/test_initialize.rb +60 -0
  164. data/spec/unit/relation/test_relops.rb +4 -0
  165. data/spec/unit/relation/test_to_a.rb +41 -0
  166. data/spec/unit/renderer/test_initialize.rb +60 -0
  167. data/spec/unit/test_environment.rb +38 -0
  168. data/spec/unit/test_heading.rb +38 -0
  169. data/spec/unit/test_reader.rb +5 -0
  170. data/spec/unit/test_relation.rb +31 -1
  171. data/spec/unit/test_renderer.rb +1 -1
  172. data/spec/unit/{renderer/text → text}/test_cell.rb +1 -1
  173. data/spec/unit/{renderer/text → text}/test_row.rb +1 -1
  174. data/spec/unit/{renderer/text → text}/test_table.rb +1 -1
  175. data/spec/unit/tools/test_ordering_key.rb +13 -0
  176. data/spec/unit/tools/test_parse_commandline_args.rb +47 -0
  177. data/spec/unit/tools/test_tuple_handle.rb +34 -2
  178. data/spec/unit/tools/test_varargs.rb +16 -0
  179. data/tasks/{spec_test.rake → integration_test.rake} +4 -32
  180. data/tasks/regression_test.rake +52 -0
  181. data/tasks/unit_test.rake +33 -58
  182. metadata +326 -66
  183. data/examples/runall.sh +0 -26
  184. data/lib/alf/relation.rb +0 -118
  185. data/lib/alf/renderer/text.rb +0 -153
  186. data/lib/alf/renderer/yaml.rb +0 -22
  187. data/spec/integration/test_alf_specs.rb +0 -37
data/CHANGELOG.md CHANGED
@@ -1,3 +1,92 @@
1
+ # 0.9.3 / FIX ME
2
+
3
+ * New operators (available both in shell and in Lispy DSL)
4
+
5
+ * Added MATCHING and NOT MATCHING operators. These operators are useful
6
+ shortcuts for the following expressions.
7
+
8
+ (matching l, r) := (project (join l, r), [l's attributes])
9
+ (not_matching l, r) := (minus l, (matching l, r))
10
+
11
+ For example:
12
+
13
+ # Give suppliers that supply at least one part
14
+ (matching suppliers, supplies)
15
+
16
+ # Give suppliers that don't supply any part
17
+ (not_matching suppliers, supplies)
18
+
19
+ * Added RANK operator. The RANK operator is useful for for computing quota
20
+ queries, as shown below. See 'alf help rank' for details.
21
+
22
+ # Give the three heaviest parts
23
+ (allbut (restrict (rank :parts, [[:weight, :desc]], :pos), lambda{ pos < 3 }), [:pos])
24
+
25
+ * Enhancements when using Alf in shell
26
+
27
+ * added alf's -r option, that mimics ruby's one (require library before run)
28
+
29
+ * When alf is invoked in shell using bin/alf (and only in this case),
30
+ ENV['ALF_OPTS'] is used a global options to apply as they were specified
31
+ inline:
32
+
33
+ % export ALF_OPTS="--env=. --yaml"
34
+ % alf show suppliers
35
+
36
+ is the same as
37
+
38
+ % alf --env=. --yaml show suppliers
39
+
40
+ * 'alf --help' now distinguish experimental operators from those coming from
41
+ the (much more stable) TUTORIAL D specification. The former should be used
42
+ with care as they specification may change at any time.
43
+
44
+ * Enhancements when using Alf in Ruby
45
+
46
+ * Alf.lispy now accepts any argument recognized by Environment.autodetect; it
47
+ obtains its working Environment this way. Among others:
48
+
49
+ Alf.lispy(Alf::Environment.folder("path/to/an/existing/folder"))
50
+
51
+ is the same as:
52
+
53
+ Alf.lispy("path/to/an/existing/folder")
54
+
55
+ * Added Relation::DUM and Relation::DEE constants (relations of empty heading
56
+ with no and one tuple, respectively). They are also available as DUM and DEE
57
+ in Lispy functional expressions.
58
+
59
+ * Added a Heading abstraction, as a set of attribute (name, type) pairs
60
+
61
+ * Internal enhancements (extension points)
62
+
63
+ * The Reader and Renderer classes now accept a Hash of options as third
64
+ argument of the constructor (friendly varargs applies there). These options
65
+ can be used by extension points.
66
+
67
+ * The Environment class now provides a class-based registering mechanism 'ala'
68
+ Reader and Renderer. This allows auto-detecting the target environment when
69
+ --env=... is used in shell. See Environment.autodetect and
70
+ Environment#recognizes? for contributing to this extension point.
71
+
72
+ * Internals now rely on Myrrha for code generation. This means that all
73
+ datatypes can now be safely used in relations and dumped to .rash files in
74
+ particular.
75
+
76
+ * Bug fixes
77
+
78
+ * Added Relation#allbut, forgotten in two previous releases
79
+ * Fixed (join xxx, DEE) and (join xxx, DUM)
80
+ * Fixed scoping bug when using attributes named :path, :expr or :block in
81
+ Lispy compiled expressions (coming from .alf files)
82
+ * Fixed 'alf --yaml show suppliers' that renderer a --text table instead of
83
+ a yaml output
84
+ * Fixed bugs when using Date and Time attributes with .rash files
85
+ * Fixed bugs when using Date and Time attributes in restrict expressions
86
+ compiled from the commandline
87
+ * Fixed a few bugs when using attribute names that are ruby keywords
88
+ (restrict & extend)
89
+
1
90
  # 0.9.2 / 2011.07.13
2
91
 
3
92
  * Bug fixes
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- alf (0.9.2)
4
+ alf (0.9.3)
5
+ myrrha (~> 1.0.0)
5
6
  quickl (~> 0.2.2)
6
7
 
7
8
  GEM
@@ -10,12 +11,15 @@ GEM
10
11
  bluecloth (2.0.11)
11
12
  diff-lcs (1.1.2)
12
13
  highline (1.6.2)
14
+ myrrha (1.0.0)
13
15
  noe (1.3.0)
14
16
  highline (~> 1.6.0)
15
17
  quickl (~> 0.2.0)
16
18
  wlang (~> 0.10.1)
17
19
  quickl (0.2.2)
18
20
  rake (0.9.2)
21
+ rcov (0.9.9)
22
+ rcov (0.9.9-java)
19
23
  rspec (2.6.0)
20
24
  rspec-core (~> 2.6.0)
21
25
  rspec-expectations (~> 2.6.0)
@@ -37,6 +41,7 @@ DEPENDENCIES
37
41
  bundler (~> 1.0)
38
42
  noe (~> 1.3.0)
39
43
  rake (~> 0.9.2)
44
+ rcov (~> 0.9.9)
40
45
  rspec (~> 2.6.0)
41
46
  wlang (~> 0.10.1)
42
47
  yard (~> 0.7.2)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Alf - Relational Algebra at your fingertips (version 0.9.1)
1
+ # Alf - Relational Algebra at your fingertips (version 0.9.3)
2
2
 
3
3
  ## Description
4
4
 
@@ -15,6 +15,11 @@ _relations_... Let's stop the segregation ;-)
15
15
  % [sudo] gem install alf
16
16
  % alf --help
17
17
 
18
+ ### Bundler & Require
19
+
20
+ # API is not considered stable enough for now, please use
21
+ gem "alf", "= 0.9.2"
22
+
18
23
  ### Links
19
24
 
20
25
  * {http://rubydoc.info/github/blambeau/alf/master/frames} (read this file there!)
@@ -31,7 +36,7 @@ of a truly relational algebra approach. Objectives behind Alf are manifold:
31
36
  as (the physical encoding of) a relation**. See 'alf --help' for the list of
32
37
  available commands and implemented relational operators.
33
38
 
34
- % alf restrict suppliers -- "city == 'London'" | alf join cities
39
+ % alf restrict suppliers -- "city == 'London'" | alf join cities
35
40
 
36
41
  * Alf is also a 100% Ruby relational algebra implementation shipped with a simple
37
42
  to use, powerful, functional DSL for compiling and evaluating relational queries.
@@ -40,9 +45,9 @@ of a truly relational algebra approach. Objectives behind Alf are manifold:
40
45
  section). See 'alf --help' as well as .alf files in the examples directory
41
46
  for syntactic examples.
42
47
 
43
- Alf.lispy.evaluate {
44
- (join (restrict :suppliers, lambda{ city == 'London' }), :cities)
45
- }
48
+ Alf.lispy.evaluate {
49
+ (join (restrict :suppliers, lambda{ city == 'London' }), :cities)
50
+ }
46
51
 
47
52
  In addition to this functional syntax, Alf comes bundled with an in-memory
48
53
  Relation data structure that provides an object-oriented way of manipulating
@@ -71,7 +76,7 @@ of a truly relational algebra approach. Objectives behind Alf are manifold:
71
76
  the following query for the kind of things that you'll never ever have in SQL
72
77
  (see also 'alf help quota', 'alf help wrap', 'alf help group', ...):
73
78
 
74
- % alf --text summarize supplies --by=sid -- total "sum(:qty)" -- which "group(:pid)"
79
+ % alf --text summarize supplies --by=sid -- total "sum(:qty)" -- which "group(:pid)"
75
80
 
76
81
  * Last, but not least, Alf is an attempt to help me test some research ideas and
77
82
  communicate about them with people that already know (all or part) of the TTM
@@ -82,7 +87,7 @@ of a truly relational algebra approach. Objectives behind Alf are manifold:
82
87
  'research work in progress', and used with care because not necessarily in
83
88
  conformance with the TTM.
84
89
 
85
- % alf --text quota supplies --by=sid --order=qty -- pos "count()"
90
+ % alf --text quota supplies --by=sid --order=qty -- pos "count()"
86
91
 
87
92
  ## Overview of relational theory
88
93
 
@@ -695,8 +700,7 @@ An environment built that way will look for .rash and .alf files in the specifie
695
700
  folder and sub-folders. I'll of course strongly consider any contribution
696
701
  implementing the Environment contract on top of SQL or NoSQL databases or anything
697
702
  that can be useful to manipulate with relational algebra. Such contributions can
698
- be added to the project directly, in the lib/alf/environment folder, for example.
699
- A base template would look like:
703
+ be added to the project directly. A base template would look like:
700
704
 
701
705
  class Foo < Alf::Environment
702
706
 
@@ -710,6 +714,9 @@ A base template would look like:
710
714
 
711
715
  end
712
716
 
717
+ Read more about Environment's API so as to let your environment be recognized
718
+ in shell (--env=...) on rubydoc.info
719
+
713
720
  ### Adding file decoders, aka Readers
714
721
 
715
722
  Environments should not be confused with Readers (see Reader class and its
@@ -837,18 +844,25 @@ as my own wish list, while I would love hearing yours instead.
837
844
  ### Versioning policy
838
845
 
839
846
  Alf respects {http://semver.org/ semantic versioning}, which means that it has
840
- a X.Y.Z version number and follows a few rules:
841
-
842
- - The public API is made of both the commandline tool as well as the Lispy
843
- dialect and will become stable with version 1.0.0 in a near future.
844
- - Backward compatible bug fixes will increase Z.
845
- - New features and enhancements that do not break backward compatibility of the
846
- public API will increase the Y number.
847
- - Non backward compatible changes of the public API will increase the X number.
848
-
849
- All classes and modules but the Alf module itself and the Lispy DSL are part of
850
- the private API and may change at any time. A best-effort strategy is followed
851
- to avoid breaking internals on tiny (Z) version increases.
847
+ a X.Y.Z version number and follows a few rules.
848
+
849
+ - The public API is made of the commandline tool, the Lispy dialect and the
850
+ Relation datastructure. This API will become stable with version 1.0.0 in a
851
+ near future.
852
+ - Currently, version 1.0.0 **has not been reached**. It means that **anything
853
+ may change at any time**. Best effort will be done to upgrade Y when backward
854
+ incompatible changes occur.
855
+ - Once 1.0.0 will be reached, the following rules will be followed:
856
+ - Backward compatible bug fixes will increase Z.
857
+ - New features and enhancements that do not break backward compatibility of
858
+ the public API will increase the Y number.
859
+ - Non backward compatible changes of the public API will increase the X
860
+ number.
861
+
862
+ All classes and modules but Alf module, the Lispy DSL and Alf::Relation are part
863
+ of the private API and may change at any time. A best-effort strategy is followed
864
+ to avoid breaking internals on tiny (Z) version increases, especially extension
865
+ points like Reader and Renderer.
852
866
 
853
867
  ## Enjoy Alf!
854
868
 
data/TODO.md CHANGED
@@ -18,9 +18,4 @@
18
18
 
19
19
  * Add PIVOT and UNPIVOT operators
20
20
 
21
- * Add MATCHING, NOT_MATCHING
22
-
23
- * Add a to_ruby abstraction and replace inspect usages in TupleHandle and
24
- Rash renderer
25
-
26
21
  * Find a way to complete the description of Quota...
data/alf.gemspec CHANGED
@@ -126,11 +126,13 @@ Gem::Specification.new do |s|
126
126
  s.add_development_dependency("rake", "~> 0.9.2")
127
127
  s.add_development_dependency("bundler", "~> 1.0")
128
128
  s.add_development_dependency("rspec", "~> 2.6.0")
129
+ s.add_development_dependency("rcov", "~> 0.9.9")
129
130
  s.add_development_dependency("yard", "~> 0.7.2")
130
131
  s.add_development_dependency("bluecloth", "~> 2.0.9")
131
132
  s.add_development_dependency("wlang", "~> 0.10.1")
132
133
  s.add_development_dependency("noe", "~> 1.3.0")
133
134
  s.add_dependency("quickl", "~> 0.2.2")
135
+ s.add_dependency("myrrha", "~> 1.0.0")
134
136
 
135
137
  # The version of ruby required by this gem
136
138
  #
data/alf.noespec CHANGED
@@ -1,13 +1,16 @@
1
1
  template-info:
2
2
  name: "ruby"
3
3
  version: 1.3.0
4
+ manifest:
5
+ tasks/unit_test.rake:
6
+ safe-override: false
4
7
  variables:
5
8
  lower:
6
9
  alf
7
10
  upper:
8
11
  Alf
9
12
  version:
10
- 0.9.2
13
+ 0.9.3
11
14
  summary: |-
12
15
  Relational Algebra at your fingertips
13
16
  description: |-
@@ -26,11 +29,10 @@ variables:
26
29
  - {name: rake, version: "~> 0.9.2", groups: [development]}
27
30
  - {name: bundler, version: "~> 1.0", groups: [development]}
28
31
  - {name: rspec, version: "~> 2.6.0", groups: [development]}
32
+ - {name: rcov, version: "~> 0.9.9", groups: [development]}
29
33
  - {name: yard, version: "~> 0.7.2", groups: [development]}
30
34
  - {name: bluecloth, version: "~> 2.0.9", groups: [development]}
31
35
  - {name: wlang, version: "~> 0.10.1", groups: [development]}
32
36
  - {name: noe, version: "~> 1.3.0", groups: [development]}
33
37
  - {name: quickl, version: "~> 0.2.2", groups: [runtime]}
34
- rake_tasks:
35
- spec_test:
36
- pattern: "spec/**/test_*.rb"
38
+ - {name: myrrha, version: "~> 1.0.0", groups: [runtime]}
data/bin/alf CHANGED
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
  module AlfLauncher
3
3
 
4
+ def self.lib
5
+ File.expand_path('../../lib', __FILE__)
6
+ end
7
+
4
8
  def self.load
5
9
  begin
6
- $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
10
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
7
11
  require "alf"
8
12
  rescue LoadError => ex
9
13
  require "rubygems"
@@ -11,20 +15,12 @@ module AlfLauncher
11
15
  end
12
16
  end
13
17
 
14
- def self.normalize(args)
15
- opts = []
16
- while !args.empty? && (args.first =~ /^\-/)
17
- opts << args.shift
18
- end
19
- if args.empty? or (args.size == 1 && File.exists?(args.first))
20
- opts << "exec"
21
- end
22
- opts += args
23
- end
24
-
25
18
  def self.start(argv)
26
19
  load
27
- Alf::Command::Main.run(normalize(argv), __FILE__)
20
+ if ENV["ALF_OPTS"]
21
+ argv = Alf::Tools::parse_commandline_args(ENV["ALF_OPTS"]) + argv
22
+ end
23
+ Alf::Command::Main.run(argv, __FILE__)
28
24
  end
29
25
 
30
26
  end # module AlfLaucher
File without changes
File without changes
File without changes
File without changes
@@ -1,4 +1,4 @@
1
- [{
1
+ Alf::Relation[{
2
2
  :suppliers => (dataset :suppliers),
3
3
  :parts => (dataset :parts),
4
4
  :cities => (dataset :cities),
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env alf
2
+ (matching :suppliers, :supplies)
File without changes
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env alf
2
+ (not_matching :suppliers, :supplies)
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env alf
2
+
3
+ # Get the three heaviest parts
4
+ (allbut (restrict (rank :parts, [[:weight, :desc]], :pos), lambda{ pos < 3 }), [:pos])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
data/lib/alf.rb CHANGED
@@ -1,8 +1,12 @@
1
+ require "alf/version"
2
+ require "alf/loader"
3
+
1
4
  require "enumerator"
2
5
  require "stringio"
3
6
  require "set"
4
- require "alf/version"
5
- require "alf/loader"
7
+
8
+ require 'myrrha/to_ruby_literal'
9
+ require 'myrrha/coerce'
6
10
 
7
11
  #
8
12
  # Classy data-manipulation dressed in a DSL (+ commandline)
@@ -15,6 +19,72 @@ module Alf
15
19
  module Tools
16
20
 
17
21
  #
22
+ # Parse a string with commandline arguments and returns an array.
23
+ #
24
+ # Example:
25
+ #
26
+ # parse_commandline_args("--text --size=10") # => ['--text', '--size=10']
27
+ #
28
+ def parse_commandline_args(args)
29
+ args = args.split(/\s+/)
30
+ result = []
31
+ until args.empty?
32
+ if args.first[0,1] == '"'
33
+ if args.first[-1,1] == '"'
34
+ result << args.shift[1...-1]
35
+ else
36
+ block = [ args.shift[1..-1] ]
37
+ while args.first[-1,1] != '"'
38
+ block << args.shift
39
+ end
40
+ block << args.shift[0...-1]
41
+ result << block.join(" ")
42
+ end
43
+ elsif args.first[0,1] == "'"
44
+ if args.first[-1,1] == "'"
45
+ result << args.shift[1...-1]
46
+ else
47
+ block = [ args.shift[1..-1] ]
48
+ while args.first[-1,1] != "'"
49
+ block << args.shift
50
+ end
51
+ block << args.shift[0...-1]
52
+ result << block.join(" ")
53
+ end
54
+ else
55
+ result << args.shift
56
+ end
57
+ end
58
+ result
59
+ end
60
+
61
+ # Helper to define methods with multiple signatures.
62
+ #
63
+ # Example:
64
+ #
65
+ # varargs([1, "hello"], [Integer, String]) # => [1, "hello"]
66
+ # varargs(["hello"], [Integer, String]) # => [nil, "hello"]
67
+ #
68
+ def varargs(args, types)
69
+ types.collect{|t| t===args.first ? args.shift : nil}
70
+ end
71
+
72
+ #
73
+ # Attempt to require(who) the most friendly way as possible.
74
+ #
75
+ def friendly_require(who, dep = nil, retried = false)
76
+ gem(who, dep) if dep && defined?(Gem)
77
+ require who
78
+ rescue LoadError => ex
79
+ if retried
80
+ raise "Unable to require #{who}, which is now needed\n"\
81
+ "Try 'gem install #{who}'"
82
+ else
83
+ require 'rubygems' unless defined?(Gem)
84
+ friendly_require(who, dep, true)
85
+ end
86
+ end
87
+
18
88
  # Returns the unqualified name of a ruby class or module
19
89
  #
20
90
  # Example
@@ -99,9 +169,8 @@ module Alf
99
169
  if expr.empty?
100
170
  compile(nil)
101
171
  else
102
- # TODO: replace inspect by to_ruby
103
- compile expr.each_pair.collect{|k,v|
104
- "(#{k} == #{v.inspect})"
172
+ compile expr.each_pair.collect{|k,v|
173
+ "(self.#{k} == #{Myrrha.to_ruby_literal(v)})"
105
174
  }.join(" && ")
106
175
  end
107
176
  when Array
@@ -207,13 +276,31 @@ module Alf
207
276
  @sorter = nil
208
277
  end
209
278
 
279
+ #
280
+ # Coerces `arg` to an ordering key.
281
+ #
282
+ # Implemented coercions are:
283
+ # * Array of symbols (all attributes in ascending order)
284
+ # * Array of [Symbol, :asc|:desc] pairs (obvious semantics)
285
+ # * ProjectionKey (all its attributes in ascending order)
286
+ # * OrderingKey (self)
287
+ #
288
+ # @return [OrderingKey]
289
+ # @raises [ArgumentError] when `arg` is not recognized
290
+ #
210
291
  def self.coerce(arg)
211
292
  case arg
212
293
  when Array
213
- if arg.all?{|a| a.is_a?(Symbol)}
214
- arg = arg.collect{|a| [a, :asc]}
294
+ if arg.all?{|a| a.is_a?(Array)}
295
+ OrderingKey.new(arg)
296
+ elsif arg.all?{|a| a.is_a?(Symbol)}
297
+ sliced = arg.each_slice(2)
298
+ if sliced.all?{|a,o| [:asc,:desc].include?(o)}
299
+ OrderingKey.new sliced.to_a
300
+ else
301
+ OrderingKey.new arg.collect{|a| [a, :asc]}
302
+ end
215
303
  end
216
- OrderingKey.new(arg)
217
304
  when ProjectionKey
218
305
  arg.to_ordering_key
219
306
  when OrderingKey
@@ -261,26 +348,6 @@ module Alf
261
348
  extend Tools
262
349
  end # module Tools
263
350
 
264
- #
265
- # Builds and returns a lispy engine on a specific environment.
266
- #
267
- # Example(s):
268
- #
269
- # # Returns a lispy instance on the default environment
270
- # lispy = Alf.lispy
271
- #
272
- # # Returns a lispy instance on the examples' environment
273
- # lispy = Alf.lispy(Alf::Environment.examples)
274
- #
275
- # # Returns a lispy instance on a folder environment of your choice
276
- # lispy = Alf.lispy(Alf::Environment.folder('path/to/a/folder'))
277
- #
278
- # @see Alf::Environment about available environments and their contract
279
- #
280
- def self.lispy(env = Alf::Environment.default)
281
- Command::Main.new(env)
282
- end
283
-
284
351
  #
285
352
  # Encapsulates the interface with the outside world, providing base iterators
286
353
  # for named datasets, among others.
@@ -301,9 +368,81 @@ module Alf
301
368
  # You can implement your own environment by subclassing this class and
302
369
  # implementing the {#dataset} method. As additional support is implemented
303
370
  # in the base class, Environment should never be mimiced.
371
+ #
372
+ # This class provides an extension point allowing to participate to auto
373
+ # detection and resolving of the --env=... option when alf is used in shell.
374
+ # See Environment.register, Environment.autodetect and Environment.recognizes?
375
+ # for details.
304
376
  #
305
377
  class Environment
306
378
 
379
+ # Registered environments
380
+ @@environments = []
381
+
382
+ #
383
+ # Register an environment class under a specific name.
384
+ #
385
+ # Registered class must implement a recognizes? method that takes an array
386
+ # of arguments; it must returns true if an environment instance can be built
387
+ # using those arguments, false otherwise. Please be very specific in the
388
+ # implementation for returning true. See also autodetect and recognizes?
389
+ #
390
+ # @param [Symbol] name name of the environment kind
391
+ # @param [Class] clazz class that implemented the environment
392
+ #
393
+ def self.register(name, clazz)
394
+ @@environments << [name, clazz]
395
+ (class << self; self; end).
396
+ send(:define_method, name) do |*args|
397
+ clazz.new(*args)
398
+ end
399
+ end
400
+
401
+ #
402
+ # Auto-detect the environment to use for specific arguments.
403
+ #
404
+ # This method returns an instance of the first registered Environment class
405
+ # that returns true to an invocation of recognizes?(args). It raises an
406
+ # ArgumentError if no such class can be found.
407
+ #
408
+ # @return [Environment] an environment instance
409
+ # @raise [ArgumentError] when no registered class recognizes the arguments
410
+ #
411
+ def self.autodetect(*args)
412
+ if (args.size == 1) && args.first.is_a?(Environment)
413
+ return args.first
414
+ else
415
+ @@environments.each do |name,clazz|
416
+ return clazz.new(*args) if clazz.recognizes?(args)
417
+ end
418
+ end
419
+ raise ArgumentError, "Unable to auto-detect Environment with #{args.inspect}"
420
+ end
421
+
422
+ #
423
+ # (see Environment.autodetect)
424
+ #
425
+ def self.coerce(*args)
426
+ autodetect(*args)
427
+ end
428
+
429
+ #
430
+ # Returns true _args_ can be used for building an environment instance,
431
+ # false otherwise.
432
+ #
433
+ # When returning true, an immediate invocation of new(*args) should
434
+ # succeed. While runtime exception are admitted (no such database, for
435
+ # example), argument errors should not occur (missing argument, wrong
436
+ # typing, etc.).
437
+ #
438
+ # Please be specific in the implementation of this extension point, as
439
+ # registered environments for a chain and each of them should have a
440
+ # chance of being selected.
441
+ #
442
+ def self.recognizes?(args)
443
+ false
444
+ end
445
+
307
446
  #
308
447
  # Returns a dataset whose name is provided.
309
448
  #
@@ -378,6 +517,18 @@ module Alf
378
517
  #
379
518
  class Folder < Environment
380
519
 
520
+ #
521
+ # (see Environment.recognizes?)
522
+ #
523
+ # Returns true if args contains onely a String which is an existing
524
+ # folder.
525
+ #
526
+ def self.recognizes?(args)
527
+ (args.size == 1) &&
528
+ args.first.is_a?(String) &&
529
+ File.directory?(args.first.to_s)
530
+ end
531
+
381
532
  #
382
533
  # Creates an environment instance, wired to the specified folder.
383
534
  #
@@ -412,15 +563,9 @@ module Alf
412
563
  end
413
564
  end
414
565
 
566
+ Environment.register(:folder, self)
415
567
  end # class Folder
416
568
 
417
- #
418
- # Factors a Folder environment on a specific path
419
- #
420
- def self.folder(path)
421
- Folder.new(path)
422
- end
423
-
424
569
  #
425
570
  # Returns the default environment
426
571
  #
@@ -432,7 +577,7 @@ module Alf
432
577
  # Returns the examples environment
433
578
  #
434
579
  def self.examples
435
- folder File.expand_path('../../examples', __FILE__)
580
+ folder File.expand_path('../../examples/operators', __FILE__)
436
581
  end
437
582
 
438
583
  end # class Environment
@@ -572,8 +717,10 @@ module Alf
572
717
  else
573
718
  raise "No registered reader for #{ext} (#{filepath})"
574
719
  end
575
- else
720
+ elsif args.empty?
576
721
  coerce(filepath)
722
+ else
723
+ raise ArgumentError, "Unable to return a reader for #{filepath} and #{args}"
577
724
  end
578
725
  end
579
726
 
@@ -603,21 +750,33 @@ module Alf
603
750
  end
604
751
  end
605
752
 
753
+ # Default reader options
754
+ DEFAULT_OPTIONS = {}
755
+
606
756
  # @return [Environment] Wired environment
607
757
  attr_accessor :environment
608
758
 
609
759
  # @return [String or IO] Input IO, or file name
610
760
  attr_accessor :input
761
+
762
+ # @return [Hash] Reader's options
763
+ attr_accessor :options
611
764
 
612
765
  #
613
- # Creates a reader instance, with an optional input and environment wiring.
766
+ # Creates a reader instance.
614
767
  #
615
768
  # @param [String or IO] path to a file or IO object for input
616
769
  # @param [Environment] environment wired environment, serving this reader
770
+ # @param [Hash] options Reader's options (see doc of subclasses)
617
771
  #
618
- def initialize(input = nil, environment = nil)
619
- @input = input
620
- @environment = environment
772
+ def initialize(*args)
773
+ @input, @environment, @options = case args.first
774
+ when String, IO, StringIO
775
+ Tools.varargs(args, [args.first.class, Environment, Hash])
776
+ else
777
+ Tools.varargs(args, [String, Environment, Hash])
778
+ end
779
+ @options = self.class.const_get(:DEFAULT_OPTIONS).merge(@options || {})
621
780
  end
622
781
 
623
782
  #
@@ -821,17 +980,33 @@ module Alf
821
980
  @@renderers.each(&Proc.new)
822
981
  end
823
982
 
983
+ # Default renderer options
984
+ DEFAULT_OPTIONS = {}
985
+
824
986
  # Renderer input (typically an Iterator)
825
987
  attr_accessor :input
826
988
 
827
989
  # @return [Environment] Optional wired environment
828
990
  attr_accessor :environment
829
991
 
992
+ # @return [Hash] Renderer's options
993
+ attr_accessor :options
994
+
830
995
  #
831
- # Creates a renderer instance, optionally wired to an input
996
+ # Creates a reader instance.
832
997
  #
833
- def initialize(input = nil)
834
- @input = input
998
+ # @param [Iterator] iterator an Iterator of tuples to render
999
+ # @param [Environment] environment wired environment, serving this reader
1000
+ # @param [Hash] options Reader's options (see doc of subclasses)
1001
+ #
1002
+ def initialize(*args)
1003
+ @input, @environment, @options = case args.first
1004
+ when Array
1005
+ Tools.varargs(args, [Array, Environment, Hash])
1006
+ else
1007
+ Tools.varargs(args, [Iterator, Environment, Hash])
1008
+ end
1009
+ @options = self.class.const_get(:DEFAULT_OPTIONS).merge(@options || {})
835
1010
  end
836
1011
 
837
1012
  #
@@ -877,7 +1052,7 @@ module Alf
877
1052
  # (see Renderer#render)
878
1053
  def render(input, output)
879
1054
  input.each do |tuple|
880
- output << tuple.inspect << "\n"
1055
+ output << Myrrha.to_ruby_literal(tuple) << "\n"
881
1056
  end
882
1057
  output
883
1058
  end
@@ -885,8 +1060,6 @@ module Alf
885
1060
  Renderer.register(:rash, "as ruby hashes", self)
886
1061
  end # class Rash
887
1062
 
888
- require "alf/renderer/text"
889
- require "alf/renderer/yaml"
890
1063
  end # module Renderer
891
1064
 
892
1065
  #
@@ -937,7 +1110,14 @@ module Alf
937
1110
  #
938
1111
  # RELATIONAL COMMANDS
939
1112
  # #{summarized_subcommands subcommands.select{|cmd|
940
- # cmd.include?(Alf::Operator::Relational)
1113
+ # cmd.include?(Alf::Operator::Relational) &&
1114
+ # !cmd.include?(Alf::Operator::Experimental)
1115
+ # }}
1116
+ #
1117
+ # EXPERIMENTAL OPERATORS
1118
+ # #{summarized_subcommands subcommands.select{|cmd|
1119
+ # cmd.include?(Alf::Operator::Relational) &&
1120
+ # cmd.include?(Alf::Operator::Experimental)
941
1121
  # }}
942
1122
  #
943
1123
  # NON-RELATIONAL COMMANDS
@@ -964,7 +1144,6 @@ module Alf
964
1144
  # Creates a command instance
965
1145
  def initialize(env = Environment.default)
966
1146
  @environment = env
967
- extend(Lispy)
968
1147
  end
969
1148
 
970
1149
  # Install options
@@ -974,16 +1153,20 @@ module Alf
974
1153
  @execute = true
975
1154
  end
976
1155
 
977
- @renderer = Renderer::Rash.new
1156
+ @renderer = nil
978
1157
  Renderer.each_renderer do |name,descr,clazz|
979
1158
  opt.on("--#{name}", "Render output #{descr}"){
980
1159
  @renderer = clazz.new
981
1160
  }
982
1161
  end
983
1162
 
984
- opt.on('--env=FOLDER',
985
- "Set the environment folder to use") do |value|
986
- @environment = Environment.folder(value)
1163
+ opt.on('--env=ENV',
1164
+ "Set the environment to use") do |value|
1165
+ @environment = Environment.autodetect(value)
1166
+ end
1167
+
1168
+ opt.on('-rlibrary', "require the library, before executing alf") do |value|
1169
+ require(value)
987
1170
  end
988
1171
 
989
1172
  opt.on_tail('-h', "--help", "Show help") do
@@ -991,16 +1174,29 @@ module Alf
991
1174
  end
992
1175
 
993
1176
  opt.on_tail('-v', "--version", "Show version") do
994
- raise Quickl::Exit, "#{program_name} #{Alf::VERSION}"\
1177
+ raise Quickl::Exit, "alf #{Alf::VERSION}"\
995
1178
  " (c) 2011, Bernard Lambeau"
996
1179
  end
997
1180
  end # Alf's options
998
1181
 
1182
+ #
1183
+ def _normalize(args)
1184
+ opts = []
1185
+ while !args.empty? && (args.first =~ /^\-/)
1186
+ opts << args.shift
1187
+ end
1188
+ if args.empty? or (args.size == 1 && File.exists?(args.first))
1189
+ opts << "exec"
1190
+ end
1191
+ opts += args
1192
+ end
1193
+
999
1194
  #
1000
1195
  # Overrided because Quickl only keep --options but modifying it there
1001
1196
  # should probably be considered a broken API.
1002
1197
  #
1003
1198
  def _run(argv = [])
1199
+ argv = _normalize(argv)
1004
1200
 
1005
1201
  # 1) Extract my options and parse them
1006
1202
  my_argv = []
@@ -1011,7 +1207,7 @@ module Alf
1011
1207
 
1012
1208
  # 2) build the operator according to -e option
1013
1209
  operator = if @execute
1014
- instance_eval(argv.first)
1210
+ Alf.lispy(environment).compile(argv.first)
1015
1211
  else
1016
1212
  super
1017
1213
  end
@@ -1019,6 +1215,7 @@ module Alf
1019
1215
  # 3) if there is a requester, then we do the job (assuming bin/alf)
1020
1216
  # with the renderer to use. Otherwise, we simply return built operator
1021
1217
  if operator && requester
1218
+ renderer = self.renderer ||= Renderer::Rash.new
1022
1219
  renderer.pipe(operator, environment).execute($stdout)
1023
1220
  else
1024
1221
  operator
@@ -1048,7 +1245,7 @@ module Alf
1048
1245
  include Command
1049
1246
 
1050
1247
  options do |opt|
1051
- @renderer = Renderer::Text.new
1248
+ @renderer = nil
1052
1249
  Renderer.each_renderer do |name,descr,clazz|
1053
1250
  opt.on("--#{name}", "Render output #{descr}"){
1054
1251
  @renderer = clazz.new
@@ -1057,7 +1254,7 @@ module Alf
1057
1254
  end
1058
1255
 
1059
1256
  def execute(args)
1060
- requester.renderer = @renderer
1257
+ requester.renderer = (@renderer || requester.renderer || Text::Renderer.new)
1061
1258
  args = [ $stdin ] if args.empty?
1062
1259
  args.first
1063
1260
  end
@@ -1456,6 +1653,9 @@ module Alf
1456
1653
 
1457
1654
  end # module Shortcut
1458
1655
 
1656
+ # Marker for experimental operators
1657
+ module Experimental; end
1658
+
1459
1659
  end # module Operator
1460
1660
 
1461
1661
  #
@@ -2089,40 +2289,80 @@ module Alf
2089
2289
  class Join < Factory::Operator(__FILE__, __LINE__)
2090
2290
  include Operator::Relational, Operator::Shortcut, Operator::Binary
2091
2291
 
2292
+ #
2293
+ # Performs a Join of two relations through a Hash buffer on the right
2294
+ # one.
2295
+ #
2092
2296
  class HashBased
2093
2297
  include Operator::Binary
2094
2298
 
2299
+ #
2300
+ # Implements a special Buffer for join-based relational operators.
2301
+ #
2302
+ # Example:
2303
+ #
2304
+ # buffer = Buffer::Join.new(...) # pass the right part of the join
2305
+ # left.each do |left_tuple|
2306
+ # key, rest = buffer.split(tuple)
2307
+ # buffer.each(key) do |right_tuple|
2308
+ # #
2309
+ # # do whatever you want with left and right tuples
2310
+ # #
2311
+ # end
2312
+ # end
2313
+ #
2095
2314
  class JoinBuffer
2096
2315
 
2316
+ #
2317
+ # Creates a buffer instance with the right part of the join.
2318
+ #
2319
+ # @param [Iterator] enum a tuple iterator, right part of the join.
2320
+ #
2097
2321
  def initialize(enum)
2098
2322
  @buffer = nil
2099
2323
  @key = nil
2100
2324
  @enum = enum
2101
2325
  end
2102
2326
 
2327
+ #
2328
+ # Splits a left tuple according to the common key.
2329
+ #
2330
+ # @param [Hash] tuple a left tuple of the join
2331
+ # @return [Array] an array of two elements, the key and the rest
2332
+ # @see ProjectionKey#split
2333
+ #
2103
2334
  def split(tuple)
2104
2335
  _init(tuple) unless @key
2105
2336
  @key.split(tuple)
2106
2337
  end
2107
2338
 
2339
+ #
2340
+ # Yields each right tuple that matches a given key value.
2341
+ #
2342
+ # @param [Hash] key a tuple that matches elements of the common key
2343
+ # (typically the first element returned by #split)
2344
+ #
2108
2345
  def each(key)
2109
2346
  @buffer[key].each(&Proc.new) if @buffer.has_key?(key)
2110
2347
  end
2111
2348
 
2112
2349
  private
2113
2350
 
2351
+ # Initialize the buffer with a right tuple
2114
2352
  def _init(right)
2115
2353
  @buffer = Hash.new{|h,k| h[k] = []}
2116
2354
  @enum.each do |left|
2117
2355
  @key = Tools::ProjectionKey.coerce(left.keys & right.keys) unless @key
2118
2356
  @buffer[@key.project(left)] << left
2119
2357
  end
2358
+ @key = Tools::ProjectionKey.coerce([]) unless @key
2120
2359
  end
2121
2360
 
2122
- end
2361
+ end # class JoinBuffer
2123
2362
 
2124
2363
  protected
2125
2364
 
2365
+ # (see Operator#_each)
2126
2366
  def _each
2127
2367
  buffer = JoinBuffer.new(right)
2128
2368
  left.each do |left_tuple|
@@ -2291,6 +2531,128 @@ module Alf
2291
2531
  end
2292
2532
 
2293
2533
  end # class Union
2534
+
2535
+ #
2536
+ # Relational matching
2537
+ #
2538
+ # SYNOPSIS
2539
+ # #{program_name} #{command_name} [LEFT] RIGHT
2540
+ #
2541
+ # API & EXAMPLE
2542
+ #
2543
+ # (matching :suppliers, :supplies)
2544
+ #
2545
+ # DESCRIPTION
2546
+ #
2547
+ # This operator restricts left tuples to those for which there exists at
2548
+ # least one right tuple that joins. This is a shortcut operator for the
2549
+ # longer expression:
2550
+ #
2551
+ # (project (join xxx, yyy), [xxx's attributes])
2552
+ #
2553
+ # In shell:
2554
+ #
2555
+ # alf matching suppliers supplies
2556
+ #
2557
+ class Matching < Factory::Operator(__FILE__, __LINE__)
2558
+ include Operator::Relational, Operator::Shortcut, Operator::Binary
2559
+
2560
+ #
2561
+ # Performs a Matching of two relations through a Hash buffer on the right
2562
+ # one.
2563
+ #
2564
+ class HashBased
2565
+ include Operator::Binary
2566
+
2567
+ # (see Operator#_each)
2568
+ def _each
2569
+ seen, key = nil, nil
2570
+ left.each do |left_tuple|
2571
+ seen ||= begin
2572
+ h = Hash.new
2573
+ right.each do |right_tuple|
2574
+ key ||= Tools::ProjectionKey.coerce(left_tuple.keys & right_tuple.keys)
2575
+ h[key.project(right_tuple)] = true
2576
+ end
2577
+ key ||= Tools::ProjectionKey.coerce([])
2578
+ h
2579
+ end
2580
+ yield(left_tuple) if seen.has_key?(key.project(left_tuple))
2581
+ end
2582
+ end
2583
+
2584
+ end # class HashBased
2585
+
2586
+ protected
2587
+
2588
+ # (see Shortcut#longexpr)
2589
+ def longexpr
2590
+ chain HashBased.new,
2591
+ datasets
2592
+ end
2593
+
2594
+ end # class Matching
2595
+
2596
+ #
2597
+ # Relational not matching
2598
+ #
2599
+ # SYNOPSIS
2600
+ # #{program_name} #{command_name} [LEFT] RIGHT
2601
+ #
2602
+ # API & EXAMPLE
2603
+ #
2604
+ # (not_matching :suppliers, :supplies)
2605
+ #
2606
+ # DESCRIPTION
2607
+ #
2608
+ # This operator restricts left tuples to those for which there does not
2609
+ # exist any right tuple that joins. This is a shortcut operator for the
2610
+ # longer expression:
2611
+ #
2612
+ # (minus xxx, (matching xxx, yyy))
2613
+ #
2614
+ # In shell:
2615
+ #
2616
+ # alf not-matching suppliers supplies
2617
+ #
2618
+ class NotMatching < Factory::Operator(__FILE__, __LINE__)
2619
+ include Operator::Relational, Operator::Shortcut, Operator::Binary
2620
+
2621
+ #
2622
+ # Performs a NotMatching of two relations through a Hash buffer on the
2623
+ # right one.
2624
+ #
2625
+ class HashBased
2626
+ include Operator::Binary
2627
+
2628
+ # (see Operator#_each)
2629
+ def _each
2630
+ seen, key = nil, nil
2631
+ left.each do |left_tuple|
2632
+ seen ||= begin
2633
+ h = Hash.new
2634
+ right.each do |right_tuple|
2635
+ key ||= Tools::ProjectionKey.coerce(left_tuple.keys & right_tuple.keys)
2636
+ h[key.project(right_tuple)] = true
2637
+ end
2638
+ key ||= Tools::ProjectionKey.coerce([])
2639
+ h
2640
+ end
2641
+ yield(left_tuple) unless seen.has_key?(key.project(left_tuple))
2642
+ end
2643
+ end
2644
+
2645
+ end # class HashBased
2646
+
2647
+ protected
2648
+
2649
+ # (see Shortcut#longexpr)
2650
+ def longexpr
2651
+ chain HashBased.new,
2652
+ datasets
2653
+ end
2654
+
2655
+ end # class NotMatching
2294
2656
 
2295
2657
  #
2296
2658
  # Relational wraping (tuple-valued attributes)
@@ -2670,6 +3032,117 @@ module Alf
2670
3032
 
2671
3033
  end # class Summarize
2672
3034
 
3035
+ #
3036
+ # Relational ranking (explicit tuple positions)
3037
+ #
3038
+ # SYNOPSIS
3039
+ # #{program_name} #{command_name} [OPERAND] --order=OR1... -- [RANKNAME]
3040
+ #
3041
+ # OPTIONS
3042
+ # #{summarized_options}
3043
+ #
3044
+ # API & EXAMPLE
3045
+ #
3046
+ # # Position attribute => # of tuples with smaller weight
3047
+ # (rank :parts, [:weight], :position)
3048
+ #
3049
+ # # Position attribute => # of tuples with greater weight
3050
+ # (rank :parts, [[:weight, :desc]], :position)
3051
+ #
3052
+ # DESCRIPTION
3053
+ #
3054
+ # This operator computes the ranking of input tuples, according to an order
3055
+ # relation. Precisely, it extends the input tuples with a RANKNAME attribute
3056
+ # whose value is the number of tuples which are considered strictly less
3057
+ # according to the specified order. For the two examples above:
3058
+ #
3059
+ # alf rank parts --order=weight -- position
3060
+ # alf rank parts --order=weight,desc -- position
3061
+ #
3062
+ # Note that, unless the ordering key includes a candidate key for the input
3063
+ # relation, the newly RANKNAME attribute is not necessarily a candidate key
3064
+ # for the output one. In the example above, adding the :pid attribute
3065
+ # ensured that position will contain all different values:
3066
+ #
3067
+ # alf rank parts --order=weight,pid -- position
3068
+ #
3069
+ # Or even:
3070
+ #
3071
+ # alf rank parts --order=weight,desc,pid,asc -- position
3072
+ #
3073
+ class Rank < Factory::Operator(__FILE__, __LINE__)
3074
+ include Operator::Relational, Operator::Shortcut, Operator::Unary
3075
+
3076
+ # Ranking order
3077
+ attr_accessor :order
3078
+
3079
+ # Ranking attribute name
3080
+ attr_accessor :ranking_name
3081
+
3082
+ def initialize(order = [], ranking_name = :rank)
3083
+ @order, @ranking_name = order, ranking_name
3084
+ end
3085
+
3086
+ options do |opt|
3087
+ opt.on('--order=x,y,z', 'Specify ranking order', Array) do |args|
3088
+ @order = args.collect{|a| a.to_sym}
3089
+ end
3090
+ end
3091
+
3092
+ class SortBased
3093
+ include Operator::Cesure
3094
+
3095
+ def initialize(order, ranking_name)
3096
+ @order, @ranking_name = order, ranking_name
3097
+ end
3098
+
3099
+ def ordering_key
3100
+ OrderingKey.coerce @order
3101
+ end
3102
+
3103
+ def cesure_key
3104
+ ProjectionKey.coerce(ordering_key)
3105
+ end
3106
+
3107
+ def start_cesure(key, receiver)
3108
+ @rank ||= 0
3109
+ @last_block = 0
3110
+ end
3111
+
3112
+ def accumulate_cesure(tuple, receiver)
3113
+ receiver.call tuple.merge(@ranking_name => @rank)
3114
+ @last_block += 1
3115
+ end
3116
+
3117
+ def flush_cesure(key, receiver)
3118
+ @rank += @last_block
3119
+ end
3120
+
3121
+ end # class SortBased
3122
+
3123
+ protected
3124
+
3125
+ # (see Operator::CommandMethods#set_args)
3126
+ def set_args(args)
3127
+ unless args.empty?
3128
+ self.ranking_name = args.first.to_sym
3129
+ end
3130
+ self
3131
+ end
3132
+
3133
+ def ordering_key
3134
+ OrderingKey.coerce @order
3135
+ end
3136
+
3137
+ def longexpr
3138
+ sort_key = ordering_key
3139
+ chain SortBased.new(sort_key, @ranking_name),
3140
+ Operator::NonRelational::Sort.new(sort_key),
3141
+ datasets
3142
+ end
3143
+
3144
+ end # class Rank
3145
+
2673
3146
  #
2674
3147
  # Relational quota-queries (position, sum progression, etc.)
2675
3148
  #
@@ -2692,7 +3165,8 @@ module Alf
2692
3165
  # alf quota supplies --by=sid --order=qty -- position count sum_qty "sum(:qty)"
2693
3166
  #
2694
3167
  class Quota < Factory::Operator(__FILE__, __LINE__)
2695
- include Operator::Relational, Operator::Shortcut, Operator::Unary
3168
+ include Operator::Relational, Operator::Experimental,
3169
+ Operator::Shortcut, Operator::Unary
2696
3170
 
2697
3171
  # Quota by
2698
3172
  attr_accessor :by
@@ -2991,24 +3465,42 @@ module Alf
2991
3465
  #
2992
3466
  # Keeps tuples ordered on a specific key
2993
3467
  #
3468
+ # Example:
3469
+ #
3470
+ # sorted = Buffer::Sorted.new OrderingKey.new(...)
3471
+ # sorted.add_all(...)
3472
+ # sorted.each do |tuple|
3473
+ # # tuples are ordered here
3474
+ # end
3475
+ #
2994
3476
  class Sorted < Buffer
2995
3477
 
3478
+ #
3479
+ # Creates a buffer instance with an ordering key
3480
+ #
2996
3481
  def initialize(ordering_key)
2997
3482
  @ordering_key = ordering_key
2998
3483
  @buffer = []
2999
3484
  end
3000
3485
 
3486
+ #
3487
+ # Adds all elements of an iterator to the buffer
3488
+ #
3001
3489
  def add_all(enum)
3002
3490
  sorter = @ordering_key.sorter
3003
3491
  @buffer = merge_sort(@buffer, enum.to_a.sort(&sorter), sorter)
3004
3492
  end
3005
3493
 
3494
+ #
3495
+ # (see Buffer#each)
3496
+ #
3006
3497
  def each
3007
3498
  @buffer.each(&Proc.new)
3008
3499
  end
3009
3500
 
3010
3501
  private
3011
3502
 
3503
+ # Implements a merge sort between two iterators s1 and s2
3012
3504
  def merge_sort(s1, s2, sorter)
3013
3505
  (s1 + s2).sort(&sorter)
3014
3506
  end
@@ -3017,7 +3509,245 @@ module Alf
3017
3509
 
3018
3510
  end # class Buffer
3019
3511
 
3020
- #
3512
+ #
3513
+ # Defines a Heading, that is, a set of attribute (name,domain) pairs.
3514
+ #
3515
+ class Heading
3516
+
3517
+ #
3518
+ # Creates a Heading instance
3519
+ #
3520
+ # @param [Hash] a hash of attribute (name, type) pairs where name is
3521
+ # a Symbol and type is a Class
3522
+ #
3523
+ def self.[](attributes)
3524
+ Heading.new(attributes)
3525
+ end
3526
+
3527
+ # @return [Hash] a (freezed) hash of (name, type) pairs
3528
+ attr_reader :attributes
3529
+
3530
+ #
3531
+ # Creates a Heading instance
3532
+ #
3533
+ # @param [Hash] a hash of attribute (name, type) pairs where name is
3534
+ # a Symbol and type is a Class
3535
+ #
3536
+ def initialize(attributes)
3537
+ @attributes = attributes.dup.freeze
3538
+ end
3539
+
3540
+ #
3541
+ # Returns heading's cardinality
3542
+ #
3543
+ def cardinality
3544
+ attributes.size
3545
+ end
3546
+ alias :size :cardinality
3547
+ alias :count :cardinality
3548
+
3549
+ #
3550
+ # Returns heading's hash code
3551
+ #
3552
+ def hash
3553
+ @hash ||= attributes.hash
3554
+ end
3555
+
3556
+ #
3557
+ # Checks equality with other heading
3558
+ #
3559
+ def ==(other)
3560
+ other.is_a?(Heading) && (other.attributes == attributes)
3561
+ end
3562
+ alias :eql? :==
3563
+
3564
+ #
3565
+ # Converts this heading to a Hash of (name,type) pairs
3566
+ #
3567
+ def to_hash
3568
+ attributes.dup
3569
+ end
3570
+
3571
+ #
3572
+ # Returns a Heading literal
3573
+ #
3574
+ def to_ruby_literal
3575
+ attributes.empty? ?
3576
+ "Alf::Heading::EMPTY" :
3577
+ "Alf::Heading[#{Myrrha.to_ruby_literal(attributes)[1...-1]}]"
3578
+ end
3579
+ alias :inspect :to_ruby_literal
3580
+
3581
+ EMPTY = Alf::Heading.new({})
3582
+ end # class Heading
3583
+
3584
+ #
3585
+ # Defines an in-memory relation data structure.
3586
+ #
3587
+ # A relation is a set of tuples; a tuple is a set of attribute (name, value)
3588
+ # pairs. The class implements such a data structure with full relational
3589
+ # algebra installed as instance methods.
3590
+ #
3591
+ # Relation values can be obtained in various ways, for example by invoking
3592
+ # a relational operator on an existing relation. Relation literals are simply
3593
+ # constructed as follows:
3594
+ #
3595
+ # Alf::Relation[
3596
+ # # ... a comma list of ruby hashes ...
3597
+ # ]
3598
+ #
3599
+ # See main Alf documentation about relational operators.
3600
+ #
3601
+ class Relation
3602
+ include Iterator
3603
+
3604
+ protected
3605
+
3606
+ # @return [Set] the set of tuples
3607
+ attr_reader :tuples
3608
+
3609
+ public
3610
+
3611
+ #
3612
+ # Creates a Relation instance.
3613
+ #
3614
+ # @param [Set] tuples a set of tuples
3615
+ #
3616
+ def initialize(tuples)
3617
+ raise ArgumentError unless tuples.is_a?(Set)
3618
+ @tuples = tuples
3619
+ end
3620
+
3621
+ #
3622
+ # Coerces `val` to a relation.
3623
+ #
3624
+ # Recognized arguments are: Relation (identity coercion), Set of ruby hashes,
3625
+ # Array of ruby hashes, Alf::Iterator.
3626
+ #
3627
+ # @return [Relation] a relation instance for the given set of tuples
3628
+ # @raise [ArgumentError] when `val` is not recognized
3629
+ #
3630
+ def self.coerce(val)
3631
+ case val
3632
+ when Relation
3633
+ val
3634
+ when Set
3635
+ Relation.new(val)
3636
+ when Array
3637
+ Relation.new val.to_set
3638
+ when Iterator
3639
+ Relation.new val.to_set
3640
+ else
3641
+ raise ArgumentError, "Unable to coerce #{val} to a Relation"
3642
+ end
3643
+ end
3644
+
3645
+ # (see Relation.coerce)
3646
+ def self.[](*tuples)
3647
+ coerce(tuples)
3648
+ end
3649
+
3650
+ #
3651
+ # (see Iterator#each)
3652
+ #
3653
+ def each(&block)
3654
+ tuples.each(&block)
3655
+ end
3656
+
3657
+ #
3658
+ # Returns relation's cardinality (number of tuples).
3659
+ #
3660
+ # @return [Integer] relation's cardinality
3661
+ #
3662
+ def cardinality
3663
+ tuples.size
3664
+ end
3665
+ alias :size :cardinality
3666
+ alias :count :cardinality
3667
+
3668
+ # Returns true if this relation is empty
3669
+ def empty?
3670
+ cardinality == 0
3671
+ end
3672
+
3673
+ #
3674
+ # Install the DSL through iteration over defined operators
3675
+ #
3676
+ Operator::each do |op_class|
3677
+ meth_name = Tools.ruby_case(Tools.class_name(op_class)).to_sym
3678
+ if op_class.unary?
3679
+ define_method(meth_name) do |*args|
3680
+ op = op_class.new(*args).pipe(self)
3681
+ Relation.coerce(op)
3682
+ end
3683
+ elsif op_class.binary?
3684
+ define_method(meth_name) do |right, *args|
3685
+ op = op_class.new(*args).pipe([self, Iterator.coerce(right)])
3686
+ Relation.coerce(op)
3687
+ end
3688
+ else
3689
+ raise "Unexpected operator #{op_class}"
3690
+ end
3691
+ end # Operators::each
3692
+
3693
+ alias :+ :union
3694
+ alias :- :minus
3695
+
3696
+ # Shortcut for project(attributes, true)
3697
+ def allbut(attributes)
3698
+ project(attributes, true)
3699
+ end
3700
+
3701
+ #
3702
+ # (see Object#hash)
3703
+ #
3704
+ def hash
3705
+ @tuples.hash
3706
+ end
3707
+
3708
+ #
3709
+ # (see Object#==)
3710
+ #
3711
+ def ==(other)
3712
+ return nil unless other.is_a?(Relation)
3713
+ other.tuples == self.tuples
3714
+ end
3715
+ alias :eql? :==
3716
+
3717
+ #
3718
+ # Returns a textual representation of this relation
3719
+ #
3720
+ def to_s
3721
+ Alf::Renderer.text(self).execute("")
3722
+ end
3723
+
3724
+ #
3725
+ # Returns an array with all tuples in this relation.
3726
+ #
3727
+ # @param [Tools::OrderingKey] an optional ordering key (any argument
3728
+ # recognized by OrderingKey.coerce is supported here).
3729
+ # @return [Array] an array of hashes, in requested order (if specified)
3730
+ #
3731
+ def to_a(okey = nil)
3732
+ okey = Tools::OrderingKey.coerce(okey) if okey
3733
+ ary = tuples.to_a
3734
+ ary.sort!(&okey.sorter) if okey
3735
+ ary
3736
+ end
3737
+
3738
+ #
3739
+ # Returns a literal representation of this relation
3740
+ #
3741
+ def to_ruby_literal
3742
+ "Alf::Relation[" +
3743
+ tuples.collect{|t| Myrrha.to_ruby_literal(t)}.join(', ') + "]"
3744
+ end
3745
+ alias :inspect :to_ruby_literal
3746
+
3747
+ DEE = Relation.coerce([{}])
3748
+ DUM = Relation.coerce([])
3749
+ end # class Relation
3750
+
3021
3751
  # Implements a small LISP-like DSL on top of Alf.
3022
3752
  #
3023
3753
  # The lispy dialect is the functional one used in .alf files and in compiled
@@ -3061,7 +3791,8 @@ module Alf
3061
3791
  if expr.nil?
3062
3792
  instance_eval(&block)
3063
3793
  else
3064
- (path ? Kernel.eval(expr, binding, path) : Kernel.eval(expr, binding))
3794
+ b = _clean_binding
3795
+ (path ? Kernel.eval(expr, b, path) : Kernel.eval(expr, b))
3065
3796
  end
3066
3797
  end
3067
3798
 
@@ -3128,8 +3859,52 @@ module Alf
3128
3859
  (project child, attributes, true)
3129
3860
  end
3130
3861
 
3862
+ #
3863
+ # Runs a command as in shell.
3864
+ #
3865
+ # Example:
3866
+ #
3867
+ # lispy = Alf.lispy(Alf::Environment.examples)
3868
+ # op = lispy.run(['restrict', 'suppliers', '--', "city == 'Paris'"])
3869
+ #
3870
+ def run(argv, requester = nil)
3871
+ Alf::Command::Main.new(environment).run(argv, requester)
3872
+ end
3873
+
3131
3874
  Agg = Alf::Aggregator
3875
+ DUM = Relation::DUM
3876
+ DEE = Relation::DEE
3877
+
3878
+ private
3879
+
3880
+ def _clean_binding
3881
+ binding
3882
+ end
3883
+
3132
3884
  end # module Lispy
3133
3885
 
3886
+ #
3887
+ # Builds and returns a lispy engine on a specific environment.
3888
+ #
3889
+ # Example(s):
3890
+ #
3891
+ # # Returns a lispy instance on the default environment
3892
+ # lispy = Alf.lispy
3893
+ #
3894
+ # # Returns a lispy instance on the examples' environment
3895
+ # lispy = Alf.lispy(Alf::Environment.examples)
3896
+ #
3897
+ # # Returns a lispy instance on a folder environment of your choice
3898
+ # lispy = Alf.lispy(Alf::Environment.folder('path/to/a/folder'))
3899
+ #
3900
+ # @see Alf::Environment about available environments and their contract
3901
+ #
3902
+ def self.lispy(env = Environment.default)
3903
+ lispy = Object.new.extend(Lispy)
3904
+ lispy.environment = Environment.coerce(env)
3905
+ lispy
3906
+ end
3907
+
3134
3908
  end # module Alf
3135
- require "alf/relation"
3909
+ require "alf/text"
3910
+ require "alf/yaml"