alf 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +89 -0
- data/Gemfile.lock +6 -1
- data/README.md +35 -21
- data/TODO.md +0 -5
- data/alf.gemspec +2 -0
- data/alf.noespec +6 -4
- data/bin/alf +9 -13
- data/examples/{autonum.alf → operators/autonum.alf} +0 -0
- data/examples/{cities.rash → operators/cities.rash} +0 -0
- data/examples/{clip.alf → operators/clip.alf} +0 -0
- data/examples/{compact.alf → operators/compact.alf} +0 -0
- data/examples/{database.alf → operators/database.alf} +1 -1
- data/examples/{defaults.alf → operators/defaults.alf} +0 -0
- data/examples/{extend.alf → operators/extend.alf} +0 -0
- data/examples/{group.alf → operators/group.alf} +0 -0
- data/examples/{intersect.alf → operators/intersect.alf} +0 -0
- data/examples/{join.alf → operators/join.alf} +0 -0
- data/examples/operators/matching.alf +2 -0
- data/examples/{minus.alf → operators/minus.alf} +0 -0
- data/examples/operators/not_matching.alf +2 -0
- data/examples/{nulls.rash → operators/nulls.rash} +0 -0
- data/examples/{parts.rash → operators/parts.rash} +0 -0
- data/examples/{project.alf → operators/project.alf} +0 -0
- data/examples/{pseudo-with.alf → operators/pseudo-with.alf} +0 -0
- data/examples/{quota.alf → operators/quota.alf} +0 -0
- data/examples/operators/rank.alf +4 -0
- data/examples/{rename.alf → operators/rename.alf} +0 -0
- data/examples/{restrict.alf → operators/restrict.alf} +0 -0
- data/examples/{schema.yaml → operators/schema.yaml} +0 -0
- data/examples/{sort.alf → operators/sort.alf} +0 -0
- data/examples/{summarize.alf → operators/summarize.alf} +0 -0
- data/examples/{suppliers.rash → operators/suppliers.rash} +0 -0
- data/examples/{supplies.rash → operators/supplies.rash} +0 -0
- data/examples/{ungroup.alf → operators/ungroup.alf} +0 -0
- data/examples/{union.alf → operators/union.alf} +0 -0
- data/examples/{unwrap.alf → operators/unwrap.alf} +0 -0
- data/examples/{wrap.alf → operators/wrap.alf} +0 -0
- data/lib/alf.rb +837 -62
- data/lib/alf/loader.rb +2 -1
- data/lib/alf/text.rb +160 -0
- data/lib/alf/version.rb +1 -1
- data/lib/alf/yaml.rb +24 -0
- data/spec/integration/__database__/group.alf +3 -0
- data/spec/integration/__database__/parts.rash +6 -0
- data/spec/integration/__database__/suppliers.rash +5 -0
- data/spec/integration/__database__/supplies.rash +12 -0
- data/spec/integration/command/alf/alf_e.cmd +1 -0
- data/spec/integration/command/alf/alf_e.stdout +4 -0
- data/spec/integration/command/alf/alf_env.cmd +1 -0
- data/spec/integration/command/alf/alf_env.stdout +5 -0
- data/spec/integration/command/alf/alf_implicit.alf +1 -0
- data/spec/integration/command/alf/alf_implicit_exec.cmd +1 -0
- data/spec/integration/command/alf/alf_implicit_exec.stdout +4 -0
- data/spec/integration/command/alf/alf_r.cmd +1 -0
- data/spec/integration/command/alf/alf_r.stdout +5 -0
- data/spec/integration/command/alf/alf_version.cmd +1 -0
- data/spec/integration/command/alf/alf_version.stdout +2 -0
- data/spec/integration/command/alf/alf_yaml.cmd +1 -0
- data/spec/integration/command/alf/alf_yaml.stdout +22 -0
- data/spec/integration/command/alf/rel.rash +1 -0
- data/spec/integration/command/autonum/autonum_0.cmd +1 -0
- data/spec/integration/command/autonum/autonum_0.stdout +9 -0
- data/spec/integration/command/autonum/autonum_1.cmd +1 -0
- data/spec/integration/command/autonum/autonum_1.stdout +9 -0
- data/spec/integration/command/clip/clip_0.cmd +1 -0
- data/spec/integration/command/clip/clip_0.stdout +9 -0
- data/spec/integration/command/clip/clip_1.cmd +1 -0
- data/spec/integration/command/clip/clip_1.stdout +9 -0
- data/spec/integration/command/compact/compact_0.cmd +1 -0
- data/spec/integration/command/compact/compact_0.stdout +9 -0
- data/spec/integration/command/defaults/defaults_0.cmd +1 -0
- data/spec/integration/command/defaults/defaults_0.stdout +9 -0
- data/spec/integration/command/defaults/defaults_1.cmd +1 -0
- data/spec/integration/command/defaults/defaults_1.stdout +9 -0
- data/spec/integration/command/extend/extend_0.cmd +1 -0
- data/spec/integration/command/extend/extend_0.stdout +16 -0
- data/spec/integration/command/group/group_0.cmd +1 -0
- data/spec/integration/command/group/group_0.stdout +32 -0
- data/spec/integration/command/group/group_1.cmd +1 -0
- data/spec/integration/command/group/group_1.stdout +32 -0
- data/spec/integration/command/intersect/intersect_0.cmd +1 -0
- data/spec/integration/command/intersect/intersect_0.stdout +9 -0
- data/spec/integration/command/join/join_0.cmd +1 -0
- data/spec/integration/command/join/join_0.stdout +16 -0
- data/spec/integration/command/matching/matching_0.cmd +1 -0
- data/spec/integration/command/matching/matching_0.stdout +8 -0
- data/spec/integration/command/minus/minus_0.cmd +1 -0
- data/spec/integration/command/minus/minus_0.stdout +4 -0
- data/spec/integration/command/not-matching/not-matching_0.cmd +1 -0
- data/spec/integration/command/not-matching/not-matching_0.stdout +5 -0
- data/spec/integration/command/project/project_0.cmd +1 -0
- data/spec/integration/command/project/project_0.stdout +9 -0
- data/spec/integration/command/project/project_1.cmd +1 -0
- data/spec/integration/command/project/project_1.stdout +9 -0
- data/spec/integration/command/quota/quota_0.cmd +1 -0
- data/spec/integration/command/quota/quota_0.stdout +16 -0
- data/spec/integration/command/rank/rank_1.cmd +1 -0
- data/spec/integration/command/rank/rank_1.stdout +10 -0
- data/spec/integration/command/rank/rank_2.cmd +1 -0
- data/spec/integration/command/rank/rank_2.stdout +10 -0
- data/spec/integration/command/rank/rank_3.cmd +1 -0
- data/spec/integration/command/rank/rank_3.stdout +10 -0
- data/spec/integration/command/rank/rank_4.cmd +1 -0
- data/spec/integration/command/rank/rank_4.stdout +6 -0
- data/spec/integration/command/rank/rank_5.cmd +1 -0
- data/spec/integration/command/rank/rank_5.stdout +6 -0
- data/spec/integration/command/rename/rename_0.cmd +1 -0
- data/spec/integration/command/rename/rename_0.stdout +9 -0
- data/spec/integration/command/restrict/restrict_0.cmd +1 -0
- data/spec/integration/command/restrict/restrict_0.stdout +6 -0
- data/spec/integration/command/restrict/restrict_1.cmd +1 -0
- data/spec/integration/command/restrict/restrict_1.stdout +6 -0
- data/spec/integration/command/show/show_base.cmd +1 -0
- data/spec/integration/command/show/show_base.stdout +9 -0
- data/spec/integration/command/show/show_conflictual.cmd +1 -0
- data/spec/integration/command/show/show_conflictual.stdout +5 -0
- data/spec/integration/command/show/show_rash.cmd +1 -0
- data/spec/integration/command/show/show_rash.stdout +5 -0
- data/spec/integration/command/show/show_rash_2.cmd +1 -0
- data/spec/integration/command/show/show_rash_2.stdout +5 -0
- data/spec/integration/command/show/show_yaml.cmd +1 -0
- data/spec/integration/command/show/show_yaml.stdout +22 -0
- data/spec/integration/command/sort/sort_0.cmd +1 -0
- data/spec/integration/command/sort/sort_0.stdout +9 -0
- data/spec/integration/command/sort/sort_1.cmd +1 -0
- data/spec/integration/command/sort/sort_1.stdout +9 -0
- data/spec/integration/command/summarize/summarize_0.cmd +1 -0
- data/spec/integration/command/summarize/summarize_0.stdout +8 -0
- data/spec/integration/command/ungroup/ungroup_0.cmd +1 -0
- data/spec/integration/command/ungroup/ungroup_0.stdout +16 -0
- data/spec/integration/command/union/union_0.cmd +1 -0
- data/spec/integration/command/union/union_0.stdout +9 -0
- data/spec/integration/command/unwrap/unwrap_0.cmd +1 -0
- data/spec/integration/command/unwrap/unwrap_0.stdout +9 -0
- data/spec/integration/command/wrap/wrap_0.cmd +1 -0
- data/spec/integration/command/wrap/wrap_0.stdout +9 -0
- data/spec/integration/semantics/test_join.alf +9 -0
- data/spec/integration/{src → semantics}/test_minus.alf +0 -0
- data/spec/integration/{src → semantics}/test_project.alf +0 -0
- data/spec/integration/semantics/test_rank.alf +34 -0
- data/spec/integration/test_command.rb +36 -0
- data/spec/integration/test_examples.rb +11 -22
- data/spec/integration/test_semantics.rb +40 -0
- data/spec/regression/alf_file/__FILE__.alf +2 -0
- data/spec/regression/alf_file/suppliers.rash +5 -0
- data/spec/regression/alf_file/test___FILE__.rb +17 -0
- data/spec/regression/heading/test_heading_with_date.rb +12 -0
- data/spec/regression/lispy/test_compile.rb +14 -0
- data/spec/regression/relation/test_relation_with_date.rb +12 -0
- data/spec/regression/restrict/test_restrict_with_keywords.rb +17 -0
- data/spec/shared/a_value.rb +12 -0
- data/spec/shared/an_operator_class.rb +35 -0
- data/spec/spec_helper.rb +12 -34
- data/spec/unit/assumptions/test_file.rb +17 -0
- data/spec/unit/{test_assumptions.rb → assumptions/test_instance_eval.rb} +1 -1
- data/spec/unit/assumptions/test_scoping.rb +29 -0
- data/spec/unit/environment/test_folder.rb +6 -1
- data/spec/unit/operator/relational/matching/test_hash_based.rb +60 -0
- data/spec/unit/operator/relational/not_matching/test_hash_based.rb +37 -0
- data/spec/unit/operator/relational/test_rank.rb +50 -0
- data/spec/unit/operator/test_relational.rb +3 -0
- data/spec/unit/reader/test_alf_file.rb +7 -4
- data/spec/unit/reader/test_initialize.rb +60 -0
- data/spec/unit/relation/test_relops.rb +4 -0
- data/spec/unit/relation/test_to_a.rb +41 -0
- data/spec/unit/renderer/test_initialize.rb +60 -0
- data/spec/unit/test_environment.rb +38 -0
- data/spec/unit/test_heading.rb +38 -0
- data/spec/unit/test_reader.rb +5 -0
- data/spec/unit/test_relation.rb +31 -1
- data/spec/unit/test_renderer.rb +1 -1
- data/spec/unit/{renderer/text → text}/test_cell.rb +1 -1
- data/spec/unit/{renderer/text → text}/test_row.rb +1 -1
- data/spec/unit/{renderer/text → text}/test_table.rb +1 -1
- data/spec/unit/tools/test_ordering_key.rb +13 -0
- data/spec/unit/tools/test_parse_commandline_args.rb +47 -0
- data/spec/unit/tools/test_tuple_handle.rb +34 -2
- data/spec/unit/tools/test_varargs.rb +16 -0
- data/tasks/{spec_test.rake → integration_test.rake} +4 -32
- data/tasks/regression_test.rake +52 -0
- data/tasks/unit_test.rake +33 -58
- metadata +326 -66
- data/examples/runall.sh +0 -26
- data/lib/alf/relation.rb +0 -118
- data/lib/alf/renderer/text.rb +0 -153
- data/lib/alf/renderer/yaml.rb +0 -22
- 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.
|
|
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
|
+
# 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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
5
|
-
require
|
|
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
|
-
|
|
103
|
-
|
|
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?(
|
|
214
|
-
arg
|
|
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
|
-
|
|
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
|
|
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(
|
|
619
|
-
@input =
|
|
620
|
-
|
|
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
|
|
996
|
+
# Creates a reader instance.
|
|
832
997
|
#
|
|
833
|
-
|
|
834
|
-
|
|
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
|
|
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 =
|
|
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=
|
|
985
|
-
"Set the environment
|
|
986
|
-
@environment = Environment.
|
|
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, "
|
|
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
|
-
|
|
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 =
|
|
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::
|
|
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
|
-
|
|
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/
|
|
3909
|
+
require "alf/text"
|
|
3910
|
+
require "alf/yaml"
|