seaquel 0.1
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.
- checksums.yaml +7 -0
- data/FEATURES +5 -0
- data/LICENSE +23 -0
- data/README +25 -0
- data/lib/seaquel.rb +13 -0
- data/lib/seaquel/ast.rb +19 -0
- data/lib/seaquel/ast/alias.rb +15 -0
- data/lib/seaquel/ast/assign.rb +17 -0
- data/lib/seaquel/ast/bin_op.rb +21 -0
- data/lib/seaquel/ast/binding.rb +20 -0
- data/lib/seaquel/ast/column.rb +50 -0
- data/lib/seaquel/ast/column_list.rb +11 -0
- data/lib/seaquel/ast/expression.rb +35 -0
- data/lib/seaquel/ast/funcall.rb +17 -0
- data/lib/seaquel/ast/immediate.rb +16 -0
- data/lib/seaquel/ast/join_op.rb +29 -0
- data/lib/seaquel/ast/list.rb +25 -0
- data/lib/seaquel/ast/literal.rb +16 -0
- data/lib/seaquel/ast/node.rb +109 -0
- data/lib/seaquel/ast/order.rb +16 -0
- data/lib/seaquel/ast/table.rb +36 -0
- data/lib/seaquel/ast/table_alias.rb +24 -0
- data/lib/seaquel/bit.rb +34 -0
- data/lib/seaquel/expression_converter.rb +181 -0
- data/lib/seaquel/generator.rb +29 -0
- data/lib/seaquel/module.rb +39 -0
- data/lib/seaquel/quoter.rb +23 -0
- data/lib/seaquel/statement.rb +245 -0
- data/lib/seaquel/statement/join.rb +36 -0
- data/lib/seaquel/statement_gatherer.rb +119 -0
- data/qed/applique/ae.rb +1 -0
- data/qed/applique/sql.rb +2 -0
- data/qed/generation.md +103 -0
- data/spec/functional/delete_spec.rb +19 -0
- data/spec/functional/insert_spec.rb +31 -0
- data/spec/functional/select_spec.rb +99 -0
- data/spec/functional/update_spec.rb +30 -0
- data/spec/lib/ast/column_spec.rb +53 -0
- data/spec/lib/ast/immediate_spec.rb +42 -0
- data/spec/spec_helper.rb +14 -0
- metadata +94 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module Seaquel::AST
|
2
|
+
class Table
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize name
|
6
|
+
@name = name
|
7
|
+
end
|
8
|
+
|
9
|
+
def visit visitor
|
10
|
+
visitor.visit_table(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def quote quoter
|
14
|
+
quoter.table(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns a table column.
|
18
|
+
#
|
19
|
+
def [] col_name
|
20
|
+
Column.new(col_name, self)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a table alias.
|
24
|
+
#
|
25
|
+
def as name
|
26
|
+
TableAlias.new(self, name)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the identifier that designates the table uniquely in the query
|
30
|
+
# as a prefix for column references. ("foo"."a")
|
31
|
+
#
|
32
|
+
def as_column_prefix quoter
|
33
|
+
quote(quoter)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
module Seaquel::AST
|
3
|
+
class TableAlias
|
4
|
+
attr_reader :table
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize table, name
|
8
|
+
@table = table
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
def [] col_name
|
13
|
+
Column.new(col_name, self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit visitor
|
17
|
+
visitor.visit_table_alias table, name
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_column_prefix quoter
|
21
|
+
quoter.identifier(name)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/seaquel/bit.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Seaquel
|
4
|
+
|
5
|
+
# Represents a small bit of an SQL expression.
|
6
|
+
#
|
7
|
+
class Bit
|
8
|
+
attr_reader :str
|
9
|
+
attr_reader :precedence
|
10
|
+
|
11
|
+
def initialize str, precedence
|
12
|
+
@str = str
|
13
|
+
@precedence = precedence
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns a string at given target_precedence. If the precedence of this
|
17
|
+
# bit is lower than target_precedence, it will not be put in parenthesis.
|
18
|
+
#
|
19
|
+
def at target_precedence
|
20
|
+
if precedence != :inf && target_precedence >= precedence
|
21
|
+
@str
|
22
|
+
else
|
23
|
+
"(#{@str})"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the SQL that you would insert toplevel, meaning at a level where
|
28
|
+
# parens aren't needed anymore.
|
29
|
+
#
|
30
|
+
def toplevel
|
31
|
+
str
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require_relative 'bit'
|
2
|
+
|
3
|
+
module Seaquel
|
4
|
+
class ExpressionConverter
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_reader :quoter
|
8
|
+
attr_reader :symbols
|
9
|
+
|
10
|
+
def initialize quoter
|
11
|
+
@quoter = quoter
|
12
|
+
@symbols = {
|
13
|
+
:eq => '=',
|
14
|
+
:gt => '>',
|
15
|
+
:gteq => '>=',
|
16
|
+
:lt => '<',
|
17
|
+
:lteq => '<=',
|
18
|
+
:is => ' IS ',
|
19
|
+
:isnot => ' IS NOT ',
|
20
|
+
:noteq => '!='
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Gets called whenever the expression contains a statement-type AST::Node.
|
25
|
+
#
|
26
|
+
def visit_node node
|
27
|
+
bit(node.to_sql, :inf)
|
28
|
+
end
|
29
|
+
|
30
|
+
def_delegator :@quoter, :string, :quote_string
|
31
|
+
def_delegator :@quoter, :number, :quote_number
|
32
|
+
|
33
|
+
def visit_assign left, right
|
34
|
+
if left.kind_of? AST::Column
|
35
|
+
bit(
|
36
|
+
[left.as_column_reference(quoter), '=', sql(right).at(0)].join)
|
37
|
+
else
|
38
|
+
bit(
|
39
|
+
[sql(left).at(0), '=', sql(right).at(0)].join)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def visit_immediate sql_val
|
44
|
+
bit(sql_val)
|
45
|
+
end
|
46
|
+
|
47
|
+
def visit_new_statement *args
|
48
|
+
::Seaquel::Generator.new(self).compact_sql
|
49
|
+
end
|
50
|
+
|
51
|
+
def visit_literal literal
|
52
|
+
bit(literal)
|
53
|
+
end
|
54
|
+
|
55
|
+
def visit_list elements
|
56
|
+
bit(elements.map { |e| sql(e).at(0) }.join(', '))
|
57
|
+
end
|
58
|
+
|
59
|
+
def visit_column_list elements
|
60
|
+
# Column lists only contain columns.
|
61
|
+
bit(elements.map { |e| e.as_column_reference(quoter) }.join(', '))
|
62
|
+
end
|
63
|
+
|
64
|
+
def visit_table table
|
65
|
+
bit(table.quote(quoter))
|
66
|
+
end
|
67
|
+
|
68
|
+
def visit_table_alias table, name
|
69
|
+
bit([
|
70
|
+
sql(table).at(0),
|
71
|
+
"AS",
|
72
|
+
quoter.identifier(name)
|
73
|
+
].join(' '))
|
74
|
+
end
|
75
|
+
|
76
|
+
def visit_alias name, to
|
77
|
+
prec = precedence(:as)
|
78
|
+
bit("#{sql(to).at(prec)} AS \"#{name}\"")
|
79
|
+
end
|
80
|
+
|
81
|
+
def visit_binop op, left, right
|
82
|
+
prec = precedence(op)
|
83
|
+
|
84
|
+
if right.nil?
|
85
|
+
op = :is if op==:eq
|
86
|
+
op = :isnot if op==:noteq
|
87
|
+
end
|
88
|
+
|
89
|
+
if symbols.has_key?(op)
|
90
|
+
symbol = symbols[op]
|
91
|
+
return bit(
|
92
|
+
[sql(left).at(prec), symbol, sql(right).at(prec)].join,
|
93
|
+
prec)
|
94
|
+
end
|
95
|
+
|
96
|
+
raise "No such operation (#{op.inspect})."
|
97
|
+
end
|
98
|
+
|
99
|
+
def visit_joinop op, exps
|
100
|
+
# Shortcut if we join one element only.
|
101
|
+
if exps.size == 1
|
102
|
+
el = exps.first
|
103
|
+
return sql(el)
|
104
|
+
end
|
105
|
+
|
106
|
+
prec = precedence(op)
|
107
|
+
parts = exps.map { |e| sql(e).at(prec) }
|
108
|
+
|
109
|
+
sql = case op
|
110
|
+
when :and
|
111
|
+
parts.join(' AND ')
|
112
|
+
end
|
113
|
+
|
114
|
+
bit(sql, prec)
|
115
|
+
end
|
116
|
+
|
117
|
+
def visit_column col
|
118
|
+
bit(col.as_full_reference(quoter))
|
119
|
+
end
|
120
|
+
|
121
|
+
def visit_binding pos
|
122
|
+
bit("$#{pos}")
|
123
|
+
end
|
124
|
+
|
125
|
+
def visit_funcall name, args
|
126
|
+
arglist = args.map { |arg| sql(arg).toplevel }.join(', ')
|
127
|
+
bit("#{name}(#{arglist})")
|
128
|
+
end
|
129
|
+
|
130
|
+
def visit_order order, expr
|
131
|
+
bit("#{sql(expr).at(0)} #{order.upcase}")
|
132
|
+
end
|
133
|
+
|
134
|
+
def sql node
|
135
|
+
# p [:sql, node]
|
136
|
+
case node
|
137
|
+
when nil
|
138
|
+
bit('NULL')
|
139
|
+
when String
|
140
|
+
bit(quote_string(node))
|
141
|
+
when Fixnum
|
142
|
+
bit(quote_number(node))
|
143
|
+
when true,false
|
144
|
+
node ? bit('TRUE') : bit('FALSE')
|
145
|
+
else
|
146
|
+
node.visit(self)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
def bit str, precedence=0
|
152
|
+
Bit.new(str, precedence)
|
153
|
+
end
|
154
|
+
|
155
|
+
def precedence op
|
156
|
+
precedences[op]
|
157
|
+
end
|
158
|
+
|
159
|
+
def precedences
|
160
|
+
@precedences ||= begin
|
161
|
+
prec = 1 # 0 reserved for simple values
|
162
|
+
precs = Hash.new(0)
|
163
|
+
|
164
|
+
assign = -> (*list) {
|
165
|
+
list.map { |e| precs[e] = prec }
|
166
|
+
prec += 1
|
167
|
+
}
|
168
|
+
|
169
|
+
# By listing something above something else, it gets a lower precedence
|
170
|
+
# assigned. List from lower to higher precedences. Equivalence classes
|
171
|
+
# by extending the argument list.
|
172
|
+
assign[:as, :is, :isnot]
|
173
|
+
assign[:or]
|
174
|
+
assign[:and]
|
175
|
+
assign[:eq, :gt, :gteq, :lt, :lteq]
|
176
|
+
|
177
|
+
precs
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
require 'seaquel/expression_converter'
|
5
|
+
|
6
|
+
module Seaquel
|
7
|
+
class Generator
|
8
|
+
def initialize ast
|
9
|
+
@ast = ast
|
10
|
+
end
|
11
|
+
|
12
|
+
def compact_sql
|
13
|
+
quoter = Quoter.new
|
14
|
+
|
15
|
+
# Construct a statement
|
16
|
+
expression_converter = ExpressionConverter.new(quoter)
|
17
|
+
statement = Statement.new(expression_converter)
|
18
|
+
|
19
|
+
# And a visitor for the AST
|
20
|
+
visitor = StatementGatherer.new(statement, quoter)
|
21
|
+
|
22
|
+
# Gather statement details from the AST
|
23
|
+
@ast.visit(visitor)
|
24
|
+
|
25
|
+
# Turn the statement into SQL.
|
26
|
+
statement.to_s(:compact)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Seaquel
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def select
|
7
|
+
AST::Node.new(:select)
|
8
|
+
end
|
9
|
+
def update table
|
10
|
+
AST::Node.new(:update, table)
|
11
|
+
end
|
12
|
+
def insert
|
13
|
+
AST::Node.new(:insert)
|
14
|
+
end
|
15
|
+
def delete
|
16
|
+
AST::Node.new(:delete)
|
17
|
+
end
|
18
|
+
|
19
|
+
def column name
|
20
|
+
AST::Column.new(name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def table name
|
24
|
+
AST::Table.new(name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def literal text
|
28
|
+
AST::Literal.new(text)
|
29
|
+
end
|
30
|
+
def immediate ruby_obj
|
31
|
+
AST::Immediate.new(ruby_obj)
|
32
|
+
end
|
33
|
+
def binding position
|
34
|
+
AST::Binding.new(position)
|
35
|
+
end
|
36
|
+
def funcall name, *args
|
37
|
+
AST::Funcall.new(name, args)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Seaquel
|
2
|
+
class Quoter
|
3
|
+
def table name
|
4
|
+
identifier(name)
|
5
|
+
end
|
6
|
+
|
7
|
+
def column name
|
8
|
+
%Q("#{name}")
|
9
|
+
end
|
10
|
+
|
11
|
+
def string str
|
12
|
+
"'" + str.gsub("'", "''") + "'"
|
13
|
+
end
|
14
|
+
|
15
|
+
def number num
|
16
|
+
num.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def identifier name
|
20
|
+
%Q("#{name}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
|
2
|
+
require_relative 'statement/join'
|
3
|
+
|
4
|
+
module Seaquel
|
5
|
+
|
6
|
+
# Gets raised when during the generation of SQL we discover that the statement
|
7
|
+
# has no valid form.
|
8
|
+
#
|
9
|
+
class InvalidStatement < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
# Root node for an sql statement.
|
13
|
+
#
|
14
|
+
class Statement
|
15
|
+
|
16
|
+
attr_reader :expression_convertor
|
17
|
+
|
18
|
+
attr_reader :from
|
19
|
+
attr_reader :project
|
20
|
+
attr_reader :where
|
21
|
+
attr_reader :set
|
22
|
+
attr_reader :target
|
23
|
+
attr_reader :values
|
24
|
+
attr_reader :fields
|
25
|
+
attr_reader :joins
|
26
|
+
attr_reader :limit
|
27
|
+
attr_reader :offset
|
28
|
+
attr_reader :order_by
|
29
|
+
|
30
|
+
def initialize expression_convertor
|
31
|
+
@expression_convertor = expression_convertor
|
32
|
+
|
33
|
+
@from = list()
|
34
|
+
@project = list()
|
35
|
+
@order_by = list()
|
36
|
+
@where = AST::JoinOp.new(:and, [])
|
37
|
+
@set = list()
|
38
|
+
@values = list()
|
39
|
+
@fields = column_list()
|
40
|
+
@joins = []
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_limit n
|
44
|
+
@limit = n
|
45
|
+
end
|
46
|
+
def set_offset n
|
47
|
+
@offset = n
|
48
|
+
end
|
49
|
+
|
50
|
+
# Allows to determine the type of statement generated.
|
51
|
+
#
|
52
|
+
# @param type [{:update, :select, :delete}]
|
53
|
+
#
|
54
|
+
def set_type type
|
55
|
+
@type = type
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_target table
|
59
|
+
@target = table
|
60
|
+
end
|
61
|
+
|
62
|
+
# Produces a join clause and adds it to the joins list.
|
63
|
+
#
|
64
|
+
# @param tables [Array<Object>] a list of tables to join to
|
65
|
+
# @return [Join] Join subobject.
|
66
|
+
#
|
67
|
+
def join tables
|
68
|
+
join = Join.new(tables)
|
69
|
+
joins << join
|
70
|
+
|
71
|
+
join
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s variant=:compact
|
75
|
+
case @type
|
76
|
+
when :update
|
77
|
+
generate_update(variant)
|
78
|
+
when :insert
|
79
|
+
generate_insert(variant)
|
80
|
+
when :delete
|
81
|
+
generate_delete(variant)
|
82
|
+
else
|
83
|
+
generate_select(variant)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Turns an expression into SQL
|
88
|
+
#
|
89
|
+
def convert exp
|
90
|
+
expression_convertor.sql(exp).toplevel
|
91
|
+
end
|
92
|
+
private
|
93
|
+
|
94
|
+
# Produces an empty list.
|
95
|
+
#
|
96
|
+
def list
|
97
|
+
AST::List.new
|
98
|
+
end
|
99
|
+
def column_list
|
100
|
+
AST::ColumnList.new
|
101
|
+
end
|
102
|
+
|
103
|
+
# Wraps str in parenthesises.
|
104
|
+
#
|
105
|
+
# Example:
|
106
|
+
# parens('foo') # => '(foo)'
|
107
|
+
def parens str
|
108
|
+
"(#{str})"
|
109
|
+
end
|
110
|
+
|
111
|
+
# Generates SQL for a DELETE statement.
|
112
|
+
#
|
113
|
+
def generate_delete variant
|
114
|
+
parts = []
|
115
|
+
parts << "DELETE"
|
116
|
+
|
117
|
+
unless from.empty?
|
118
|
+
parts << 'FROM'
|
119
|
+
parts << convert(from)
|
120
|
+
end
|
121
|
+
|
122
|
+
unless joins.empty?
|
123
|
+
joins.each do |join|
|
124
|
+
parts << join.convert(
|
125
|
+
self # as #convert
|
126
|
+
)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
unless where.empty?
|
131
|
+
parts << 'WHERE'
|
132
|
+
parts << convert(where)
|
133
|
+
end
|
134
|
+
|
135
|
+
parts.join(' ')
|
136
|
+
end
|
137
|
+
|
138
|
+
# Generates SQL for an INSERT statement.
|
139
|
+
#
|
140
|
+
def generate_insert variant
|
141
|
+
parts = []
|
142
|
+
parts << "INSERT"
|
143
|
+
|
144
|
+
if target
|
145
|
+
parts << "INTO"
|
146
|
+
parts << convert(target)
|
147
|
+
end
|
148
|
+
|
149
|
+
unless values.empty?
|
150
|
+
if fields.empty?
|
151
|
+
# If you are inserting data to all the columns, the column names can be
|
152
|
+
# omitted.
|
153
|
+
parts << 'VALUES'
|
154
|
+
parts << values.
|
155
|
+
map { |value_list|
|
156
|
+
parens(convert(value_list)) }.
|
157
|
+
join(', ')
|
158
|
+
else
|
159
|
+
# A field list must be produced
|
160
|
+
# assert: !values.empty? && !fields.empty?
|
161
|
+
|
162
|
+
parts << parens(convert(fields))
|
163
|
+
parts << 'VALUES'
|
164
|
+
parts << values.
|
165
|
+
map { |value_list|
|
166
|
+
raise InvalidStatement, "Field list in INSERT statement doesn't match value list (#{fields.size} fields and #{value_list.size} values)." \
|
167
|
+
unless fields.size == value_list.size
|
168
|
+
|
169
|
+
parens(convert(value_list)) }.
|
170
|
+
join(', ')
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
parts.join(' ')
|
175
|
+
end
|
176
|
+
|
177
|
+
# Generates SQL for a SELECT statement.
|
178
|
+
#
|
179
|
+
def generate_select variant
|
180
|
+
parts = []
|
181
|
+
parts << "SELECT"
|
182
|
+
|
183
|
+
if project.empty?
|
184
|
+
parts << '*'
|
185
|
+
else
|
186
|
+
parts << convert(project)
|
187
|
+
end
|
188
|
+
|
189
|
+
unless from.empty?
|
190
|
+
parts << 'FROM'
|
191
|
+
parts << convert(from)
|
192
|
+
end
|
193
|
+
|
194
|
+
unless joins.empty?
|
195
|
+
joins.each do |join|
|
196
|
+
parts << join.convert(
|
197
|
+
self # as #convert
|
198
|
+
)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
unless where.empty?
|
203
|
+
parts << 'WHERE'
|
204
|
+
parts << convert(where)
|
205
|
+
end
|
206
|
+
|
207
|
+
unless order_by.empty?
|
208
|
+
parts << 'ORDER BY'
|
209
|
+
parts << convert(order_by)
|
210
|
+
end
|
211
|
+
|
212
|
+
if offset
|
213
|
+
parts << 'OFFSET'
|
214
|
+
parts << convert(offset)
|
215
|
+
end
|
216
|
+
|
217
|
+
if limit
|
218
|
+
parts << 'LIMIT'
|
219
|
+
parts << convert(limit)
|
220
|
+
end
|
221
|
+
|
222
|
+
parts.join(' ')
|
223
|
+
end
|
224
|
+
|
225
|
+
# Generates SQL for an UPDATE statement.
|
226
|
+
#
|
227
|
+
def generate_update variant
|
228
|
+
parts = []
|
229
|
+
parts << 'UPDATE'
|
230
|
+
parts << convert(target)
|
231
|
+
|
232
|
+
unless set.empty?
|
233
|
+
parts << 'SET'
|
234
|
+
parts << convert(set)
|
235
|
+
end
|
236
|
+
|
237
|
+
unless where.empty?
|
238
|
+
parts << 'WHERE'
|
239
|
+
parts << convert(where)
|
240
|
+
end
|
241
|
+
|
242
|
+
parts.join(' ')
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|