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.
@@ -0,0 +1,16 @@
1
+
2
+
3
+ module Seaquel::AST
4
+ class Order
5
+ attr_reader :order, :column
6
+
7
+ def initialize order, column
8
+ @order = order
9
+ @column = column
10
+ end
11
+
12
+ def visit visitor
13
+ visitor.visit_order(order, column)
14
+ end
15
+ end
16
+ end
@@ -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
@@ -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