lib/eno.rb in eno-0.4 vs lib/eno.rb in eno-0.5

- old
+ new

@@ -1,486 +1,17 @@ # frozen_string_literal: true require 'modulation/gem' -export :Query, :SQL, :Expression, :Identifier, :Alias +export_default :Eno module ::Kernel def Q(**ctx, &block) - Query.new(**ctx, &block) + Eno::Query.new(**ctx, &block) end end -class Expression - def self.quote(expr) - case expr - when Query - "(#{expr.to_sql.strip})" - when Expression - expr.to_sql - when Symbol - expr.to_s - when String - "'#{expr}'" - else - expr.inspect - end - end - - attr_reader :members, :props - - def initialize(*members, **props) - @members = members - @props = props - end - - def as(sym = nil, &block) - if sym - Alias.new(self, sym) - else - Alias.new(self, Query.new(&block)) - end - end - - def desc - Desc.new(self) - end - - def over(sym = nil, &block) - Over.new(self, sym || WindowExpression.new(&block)) - end - - def ==(expr2) - Operator.new('=', self, expr2) - end - - def !=(expr2) - Operator.new('<>', self, expr2) - end - - def <(expr2) - Operator.new('<', self, expr2) - end - - def >(expr2) - Operator.new('>', self, expr2) - end - - def <=(expr2) - Operator.new('<=', self, expr2) - end - - def >=(expr2) - Operator.new('>=', self, expr2) - end - - def &(expr2) - Operator.new('and', self, expr2) - end - - def |(expr2) - Operator.new('or', self, expr2) - end - - def +(expr2) - Operator.new('+', self, expr2) - end - - def -(expr2) - Operator.new('-', self, expr2) - end - - def *(expr2) - Operator.new('*', self, expr2) - end - - def /(expr2) - Operator.new('/', self, expr2) - end - - def %(expr2) - Operator.new('%', self, expr2) - end - - # not - def !@ - Not.new(self) - end - - def null? - IsNull.new(self) - end - - def not_null? - IsNotNull.new(self) - end - - def join(sym, **props) - Join.new(self, sym, **props) - end - - def inner_join(sym, **props) - join(sym, props.merge(type: :inner)) - end -end - -class Operator < Expression - def initialize(*members, **props) - op = members[0] - if Operator === members[1] && op == members[1].op - members = [op] + members[1].members[1..-1] + members[2..-1] - end - if Operator === members[2] && op == members[2].op - members = members[0..1] + members[2].members[1..-1] - end - - super(*members, **props) - end - - def op - @members[0] - end - - def to_sql - op = " #{@members[0]} " - "(%s)" % @members[1..-1].map { |m| Expression.quote(m) }.join(op) - end -end - -class Desc < Expression - def to_sql - "#{Expression.quote(@members[0])} desc" - end -end - -class Over < Expression - def to_sql - "#{Expression.quote(@members[0])} over #{Expression.quote(@members[1])}" - end -end - -class Not < Expression - def to_sql - "(not #{Expression.quote(@members[0])})" - end -end - -class IsNull < Expression - def to_sql - "(#{Expression.quote(@members[0])} is null)" - end - - def !@ - IsNotNull.new(members[0]) - end -end - -class IsNotNull < Expression - def to_sql - "(#{Expression.quote(@members[0])} is not null)" - end -end - -class WindowExpression < Expression - def initialize(&block) - instance_eval(&block) - end - - def partition_by(*args) - @partition_by = args - end - - def order_by(*args) - @order_by = args - end - - def range_unbounded - @range = 'between unbounded preceding and unbounded following' - end - - def to_sql - "(%s)" % [ - _partition_by_clause, - _order_by_clause, - _range_clause - ].join.strip - end - - def _partition_by_clause - return nil unless @partition_by - "partition by %s " % @partition_by.map { |e| Expression.quote(e) }.join(', ') - end - - def _order_by_clause - return nil unless @order_by - "order by %s " % @order_by.map { |e| Expression.quote(e) }.join(', ') - end - - def _range_clause - return nil unless @range - "range #{@range} " - end - - def method_missing(sym) - super if sym == :to_hash - Identifier.new(sym) - end -end - -class QuotedExpression < Expression - def to_sql - Expression.quote(@members[0]) - end -end - -class Identifier < Expression - def to_sql - @members[0].to_s - end - - def method_missing(sym) - super if sym == :to_hash - Identifier.new("#{@members[0]}.#{sym}") - end - - def _empty_placeholder? - m = @members[0] - Symbol === m && m == :_ - end -end - -class Alias < Expression - def to_sql - "#{Expression.quote(@members[0])} as #{Expression.quote(@members[1])}" - end -end - -class FunctionCall < Expression - def to_sql - fun = @members[0] - if @members.size == 2 && Identifier === @members.last && @members.last._empty_placeholder? - "#{fun}()" - else - "#{fun}(#{@members[1..-1].map { |a| Expression.quote(a) }.join(', ')})" - end - end -end - -class Join < Expression - H_JOIN_TYPES = { - nil: 'join', - inner: 'inner join', - outer: 'outer join' - } - - def to_sql - ("%s %s %s %s" % [ - Expression.quote(@members[0]), - H_JOIN_TYPES[@props[:type]], - Expression.quote(@members[1]), - condition_sql - ]).strip - end - - def condition_sql - if @props[:on] - 'on %s' % Expression.quote(@props[:on]) - elsif using_fields = @props[:using] - fields = using_fields.is_a?(Array) ? using_fields : [using_fields] - 'using (%s)' % fields.map { |f| Expression.quote(f) }.join(', ') - else - nil - end - end -end - -class From < Expression - def to_sql - "from %s" % @members.map { |m| member_sql(m) }.join(', ') - end - - def member_sql(member) - if Query === member - "%s t1" % Expression.quote(member) - elsif Alias === member && Query === member.members[0] - "%s %s" % [Expression.quote(member.members[0]), Expression.quote(member.members[1])] - else - Expression.quote(member) - end - end -end - -class With < Expression - def to_sql - "with %s" % @members.map { |e| Expression.quote(e) }.join(', ') - end -end - -class Select < Expression - def to_sql - "select %s%s" % [distinct_clause, @members.map { |e| Expression.quote(e) }.join(', ')] - end - - def distinct_clause - case (on = @props[:distinct]) - when nil - nil - when true - "distinct " - when Array - "distinct on (%s) " % on.map { |e| Expression.quote(e) }.join(', ') - else - "distinct on %s " % Expression.quote(on) - end - end -end - -class Where < Expression - def to_sql - "where %s" % @members.map { |e| Expression.quote(e) }.join(' and ') - end -end - -class Window < Expression - def initialize(sym, &block) - super(sym) - @block = block - end - - def to_sql - "window %s as %s" % [ - Expression.quote(@members.first), - WindowExpression.new(&@block).to_sql - ] - end -end - -class OrderBy < Expression - def to_sql - "order by %s" % @members.map { |e| Expression.quote(e) }.join(', ') - end -end - -class Limit < Expression - def to_sql - "limit %d" % @members[0] - end -end - -class Query - def initialize(**ctx, &block) - @ctx = ctx - @block = block - end - - def to_sql(**ctx) - r = SQL.new(@ctx.merge(ctx)) - r.to_sql(&@block) - end - - def as(sym) - Alias.new(self, sym) - end - - def where(&block) - old_block = @block - Query.new(@ctx) { - instance_eval(&old_block) - where instance_eval(&block) - } - end - - def mutate(&block) - old_block = @block - Query.new(@ctx) { - instance_eval(&old_block) - instance_eval(&block) - } - end -end - -class SQL - def initialize(ctx) - @ctx = ctx - end - - def to_sql(&block) - instance_eval(&block) - [ - @with, - @select || default_select, - @from, - @where, - @window, - @order_by, - @limit - ].compact.map { |c| c.to_sql }.join(' ') - end - - def _q(expr) - QuotedExpression.new(expr) - end - - def default_select - Select.new(:*) - end - - def method_missing(sym, *args) - if @ctx.has_key?(sym) - value = @ctx[sym] - return Symbol === value ? Identifier.new(value) : value - end - - super if sym == :to_hash - if args.empty? - Identifier.new(sym) - else - FunctionCall.new(sym, *args) - end - end - - def with(*members, **props) - @with = With.new(*members, **props) - end - - H_EMPTY = {}.freeze - - def select(*members, **props) - if members.empty? && !props.empty? - members = props.map { |k, v| Alias.new(v, k) } - props = {} - end - @select = Select.new(*members, **props) - end - - def from(*members, **props) - @from = From.new(*members, **props) - end - - def where(expr) - if @where - @where.members << expr - else - @where = Where.new(expr) - end - end - - def window(sym, &block) - @window = Window.new(sym, &block) - end - - def order_by(*members, **props) - @order_by = OrderBy.new(*members, **props) - end - - def limit(*members) - @limit = Limit.new(*members) - end - - def all(sym = nil) - if sym - Identifier.new("#{sym}.*") - else - Identifier.new('*') - end - end -end +module Eno + include_from('./eno/expressions') + SQL = import('./eno/sql')::SQL + Query = import('./eno/query')::Query +end \ No newline at end of file