lib/alf.rb in alf-0.9.2 vs lib/alf.rb in alf-0.9.3
- old
+ new
@@ -1,11 +1,15 @@
+require "alf/version"
+require "alf/loader"
+
require "enumerator"
require "stringio"
require "set"
-require "alf/version"
-require "alf/loader"
+require 'myrrha/to_ruby_literal'
+require 'myrrha/coerce'
+
#
# Classy data-manipulation dressed in a DSL (+ commandline)
#
module Alf
@@ -13,10 +17,76 @@
# Provides tooling methods that are used here and there in Alf.
#
module Tools
#
+ # Parse a string with commandline arguments and returns an array.
+ #
+ # Example:
+ #
+ # parse_commandline_args("--text --size=10") # => ['--text', '--size=10']
+ #
+ def parse_commandline_args(args)
+ args = args.split(/\s+/)
+ result = []
+ until args.empty?
+ if args.first[0,1] == '"'
+ if args.first[-1,1] == '"'
+ result << args.shift[1...-1]
+ else
+ block = [ args.shift[1..-1] ]
+ while args.first[-1,1] != '"'
+ block << args.shift
+ end
+ block << args.shift[0...-1]
+ result << block.join(" ")
+ end
+ elsif args.first[0,1] == "'"
+ if args.first[-1,1] == "'"
+ result << args.shift[1...-1]
+ else
+ block = [ args.shift[1..-1] ]
+ while args.first[-1,1] != "'"
+ block << args.shift
+ end
+ block << args.shift[0...-1]
+ result << block.join(" ")
+ end
+ else
+ result << args.shift
+ end
+ end
+ result
+ end
+
+ # Helper to define methods with multiple signatures.
+ #
+ # Example:
+ #
+ # varargs([1, "hello"], [Integer, String]) # => [1, "hello"]
+ # varargs(["hello"], [Integer, String]) # => [nil, "hello"]
+ #
+ def varargs(args, types)
+ types.collect{|t| t===args.first ? args.shift : nil}
+ end
+
+ #
+ # Attempt to require(who) the most friendly way as possible.
+ #
+ def friendly_require(who, dep = nil, retried = false)
+ gem(who, dep) if dep && defined?(Gem)
+ require who
+ rescue LoadError => ex
+ if retried
+ raise "Unable to require #{who}, which is now needed\n"\
+ "Try 'gem install #{who}'"
+ else
+ require 'rubygems' unless defined?(Gem)
+ friendly_require(who, dep, true)
+ end
+ end
+
# Returns the unqualified name of a ruby class or module
#
# Example
#
# class_name(Alf::Tools) -> :Tools
@@ -97,13 +167,12 @@
compile('true')
when Hash
if expr.empty?
compile(nil)
else
- # TODO: replace inspect by to_ruby
- compile expr.each_pair.collect{|k,v|
- "(#{k} == #{v.inspect})"
+ compile expr.each_pair.collect{|k,v|
+ "(self.#{k} == #{Myrrha.to_ruby_literal(v)})"
}.join(" && ")
end
when Array
compile(Hash[*expr])
when String, Symbol
@@ -205,17 +274,35 @@
def initialize(ordering = [])
@ordering = ordering
@sorter = nil
end
+ #
+ # Coerces `arg` to an ordering key.
+ #
+ # Implemented coercions are:
+ # * Array of symbols (all attributes in ascending order)
+ # * Array of [Symbol, :asc|:desc] pairs (obvious semantics)
+ # * ProjectionKey (all its attributes in ascending order)
+ # * OrderingKey (self)
+ #
+ # @return [OrderingKey]
+ # @raises [ArgumentError] when `arg` is not recognized
+ #
def self.coerce(arg)
case arg
when Array
- if arg.all?{|a| a.is_a?(Symbol)}
- arg = arg.collect{|a| [a, :asc]}
+ if arg.all?{|a| a.is_a?(Array)}
+ OrderingKey.new(arg)
+ elsif arg.all?{|a| a.is_a?(Symbol)}
+ sliced = arg.each_slice(2)
+ if sliced.all?{|a,o| [:asc,:desc].include?(o)}
+ OrderingKey.new sliced.to_a
+ else
+ OrderingKey.new arg.collect{|a| [a, :asc]}
+ end
end
- OrderingKey.new(arg)
when ProjectionKey
arg.to_ordering_key
when OrderingKey
arg
else
@@ -260,30 +347,10 @@
extend Tools
end # module Tools
#
- # Builds and returns a lispy engine on a specific environment.
- #
- # Example(s):
- #
- # # Returns a lispy instance on the default environment
- # lispy = Alf.lispy
- #
- # # Returns a lispy instance on the examples' environment
- # lispy = Alf.lispy(Alf::Environment.examples)
- #
- # # Returns a lispy instance on a folder environment of your choice
- # lispy = Alf.lispy(Alf::Environment.folder('path/to/a/folder'))
- #
- # @see Alf::Environment about available environments and their contract
- #
- def self.lispy(env = Alf::Environment.default)
- Command::Main.new(env)
- end
-
- #
# Encapsulates the interface with the outside world, providing base iterators
# for named datasets, among others.
#
# An environment is typically obtained through the factory defined by this
# class:
@@ -299,14 +366,86 @@
# Alf::Environment.folder('path/to/a/folder')
#
# You can implement your own environment by subclassing this class and
# implementing the {#dataset} method. As additional support is implemented
# in the base class, Environment should never be mimiced.
+ #
+ # This class provides an extension point allowing to participate to auto
+ # detection and resolving of the --env=... option when alf is used in shell.
+ # See Environment.register, Environment.autodetect and Environment.recognizes?
+ # for details.
#
class Environment
+ # Registered environments
+ @@environments = []
+
#
+ # Register an environment class under a specific name.
+ #
+ # Registered class must implement a recognizes? method that takes an array
+ # of arguments; it must returns true if an environment instance can be built
+ # using those arguments, false otherwise. Please be very specific in the
+ # implementation for returning true. See also autodetect and recognizes?
+ #
+ # @param [Symbol] name name of the environment kind
+ # @param [Class] clazz class that implemented the environment
+ #
+ def self.register(name, clazz)
+ @@environments << [name, clazz]
+ (class << self; self; end).
+ send(:define_method, name) do |*args|
+ clazz.new(*args)
+ end
+ end
+
+ #
+ # Auto-detect the environment to use for specific arguments.
+ #
+ # This method returns an instance of the first registered Environment class
+ # that returns true to an invocation of recognizes?(args). It raises an
+ # ArgumentError if no such class can be found.
+ #
+ # @return [Environment] an environment instance
+ # @raise [ArgumentError] when no registered class recognizes the arguments
+ #
+ def self.autodetect(*args)
+ if (args.size == 1) && args.first.is_a?(Environment)
+ return args.first
+ else
+ @@environments.each do |name,clazz|
+ return clazz.new(*args) if clazz.recognizes?(args)
+ end
+ end
+ raise ArgumentError, "Unable to auto-detect Environment with #{args.inspect}"
+ end
+
+ #
+ # (see Environment.autodetect)
+ #
+ def self.coerce(*args)
+ autodetect(*args)
+ end
+
+ #
+ # Returns true _args_ can be used for building an environment instance,
+ # false otherwise.
+ #
+ # When returning true, an immediate invocation of new(*args) should
+ # succeed. While runtime exception are admitted (no such database, for
+ # example), argument errors should not occur (missing argument, wrong
+ # typing, etc.).
+ #
+ # Please be specific in the implementation of this extension point, as
+ # registered environments for a chain and each of them should have a
+ # chance of being selected.
+ #
+ def self.recognizes?(args)
+ false
+ end
+
+ #
# Returns a dataset whose name is provided.
#
# This method resolves named datasets to tuple enumerables. When the
# dataset exists, this method must return an Iterator, typically a
# Reader instance. Otherwise, it must throw a NoSuchDatasetError.
@@ -376,11 +515,23 @@
# those for which a Reader subclass has been previously registered.
# This environment then serves reader instances.
#
class Folder < Environment
+ #
+ # (see Environment.recognizes?)
#
+ # Returns true if args contains onely a String which is an existing
+ # folder.
+ #
+ def self.recognizes?(args)
+ (args.size == 1) &&
+ args.first.is_a?(String) &&
+ File.directory?(args.first.to_s)
+ end
+
+ #
# Creates an environment instance, wired to the specified folder.
#
# @param [String] folder path to the folder to use as dataset source
#
def initialize(folder)
@@ -410,31 +561,25 @@
File.file?(f)
end
end
end
+ Environment.register(:folder, self)
end # class Folder
#
- # Factors a Folder environment on a specific path
- #
- def self.folder(path)
- Folder.new(path)
- end
-
- #
# Returns the default environment
#
def self.default
examples
end
#
# Returns the examples environment
#
def self.examples
- folder File.expand_path('../../examples', __FILE__)
+ folder File.expand_path('../../examples/operators', __FILE__)
end
end # class Environment
#
@@ -570,12 +715,14 @@
if registered = @@readers.find{|r| r[1].include?(ext)}
registered[2].new(filepath, *args)
else
raise "No registered reader for #{ext} (#{filepath})"
end
- else
+ elsif args.empty?
coerce(filepath)
+ else
+ raise ArgumentError, "Unable to return a reader for #{filepath} and #{args}"
end
end
#
# Coerces an argument to a reader, using an optional environment to convert
@@ -601,25 +748,37 @@
else
raise ArgumentError, "Unable to coerce #{arg.inspect} to a reader"
end
end
+ # Default reader options
+ DEFAULT_OPTIONS = {}
+
# @return [Environment] Wired environment
attr_accessor :environment
# @return [String or IO] Input IO, or file name
attr_accessor :input
+
+ # @return [Hash] Reader's options
+ attr_accessor :options
#
- # Creates a reader instance, with an optional input and environment wiring.
+ # Creates a reader instance.
#
# @param [String or IO] path to a file or IO object for input
# @param [Environment] environment wired environment, serving this reader
+ # @param [Hash] options Reader's options (see doc of subclasses)
#
- def initialize(input = nil, environment = nil)
- @input = input
- @environment = environment
+ def initialize(*args)
+ @input, @environment, @options = case args.first
+ when String, IO, StringIO
+ Tools.varargs(args, [args.first.class, Environment, Hash])
+ else
+ Tools.varargs(args, [String, Environment, Hash])
+ end
+ @options = self.class.const_get(:DEFAULT_OPTIONS).merge(@options || {})
end
#
# (see Iterator#pipe)
#
@@ -819,21 +978,37 @@
#
def self.each_renderer
@@renderers.each(&Proc.new)
end
+ # Default renderer options
+ DEFAULT_OPTIONS = {}
+
# Renderer input (typically an Iterator)
attr_accessor :input
# @return [Environment] Optional wired environment
attr_accessor :environment
+ # @return [Hash] Renderer's options
+ attr_accessor :options
+
#
- # Creates a renderer instance, optionally wired to an input
+ # Creates a reader instance.
#
- def initialize(input = nil)
- @input = input
+ # @param [Iterator] iterator an Iterator of tuples to render
+ # @param [Environment] environment wired environment, serving this reader
+ # @param [Hash] options Reader's options (see doc of subclasses)
+ #
+ def initialize(*args)
+ @input, @environment, @options = case args.first
+ when Array
+ Tools.varargs(args, [Array, Environment, Hash])
+ else
+ Tools.varargs(args, [Iterator, Environment, Hash])
+ end
+ @options = self.class.const_get(:DEFAULT_OPTIONS).merge(@options || {})
end
#
# Sets the renderer input.
#
@@ -875,20 +1050,18 @@
class Rash < Renderer
# (see Renderer#render)
def render(input, output)
input.each do |tuple|
- output << tuple.inspect << "\n"
+ output << Myrrha.to_ruby_literal(tuple) << "\n"
end
output
end
Renderer.register(:rash, "as ruby hashes", self)
end # class Rash
- require "alf/renderer/text"
- require "alf/renderer/yaml"
end # module Renderer
#
# Provides a factory over Alf operators and handles the interface with
# Quickl for commandline support.
@@ -935,13 +1108,20 @@
# OPTIONS
# #{summarized_options}
#
# RELATIONAL COMMANDS
# #{summarized_subcommands subcommands.select{|cmd|
- # cmd.include?(Alf::Operator::Relational)
+ # cmd.include?(Alf::Operator::Relational) &&
+ # !cmd.include?(Alf::Operator::Experimental)
# }}
#
+ # EXPERIMENTAL OPERATORS
+ # #{summarized_subcommands subcommands.select{|cmd|
+ # cmd.include?(Alf::Operator::Relational) &&
+ # cmd.include?(Alf::Operator::Experimental)
+ # }}
+ #
# NON-RELATIONAL COMMANDS
# #{summarized_subcommands subcommands.select{|cmd|
# cmd.include?(Alf::Operator::NonRelational)
# }}
#
@@ -962,65 +1142,82 @@
attr_accessor :renderer
# Creates a command instance
def initialize(env = Environment.default)
@environment = env
- extend(Lispy)
end
# Install options
options do |opt|
@execute = false
opt.on("-e", "--execute", "Execute one line of script (Lispy API)") do
@execute = true
end
- @renderer = Renderer::Rash.new
+ @renderer = nil
Renderer.each_renderer do |name,descr,clazz|
opt.on("--#{name}", "Render output #{descr}"){
@renderer = clazz.new
}
end
- opt.on('--env=FOLDER',
- "Set the environment folder to use") do |value|
- @environment = Environment.folder(value)
+ opt.on('--env=ENV',
+ "Set the environment to use") do |value|
+ @environment = Environment.autodetect(value)
end
+ opt.on('-rlibrary', "require the library, before executing alf") do |value|
+ require(value)
+ end
+
opt.on_tail('-h', "--help", "Show help") do
raise Quickl::Help
end
opt.on_tail('-v', "--version", "Show version") do
- raise Quickl::Exit, "#{program_name} #{Alf::VERSION}"\
+ raise Quickl::Exit, "alf #{Alf::VERSION}"\
" (c) 2011, Bernard Lambeau"
end
end # Alf's options
#
+ def _normalize(args)
+ opts = []
+ while !args.empty? && (args.first =~ /^\-/)
+ opts << args.shift
+ end
+ if args.empty? or (args.size == 1 && File.exists?(args.first))
+ opts << "exec"
+ end
+ opts += args
+ end
+
+ #
# Overrided because Quickl only keep --options but modifying it there
# should probably be considered a broken API.
#
def _run(argv = [])
+ argv = _normalize(argv)
# 1) Extract my options and parse them
my_argv = []
while argv.first =~ /^-/
my_argv << argv.shift
end
parse_options(my_argv)
# 2) build the operator according to -e option
operator = if @execute
- instance_eval(argv.first)
+ Alf.lispy(environment).compile(argv.first)
else
super
end
# 3) if there is a requester, then we do the job (assuming bin/alf)
# with the renderer to use. Otherwise, we simply return built operator
if operator && requester
+ renderer = self.renderer ||= Renderer::Rash.new
renderer.pipe(operator, environment).execute($stdout)
else
operator
end
end
@@ -1046,20 +1243,20 @@
#
class Show < Factory::Command(__FILE__, __LINE__)
include Command
options do |opt|
- @renderer = Renderer::Text.new
+ @renderer = nil
Renderer.each_renderer do |name,descr,clazz|
opt.on("--#{name}", "Render output #{descr}"){
@renderer = clazz.new
}
end
end
def execute(args)
- requester.renderer = @renderer
+ requester.renderer = (@renderer || requester.renderer || Text::Renderer.new)
args = [ $stdin ] if args.empty?
args.first
end
end # class Show
@@ -1454,10 +1651,13 @@
end
end
end # module Shortcut
+ # Marker for experimental operators
+ module Experimental; end
+
end # module Operator
#
# Marker module and namespace for non relational operators
#
@@ -2087,44 +2287,84 @@
# alf join suppliers supplies
#
class Join < Factory::Operator(__FILE__, __LINE__)
include Operator::Relational, Operator::Shortcut, Operator::Binary
+ #
+ # Performs a Join of two relations through a Hash buffer on the right
+ # one.
+ #
class HashBased
include Operator::Binary
+ #
+ # Implements a special Buffer for join-based relational operators.
+ #
+ # Example:
+ #
+ # buffer = Buffer::Join.new(...) # pass the right part of the join
+ # left.each do |left_tuple|
+ # key, rest = buffer.split(tuple)
+ # buffer.each(key) do |right_tuple|
+ # #
+ # # do whatever you want with left and right tuples
+ # #
+ # end
+ # end
+ #
class JoinBuffer
+ #
+ # Creates a buffer instance with the right part of the join.
+ #
+ # @param [Iterator] enum a tuple iterator, right part of the join.
+ #
def initialize(enum)
@buffer = nil
@key = nil
@enum = enum
end
+ #
+ # Splits a left tuple according to the common key.
+ #
+ # @param [Hash] tuple a left tuple of the join
+ # @return [Array] an array of two elements, the key and the rest
+ # @see ProjectionKey#split
+ #
def split(tuple)
_init(tuple) unless @key
@key.split(tuple)
end
+ #
+ # Yields each right tuple that matches a given key value.
+ #
+ # @param [Hash] key a tuple that matches elements of the common key
+ # (typically the first element returned by #split)
+ #
def each(key)
@buffer[key].each(&Proc.new) if @buffer.has_key?(key)
end
private
+ # Initialize the buffer with a right tuple
def _init(right)
@buffer = Hash.new{|h,k| h[k] = []}
@enum.each do |left|
@key = Tools::ProjectionKey.coerce(left.keys & right.keys) unless @key
@buffer[@key.project(left)] << left
end
+ @key = Tools::ProjectionKey.coerce([]) unless @key
end
- end
+ end # class JoinBuffer
protected
+ # (see Operator#_each)
def _each
buffer = JoinBuffer.new(right)
left.each do |left_tuple|
key, rest = buffer.split(left_tuple)
buffer.each(key) do |right|
@@ -2289,10 +2529,132 @@
DisjointBased.new,
datasets
end
end # class Union
+
+ #
+ # Relational matching
+ #
+ # SYNOPSIS
+ # #{program_name} #{command_name} [LEFT] RIGHT
+ #
+ # API & EXAMPLE
+ #
+ # (matching :suppliers, :supplies)
+ #
+ # DESCRIPTION
+ #
+ # This operator restricts left tuples to those for which there exists at
+ # least one right tuple that joins. This is a shortcut operator for the
+ # longer expression:
+ #
+ # (project (join xxx, yyy), [xxx's attributes])
+ #
+ # In shell:
+ #
+ # alf matching suppliers supplies
+ #
+ class Matching < Factory::Operator(__FILE__, __LINE__)
+ include Operator::Relational, Operator::Shortcut, Operator::Binary
+
+ #
+ # Performs a Matching of two relations through a Hash buffer on the right
+ # one.
+ #
+ class HashBased
+ include Operator::Binary
+
+ # (see Operator#_each)
+ def _each
+ seen, key = nil, nil
+ left.each do |left_tuple|
+ seen ||= begin
+ h = Hash.new
+ right.each do |right_tuple|
+ key ||= Tools::ProjectionKey.coerce(left_tuple.keys & right_tuple.keys)
+ h[key.project(right_tuple)] = true
+ end
+ key ||= Tools::ProjectionKey.coerce([])
+ h
+ end
+ yield(left_tuple) if seen.has_key?(key.project(left_tuple))
+ end
+ end
+
+ end # class HashBased
+
+ protected
+
+ # (see Shortcut#longexpr)
+ def longexpr
+ chain HashBased.new,
+ datasets
+ end
+
+ end # class Matching
+
+ #
+ # Relational not matching
+ #
+ # SYNOPSIS
+ # #{program_name} #{command_name} [LEFT] RIGHT
+ #
+ # API & EXAMPLE
+ #
+ # (not_matching :suppliers, :supplies)
+ #
+ # DESCRIPTION
+ #
+ # This operator restricts left tuples to those for which there does not
+ # exist any right tuple that joins. This is a shortcut operator for the
+ # longer expression:
+ #
+ # (minus xxx, (matching xxx, yyy))
+ #
+ # In shell:
+ #
+ # alf not-matching suppliers supplies
+ #
+ class NotMatching < Factory::Operator(__FILE__, __LINE__)
+ include Operator::Relational, Operator::Shortcut, Operator::Binary
+
+ #
+ # Performs a NotMatching of two relations through a Hash buffer on the
+ # right one.
+ #
+ class HashBased
+ include Operator::Binary
+
+ # (see Operator#_each)
+ def _each
+ seen, key = nil, nil
+ left.each do |left_tuple|
+ seen ||= begin
+ h = Hash.new
+ right.each do |right_tuple|
+ key ||= Tools::ProjectionKey.coerce(left_tuple.keys & right_tuple.keys)
+ h[key.project(right_tuple)] = true
+ end
+ key ||= Tools::ProjectionKey.coerce([])
+ h
+ end
+ yield(left_tuple) unless seen.has_key?(key.project(left_tuple))
+ end
+ end
+
+ end # class HashBased
+
+ protected
+
+ # (see Shortcut#longexpr)
+ def longexpr
+ chain HashBased.new,
+ datasets
+ end
+
+ end # class NotMatching
#
# Relational wraping (tuple-valued attributes)
#
# SYNOPSIS
@@ -2669,10 +3031,121 @@
end
end # class Summarize
#
+ # Relational ranking (explicit tuple positions)
+ #
+ # SYNOPSIS
+ # #{program_name} #{command_name} [OPERAND] --order=OR1... -- [RANKNAME]
+ #
+ # OPTIONS
+ # #{summarized_options}
+ #
+ # API & EXAMPLE
+ #
+ # # Position attribute => # of tuples with smaller weight
+ # (rank :parts, [:weight], :position)
+ #
+ # # Position attribute => # of tuples with greater weight
+ # (rank :parts, [[:weight, :desc]], :position)
+ #
+ # DESCRIPTION
+ #
+ # This operator computes the ranking of input tuples, according to an order
+ # relation. Precisely, it extends the input tuples with a RANKNAME attribute
+ # whose value is the number of tuples which are considered strictly less
+ # according to the specified order. For the two examples above:
+ #
+ # alf rank parts --order=weight -- position
+ # alf rank parts --order=weight,desc -- position
+ #
+ # Note that, unless the ordering key includes a candidate key for the input
+ # relation, the newly RANKNAME attribute is not necessarily a candidate key
+ # for the output one. In the example above, adding the :pid attribute
+ # ensured that position will contain all different values:
+ #
+ # alf rank parts --order=weight,pid -- position
+ #
+ # Or even:
+ #
+ # alf rank parts --order=weight,desc,pid,asc -- position
+ #
+ class Rank < Factory::Operator(__FILE__, __LINE__)
+ include Operator::Relational, Operator::Shortcut, Operator::Unary
+
+ # Ranking order
+ attr_accessor :order
+
+ # Ranking attribute name
+ attr_accessor :ranking_name
+
+ def initialize(order = [], ranking_name = :rank)
+ @order, @ranking_name = order, ranking_name
+ end
+
+ options do |opt|
+ opt.on('--order=x,y,z', 'Specify ranking order', Array) do |args|
+ @order = args.collect{|a| a.to_sym}
+ end
+ end
+
+ class SortBased
+ include Operator::Cesure
+
+ def initialize(order, ranking_name)
+ @order, @ranking_name = order, ranking_name
+ end
+
+ def ordering_key
+ OrderingKey.coerce @order
+ end
+
+ def cesure_key
+ ProjectionKey.coerce(ordering_key)
+ end
+
+ def start_cesure(key, receiver)
+ @rank ||= 0
+ @last_block = 0
+ end
+
+ def accumulate_cesure(tuple, receiver)
+ receiver.call tuple.merge(@ranking_name => @rank)
+ @last_block += 1
+ end
+
+ def flush_cesure(key, receiver)
+ @rank += @last_block
+ end
+
+ end # class SortBased
+
+ protected
+
+ # (see Operator::CommandMethods#set_args)
+ def set_args(args)
+ unless args.empty?
+ self.ranking_name = args.first.to_sym
+ end
+ self
+ end
+
+ def ordering_key
+ OrderingKey.coerce @order
+ end
+
+ def longexpr
+ sort_key = ordering_key
+ chain SortBased.new(sort_key, @ranking_name),
+ Operator::NonRelational::Sort.new(sort_key),
+ datasets
+ end
+
+ end # class Rank
+
+ #
# Relational quota-queries (position, sum progression, etc.)
#
# SYNOPSIS
# #{program_name} #{command_name} [OPERAND] --by=KEY1,... --order=OR1... AGG1 EXPR1...
#
@@ -2690,11 +3163,12 @@
# This operator computes quota values on input tuples.
#
# alf quota supplies --by=sid --order=qty -- position count sum_qty "sum(:qty)"
#
class Quota < Factory::Operator(__FILE__, __LINE__)
- include Operator::Relational, Operator::Shortcut, Operator::Unary
+ include Operator::Relational, Operator::Experimental,
+ Operator::Shortcut, Operator::Unary
# Quota by
attr_accessor :by
# Quota order
@@ -2989,37 +3463,293 @@
class Buffer
#
# Keeps tuples ordered on a specific key
#
+ # Example:
+ #
+ # sorted = Buffer::Sorted.new OrderingKey.new(...)
+ # sorted.add_all(...)
+ # sorted.each do |tuple|
+ # # tuples are ordered here
+ # end
+ #
class Sorted < Buffer
+ #
+ # Creates a buffer instance with an ordering key
+ #
def initialize(ordering_key)
@ordering_key = ordering_key
@buffer = []
end
+ #
+ # Adds all elements of an iterator to the buffer
+ #
def add_all(enum)
sorter = @ordering_key.sorter
@buffer = merge_sort(@buffer, enum.to_a.sort(&sorter), sorter)
end
+ #
+ # (see Buffer#each)
+ #
def each
@buffer.each(&Proc.new)
end
private
+ # Implements a merge sort between two iterators s1 and s2
def merge_sort(s1, s2, sorter)
(s1 + s2).sort(&sorter)
end
end # class Buffer::Sorted
end # class Buffer
- #
+ #
+ # Defines a Heading, that is, a set of attribute (name,domain) pairs.
+ #
+ class Heading
+
+ #
+ # Creates a Heading instance
+ #
+ # @param [Hash] a hash of attribute (name, type) pairs where name is
+ # a Symbol and type is a Class
+ #
+ def self.[](attributes)
+ Heading.new(attributes)
+ end
+
+ # @return [Hash] a (freezed) hash of (name, type) pairs
+ attr_reader :attributes
+
+ #
+ # Creates a Heading instance
+ #
+ # @param [Hash] a hash of attribute (name, type) pairs where name is
+ # a Symbol and type is a Class
+ #
+ def initialize(attributes)
+ @attributes = attributes.dup.freeze
+ end
+
+ #
+ # Returns heading's cardinality
+ #
+ def cardinality
+ attributes.size
+ end
+ alias :size :cardinality
+ alias :count :cardinality
+
+ #
+ # Returns heading's hash code
+ #
+ def hash
+ @hash ||= attributes.hash
+ end
+
+ #
+ # Checks equality with other heading
+ #
+ def ==(other)
+ other.is_a?(Heading) && (other.attributes == attributes)
+ end
+ alias :eql? :==
+
+ #
+ # Converts this heading to a Hash of (name,type) pairs
+ #
+ def to_hash
+ attributes.dup
+ end
+
+ #
+ # Returns a Heading literal
+ #
+ def to_ruby_literal
+ attributes.empty? ?
+ "Alf::Heading::EMPTY" :
+ "Alf::Heading[#{Myrrha.to_ruby_literal(attributes)[1...-1]}]"
+ end
+ alias :inspect :to_ruby_literal
+
+ EMPTY = Alf::Heading.new({})
+ end # class Heading
+
+ #
+ # Defines an in-memory relation data structure.
+ #
+ # A relation is a set of tuples; a tuple is a set of attribute (name, value)
+ # pairs. The class implements such a data structure with full relational
+ # algebra installed as instance methods.
+ #
+ # Relation values can be obtained in various ways, for example by invoking
+ # a relational operator on an existing relation. Relation literals are simply
+ # constructed as follows:
+ #
+ # Alf::Relation[
+ # # ... a comma list of ruby hashes ...
+ # ]
+ #
+ # See main Alf documentation about relational operators.
+ #
+ class Relation
+ include Iterator
+
+ protected
+
+ # @return [Set] the set of tuples
+ attr_reader :tuples
+
+ public
+
+ #
+ # Creates a Relation instance.
+ #
+ # @param [Set] tuples a set of tuples
+ #
+ def initialize(tuples)
+ raise ArgumentError unless tuples.is_a?(Set)
+ @tuples = tuples
+ end
+
+ #
+ # Coerces `val` to a relation.
+ #
+ # Recognized arguments are: Relation (identity coercion), Set of ruby hashes,
+ # Array of ruby hashes, Alf::Iterator.
+ #
+ # @return [Relation] a relation instance for the given set of tuples
+ # @raise [ArgumentError] when `val` is not recognized
+ #
+ def self.coerce(val)
+ case val
+ when Relation
+ val
+ when Set
+ Relation.new(val)
+ when Array
+ Relation.new val.to_set
+ when Iterator
+ Relation.new val.to_set
+ else
+ raise ArgumentError, "Unable to coerce #{val} to a Relation"
+ end
+ end
+
+ # (see Relation.coerce)
+ def self.[](*tuples)
+ coerce(tuples)
+ end
+
+ #
+ # (see Iterator#each)
+ #
+ def each(&block)
+ tuples.each(&block)
+ end
+
+ #
+ # Returns relation's cardinality (number of tuples).
+ #
+ # @return [Integer] relation's cardinality
+ #
+ def cardinality
+ tuples.size
+ end
+ alias :size :cardinality
+ alias :count :cardinality
+
+ # Returns true if this relation is empty
+ def empty?
+ cardinality == 0
+ end
+
+ #
+ # Install the DSL through iteration over defined operators
+ #
+ Operator::each do |op_class|
+ meth_name = Tools.ruby_case(Tools.class_name(op_class)).to_sym
+ if op_class.unary?
+ define_method(meth_name) do |*args|
+ op = op_class.new(*args).pipe(self)
+ Relation.coerce(op)
+ end
+ elsif op_class.binary?
+ define_method(meth_name) do |right, *args|
+ op = op_class.new(*args).pipe([self, Iterator.coerce(right)])
+ Relation.coerce(op)
+ end
+ else
+ raise "Unexpected operator #{op_class}"
+ end
+ end # Operators::each
+
+ alias :+ :union
+ alias :- :minus
+
+ # Shortcut for project(attributes, true)
+ def allbut(attributes)
+ project(attributes, true)
+ end
+
+ #
+ # (see Object#hash)
+ #
+ def hash
+ @tuples.hash
+ end
+
+ #
+ # (see Object#==)
+ #
+ def ==(other)
+ return nil unless other.is_a?(Relation)
+ other.tuples == self.tuples
+ end
+ alias :eql? :==
+
+ #
+ # Returns a textual representation of this relation
+ #
+ def to_s
+ Alf::Renderer.text(self).execute("")
+ end
+
+ #
+ # Returns an array with all tuples in this relation.
+ #
+ # @param [Tools::OrderingKey] an optional ordering key (any argument
+ # recognized by OrderingKey.coerce is supported here).
+ # @return [Array] an array of hashes, in requested order (if specified)
+ #
+ def to_a(okey = nil)
+ okey = Tools::OrderingKey.coerce(okey) if okey
+ ary = tuples.to_a
+ ary.sort!(&okey.sorter) if okey
+ ary
+ end
+
+ #
+ # Returns a literal representation of this relation
+ #
+ def to_ruby_literal
+ "Alf::Relation[" +
+ tuples.collect{|t| Myrrha.to_ruby_literal(t)}.join(', ') + "]"
+ end
+ alias :inspect :to_ruby_literal
+
+ DEE = Relation.coerce([{}])
+ DUM = Relation.coerce([])
+ end # class Relation
+
# Implements a small LISP-like DSL on top of Alf.
#
# The lispy dialect is the functional one used in .alf files and in compiled
# expressions as below:
#
@@ -3059,11 +3789,12 @@
#
def compile(expr = nil, path = nil, &block)
if expr.nil?
instance_eval(&block)
else
- (path ? Kernel.eval(expr, binding, path) : Kernel.eval(expr, binding))
+ b = _clean_binding
+ (path ? Kernel.eval(expr, b, path) : Kernel.eval(expr, b))
end
end
#
# Evaluates a query expression given by a String or a block and returns
@@ -3126,10 +3857,54 @@
def allbut(child, attributes)
(project child, attributes, true)
end
+ #
+ # Runs a command as in shell.
+ #
+ # Example:
+ #
+ # lispy = Alf.lispy(Alf::Environment.examples)
+ # op = lispy.run(['restrict', 'suppliers', '--', "city == 'Paris'"])
+ #
+ def run(argv, requester = nil)
+ Alf::Command::Main.new(environment).run(argv, requester)
+ end
+
Agg = Alf::Aggregator
+ DUM = Relation::DUM
+ DEE = Relation::DEE
+
+ private
+
+ def _clean_binding
+ binding
+ end
+
end # module Lispy
+ #
+ # Builds and returns a lispy engine on a specific environment.
+ #
+ # Example(s):
+ #
+ # # Returns a lispy instance on the default environment
+ # lispy = Alf.lispy
+ #
+ # # Returns a lispy instance on the examples' environment
+ # lispy = Alf.lispy(Alf::Environment.examples)
+ #
+ # # Returns a lispy instance on a folder environment of your choice
+ # lispy = Alf.lispy(Alf::Environment.folder('path/to/a/folder'))
+ #
+ # @see Alf::Environment about available environments and their contract
+ #
+ def self.lispy(env = Environment.default)
+ lispy = Object.new.extend(Lispy)
+ lispy.environment = Environment.coerce(env)
+ lispy
+ end
+
end # module Alf
-require "alf/relation"
+require "alf/text"
+require "alf/yaml"
\ No newline at end of file