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,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
@@ -0,0 +1 @@
1
+ require 'ae'
@@ -0,0 +1,2 @@
1
+
2
+ require 'seaquel'
@@ -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