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
|
+
|
2
|
+
|
3
|
+
module Seaquel
|
4
|
+
class Statement
|
5
|
+
# A join clause inside an SQL statement.
|
6
|
+
class Join
|
7
|
+
attr_reader :tables
|
8
|
+
attr_reader :ons
|
9
|
+
|
10
|
+
def initialize tables
|
11
|
+
@tables = AST::List.new(tables)
|
12
|
+
|
13
|
+
@ons = AST::JoinOp.new(:and, [])
|
14
|
+
end
|
15
|
+
|
16
|
+
def on exps
|
17
|
+
ons.concat(*exps)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Turns a join statement into SQL by invoking conversion_helper on the
|
21
|
+
# right parts.
|
22
|
+
#
|
23
|
+
# @param conversion_helper [#convert] instance of a class that implements
|
24
|
+
# a #convert method that turns expressions into SQL.
|
25
|
+
# @return [String] SQL string for this join
|
26
|
+
#
|
27
|
+
def convert conversion_helper
|
28
|
+
parts = []
|
29
|
+
parts << 'INNER JOIN'
|
30
|
+
parts << conversion_helper.convert(tables)
|
31
|
+
parts << 'ON'
|
32
|
+
parts << conversion_helper.convert(ons)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
|
2
|
+
module Seaquel
|
3
|
+
# Visits a statement AST and issues a statement.
|
4
|
+
class StatementGatherer
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_reader :current_join
|
8
|
+
|
9
|
+
def initialize statement, quoter
|
10
|
+
@statement = statement
|
11
|
+
@quoter = quoter
|
12
|
+
@current_join = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def_delegator :@quoter, :table, :quote_table
|
16
|
+
|
17
|
+
def visit_from parent, *args
|
18
|
+
s.from.concat(args)
|
19
|
+
|
20
|
+
continue(parent)
|
21
|
+
end
|
22
|
+
def visit_select parent
|
23
|
+
continue(parent)
|
24
|
+
end
|
25
|
+
def visit_project parent, fields
|
26
|
+
continue(parent)
|
27
|
+
|
28
|
+
# Since the tree is processed in lifo order, we need to only apply the
|
29
|
+
# very last project.
|
30
|
+
s.project.replace(fields)
|
31
|
+
end
|
32
|
+
def visit_order_by parent, list
|
33
|
+
continue(parent)
|
34
|
+
|
35
|
+
s.order_by.replace(list)
|
36
|
+
end
|
37
|
+
def visit_where parent, expression
|
38
|
+
continue(parent)
|
39
|
+
|
40
|
+
s.where.concat(expression)
|
41
|
+
end
|
42
|
+
|
43
|
+
def visit_update parent, table
|
44
|
+
continue(parent)
|
45
|
+
|
46
|
+
s.set_type(:update)
|
47
|
+
s.set_target(table)
|
48
|
+
end
|
49
|
+
def visit_set parent, assign_list
|
50
|
+
continue(parent)
|
51
|
+
|
52
|
+
s.set.concat(assign_list)
|
53
|
+
end
|
54
|
+
|
55
|
+
def visit_join parent, tables
|
56
|
+
continue(parent)
|
57
|
+
|
58
|
+
@current_join = s.join(tables)
|
59
|
+
end
|
60
|
+
def visit_on(parent, exps)
|
61
|
+
continue(parent)
|
62
|
+
|
63
|
+
raise InvalidExpression, ".on without a .join encoutered" \
|
64
|
+
unless current_join
|
65
|
+
|
66
|
+
current_join.on(exps)
|
67
|
+
end
|
68
|
+
|
69
|
+
def visit_insert parent
|
70
|
+
continue(parent)
|
71
|
+
|
72
|
+
s.set_type(:insert)
|
73
|
+
end
|
74
|
+
def visit_into parent, table
|
75
|
+
continue(parent)
|
76
|
+
|
77
|
+
s.set_target(table)
|
78
|
+
end
|
79
|
+
def visit_values parent, values
|
80
|
+
continue(parent)
|
81
|
+
|
82
|
+
s.values << AST::List.new(values)
|
83
|
+
end
|
84
|
+
def visit_fields parent, fields
|
85
|
+
continue(parent)
|
86
|
+
|
87
|
+
s.fields.concat(fields)
|
88
|
+
end
|
89
|
+
|
90
|
+
def visit_delete parent
|
91
|
+
continue(parent)
|
92
|
+
|
93
|
+
s.set_type(:delete)
|
94
|
+
end
|
95
|
+
|
96
|
+
def visit_offset parent, n
|
97
|
+
continue(parent)
|
98
|
+
|
99
|
+
s.set_offset(n)
|
100
|
+
end
|
101
|
+
def visit_limit parent, n
|
102
|
+
continue(parent)
|
103
|
+
|
104
|
+
s.set_limit(n)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
# A shorthand for saying that node needs to be visited.
|
109
|
+
#
|
110
|
+
def continue node
|
111
|
+
node.visit(self) if node
|
112
|
+
end
|
113
|
+
|
114
|
+
# A short-cut for the code in here.
|
115
|
+
def s
|
116
|
+
@statement
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/qed/applique/ae.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ae'
|
data/qed/applique/sql.rb
ADDED
data/qed/generation.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
|
2
|
+
# Overview
|
3
|
+
|
4
|
+
This library deals with generation of valid SQL. What one would like to have is
|
5
|
+
a programmatic interface to SQL that allows to create a query and turn it into
|
6
|
+
SQL.
|
7
|
+
|
8
|
+
~~~ruby
|
9
|
+
include Seaquel # to avoid having to write Seaquel.select - optional
|
10
|
+
|
11
|
+
select.from(table('foobar')).where(column('name').eq('baz')).
|
12
|
+
to_sql # => %Q(SELECT * FROM "foobar" WHERE "name"='baz')
|
13
|
+
~~~
|
14
|
+
|
15
|
+
As can be seen from the above snippet, SQL construction uses a DSL that is functional in nature. The DSL needs to support all kinds of constructs and cover a broad range of SQL syntax.
|
16
|
+
|
17
|
+
~~~ruby
|
18
|
+
update(table('bar')).set(literal('a=1'), column('b').to(2)).
|
19
|
+
to_sql # => %Q(UPDATE "bar" SET a=1, "b"=2)
|
20
|
+
~~~
|
21
|
+
|
22
|
+
~~~ruby
|
23
|
+
insert.fields(column('a'), column('b')).values('1', 2).
|
24
|
+
into(table('baz')).
|
25
|
+
to_sql # => %Q(INSERT INTO "baz" ("a", "b") VALUES ('1', 2))
|
26
|
+
~~~
|
27
|
+
|
28
|
+
~~~ruby
|
29
|
+
delete.from(table('baz')).
|
30
|
+
where(column('a').eq(1)).
|
31
|
+
join(table('bar')).on(column('b').eq(column('c'))).
|
32
|
+
to_sql # => %Q(DELETE FROM "baz" INNER JOIN "bar" ON "b"="c" WHERE "a"=1)
|
33
|
+
~~~
|
34
|
+
|
35
|
+
As you can see, method invocation can be in any order and the generator figures out what you mean.
|
36
|
+
|
37
|
+
# Different Types of JOINs
|
38
|
+
|
39
|
+
# Positional (Binding) Parameters
|
40
|
+
|
41
|
+
To bind a value in your client, use the `binding(position)` macro. The returned object can be treated as part of an expression.
|
42
|
+
|
43
|
+
~~~ruby
|
44
|
+
select.where(column('a').eq(binding(1))).
|
45
|
+
to_sql # => %Q(SELECT * WHERE "a"=$1)
|
46
|
+
~~~
|
47
|
+
|
48
|
+
# SQL Function Calls
|
49
|
+
|
50
|
+
To formulate something like `SELECT string_agg(a) FROM table`, you need to be able to generate function calls.
|
51
|
+
|
52
|
+
~~~ruby
|
53
|
+
select.project(funcall('string_agg', column(:a))).from(table(:table)).
|
54
|
+
to_sql # => %Q(SELECT string_agg("a") FROM "table")
|
55
|
+
~~~
|
56
|
+
|
57
|
+
The funcall method call can be wrapped inside a Ruby function, yielding a quite natural interface:
|
58
|
+
|
59
|
+
~~~ruby
|
60
|
+
def string_agg col
|
61
|
+
funcall('string_agg', column(col))
|
62
|
+
end
|
63
|
+
|
64
|
+
select.project(string_agg(:a)).from(table(:table)).
|
65
|
+
to_sql # => %Q(SELECT string_agg("a") FROM "table")
|
66
|
+
~~~
|
67
|
+
|
68
|
+
# SELECT
|
69
|
+
|
70
|
+
Here's how you access columns from a given table.
|
71
|
+
|
72
|
+
~~~ruby
|
73
|
+
users = table('users')
|
74
|
+
select.project(users['id'], users['name']).from(users).
|
75
|
+
to_sql # => %Q(SELECT "users"."id", "users"."name" FROM "users")
|
76
|
+
~~~
|
77
|
+
|
78
|
+
Also, table aliases are possible and can shorten the issued statements quite a bit.
|
79
|
+
|
80
|
+
~~~ruby
|
81
|
+
users = table('users').as('u')
|
82
|
+
select.project(users['id'], users['name']).from(users).
|
83
|
+
to_sql # => %Q(SELECT "u"."id", "u"."name" FROM "users" AS "u")
|
84
|
+
~~~
|
85
|
+
|
86
|
+
Here's how you set an offset and a limit.
|
87
|
+
|
88
|
+
~~~ruby
|
89
|
+
select.limit(5).offset(3).
|
90
|
+
to_sql # => %Q(SELECT * OFFSET 3 LIMIT 5)
|
91
|
+
~~~
|
92
|
+
|
93
|
+
|
94
|
+
# INSERT
|
95
|
+
|
96
|
+
PostgreSQL allows inserting multiple rows at once:
|
97
|
+
|
98
|
+
~~~ruby
|
99
|
+
insert.into(table('foo')).fields(column('a'), column('b')).
|
100
|
+
values(1, 2).
|
101
|
+
values(2, 3).
|
102
|
+
to_sql # => %Q(INSERT INTO "foo" ("a", "b") VALUES (1, 2), (2, 3))
|
103
|
+
~~~
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe 'DELETE statements' do
|
5
|
+
include Seaquel
|
6
|
+
|
7
|
+
it 'basic' do
|
8
|
+
delete.from(table('bar')).generates <<-SQL
|
9
|
+
DELETE FROM "bar"
|
10
|
+
SQL
|
11
|
+
end
|
12
|
+
it 'initial example from QED' do
|
13
|
+
delete.
|
14
|
+
from(table('baz')).where(column('a').eq(1)).
|
15
|
+
join(table('bar')).on(column('b').eq(column('c'))).generates <<-SQL
|
16
|
+
DELETE FROM "baz" INNER JOIN "bar" ON "b"="c" WHERE "a"=1
|
17
|
+
SQL
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe 'INSERT statements' do
|
5
|
+
include Seaquel
|
6
|
+
|
7
|
+
it 'basic functionality' do
|
8
|
+
insert.into(table('foo')).generates <<-SQL
|
9
|
+
INSERT INTO "foo"
|
10
|
+
SQL
|
11
|
+
end
|
12
|
+
it 'an actual statement' do
|
13
|
+
insert.
|
14
|
+
fields(column('a'), column('b')).
|
15
|
+
values('1', 2).into(table('baz')).
|
16
|
+
generates <<-SQL
|
17
|
+
INSERT INTO "baz" ("a", "b") VALUES ('1', 2)
|
18
|
+
SQL
|
19
|
+
end
|
20
|
+
it 'omitting the fields part' do
|
21
|
+
insert.values(1, 2).into(table('bar')).generates <<-SQL
|
22
|
+
INSERT INTO "bar" VALUES (1, 2)
|
23
|
+
SQL
|
24
|
+
end
|
25
|
+
it 'allows usage of columns linked to a table' do
|
26
|
+
foo = table('foo')
|
27
|
+
insert.into(foo).fields(foo['bar']).values(1).generates <<-SQL
|
28
|
+
INSERT INTO "foo" ("bar") VALUES (1)
|
29
|
+
SQL
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe 'SELECT statements' do
|
5
|
+
include Seaquel
|
6
|
+
|
7
|
+
it 'allows FROM clause' do
|
8
|
+
select.from(table('foo')).generates <<-SQL
|
9
|
+
SELECT * FROM "foo"
|
10
|
+
SQL
|
11
|
+
end
|
12
|
+
it 'allows bare selection' do
|
13
|
+
select.project(1).generates <<-SQL
|
14
|
+
SELECT 1
|
15
|
+
SQL
|
16
|
+
end
|
17
|
+
it 'allows bare selection list' do
|
18
|
+
select.project(1, 2, 3).generates <<-SQL
|
19
|
+
SELECT 1, 2, 3
|
20
|
+
SQL
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#join' do
|
24
|
+
it 'allows joining tables (INNER JOIN being the default)' do
|
25
|
+
foo = table(:foo)
|
26
|
+
bar = table(:bar)
|
27
|
+
select.from(foo).join(bar).on(foo[:id].eq(bar[:foo_id])).generates <<-SQL
|
28
|
+
SELECT * FROM "foo" INNER JOIN "bar" ON "foo"."id"="bar"."foo_id"
|
29
|
+
SQL
|
30
|
+
end
|
31
|
+
end
|
32
|
+
describe '#order_by' do
|
33
|
+
it 'allows ordering the query' do
|
34
|
+
select.order_by(column(:a)).generates <<-SQL
|
35
|
+
SELECT * ORDER BY "a"
|
36
|
+
SQL
|
37
|
+
end
|
38
|
+
it 'overrides previous order_bys' do
|
39
|
+
select.order_by(column(:a)).order_by(column(:b)).generates <<-SQL
|
40
|
+
SELECT * ORDER BY "b"
|
41
|
+
SQL
|
42
|
+
end
|
43
|
+
end
|
44
|
+
describe '#project' do
|
45
|
+
it 'allows expressions in project' do
|
46
|
+
select.project(literal(1).as('one')).generates <<-SQL
|
47
|
+
SELECT 1 AS "one"
|
48
|
+
SQL
|
49
|
+
end
|
50
|
+
it 'overwrites any earlier projects (for flexibility)' do
|
51
|
+
select.project(column('a')).project(column('b')).generates <<-SQL
|
52
|
+
SELECT "b"
|
53
|
+
SQL
|
54
|
+
end
|
55
|
+
end
|
56
|
+
describe '#where' do
|
57
|
+
it 'allows simple literal WHERE clause' do
|
58
|
+
select.where(literal('1=2')).generates <<-SQL
|
59
|
+
SELECT * WHERE 1=2
|
60
|
+
SQL
|
61
|
+
end
|
62
|
+
it 'joins literals with correct precedence' do
|
63
|
+
select.where(literal('1=2'), literal('2=3')).generates <<-SQL
|
64
|
+
SELECT * WHERE 1=2 AND 2=3
|
65
|
+
SQL
|
66
|
+
end
|
67
|
+
it 'allows more complex WHERE clause' do
|
68
|
+
select.where(column('name').eq('baz')).generates <<-SQL
|
69
|
+
SELECT * WHERE "name"='baz'
|
70
|
+
SQL
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'subselects' do
|
74
|
+
it 'are allowed' do
|
75
|
+
sub = select.
|
76
|
+
from(table('foo')).
|
77
|
+
project(column('id')).
|
78
|
+
where(column('count').gt(10))
|
79
|
+
|
80
|
+
select.from(table("bar")).where(column('id').eq(sub)).generates <<-SQL
|
81
|
+
SELECT * FROM "bar" WHERE "id"=(SELECT "id" FROM "foo" WHERE "count">10)
|
82
|
+
SQL
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'multiple where() calls' do
|
87
|
+
it 'are joined using AND' do
|
88
|
+
select.where(true).where(false).generates <<-SQL
|
89
|
+
SELECT * WHERE TRUE AND FALSE
|
90
|
+
SQL
|
91
|
+
end
|
92
|
+
it 'are joined using AND (arglist)' do
|
93
|
+
select.where(true, false).generates <<-SQL
|
94
|
+
SELECT * WHERE TRUE AND FALSE
|
95
|
+
SQL
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe 'UPDATE statements' do
|
5
|
+
include Seaquel
|
6
|
+
|
7
|
+
it 'can be used' do
|
8
|
+
update(table('bar')).set(column('foo').to(immediate(1))).generates <<-SQL
|
9
|
+
UPDATE "bar" SET "foo"=1
|
10
|
+
SQL
|
11
|
+
end
|
12
|
+
it 'set statements can be chained' do
|
13
|
+
update(table('bar')).
|
14
|
+
set(column('foo').to(immediate(1))).
|
15
|
+
set(column('bar').to(immediate(2))).generates <<-SQL
|
16
|
+
UPDATE "bar" SET "foo"=1, "bar"=2
|
17
|
+
SQL
|
18
|
+
end
|
19
|
+
it 'works using the table[column] syntax' do
|
20
|
+
foo = table('foo')
|
21
|
+
update(foo).set(foo['bar'].to(1)).generates <<-SQL
|
22
|
+
UPDATE "foo" SET "bar"=1
|
23
|
+
SQL
|
24
|
+
end
|
25
|
+
it 'allows WHERE clauses' do
|
26
|
+
update(table('foo')).where(column('id').eq(1)).generates <<-SQL
|
27
|
+
UPDATE "foo" WHERE "id"=1
|
28
|
+
SQL
|
29
|
+
end
|
30
|
+
end
|