flounder 0.8.1 → 0.9.0

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.
@@ -1,14 +1,14 @@
1
- module Flounder
1
+
2
+ require_relative 'base'
3
+
4
+ module Flounder::Query
2
5
 
3
6
  # A query obtained by calling any of the chain methods on an entity.
4
7
  #
5
- class Query
8
+ class Select < Base
6
9
  def initialize domain, from_entity
7
- @domain = domain
8
- @from_entity = from_entity
9
- @engine = Engine.new(from_entity.domain.connection_pool)
10
- @manager = Arel::SelectManager.new(@engine)
11
-
10
+ super domain, Arel::SelectManager, from_entity
11
+
12
12
  @has_projection = false
13
13
 
14
14
  @projection_prefixes = Hash.new
@@ -19,15 +19,8 @@ module Flounder
19
19
  manager.from from_entity.table
20
20
  end
21
21
 
22
- # Domain that this query was issued from.
23
- attr_reader :domain
24
- # Entity that this query acts on.
25
- attr_reader :from_entity
26
-
27
22
  # Arel SqlManager that accumulates this query.
28
23
  attr_reader :manager
29
- # Database engine that links Arel to Postgres.
30
- attr_reader :engine
31
24
 
32
25
  # All projected fields if no custom projection is made. Fields are encoded
33
26
  # so that they can be traced back to the entity that contributed them.
@@ -37,13 +30,6 @@ module Flounder
37
30
  # prefix during this query.
38
31
  attr_reader :projection_prefixes
39
32
 
40
- def where conditions
41
- conditions.each do |k, v|
42
- manager.where(transform_hash(k, v))
43
- end
44
- self
45
- end
46
-
47
33
  def _join join_node, entity
48
34
  @last_join = entity
49
35
 
@@ -53,6 +39,7 @@ module Flounder
53
39
 
54
40
  self
55
41
  end
42
+
56
43
  def add_fields_to_default entity
57
44
  prefix = entity.name.to_s
58
45
  table = entity.table
@@ -79,12 +66,12 @@ module Flounder
79
66
  def on join_conditions
80
67
  join_conditions.each do |k, v|
81
68
  manager.on(
82
- transform_hash(k, join_field(v)))
69
+ transform_tuple(k, join_field(v)))
83
70
  end
84
71
  self
85
72
  end
86
73
  def anchor
87
- @from_entity = @last_join
74
+ @entity = @last_join
88
75
  self
89
76
  end
90
77
 
@@ -115,14 +102,7 @@ module Flounder
115
102
  self
116
103
  end
117
104
 
118
- # Kickers
119
- def to_sql
120
- prepare_kick
121
-
122
- manager.to_sql.tap { |sql|
123
- domain.log_sql(sql) }
124
- end
125
- alias sql to_sql
105
+ alias all kick
126
106
 
127
107
  def each &block
128
108
  all.each(&block)
@@ -140,46 +120,20 @@ module Flounder
140
120
 
141
121
  all.first.count
142
122
  end
143
- def delete
144
- # things.where(...).delete.all
145
- #
146
- # Code that I actually want:
147
- #
148
- # @manager = manager.compile_delete
149
- # self
150
- #
151
- # But for now it's a kicker:
152
- #
153
- engine.exec(manager.compile_delete.to_sql)
154
- end
155
123
 
156
- # Returns all rows of the query result as an array. Individual rows are
157
- # mapped to objects using the row mapper.
158
- #
159
- def all
160
- all = nil
161
- engine.exec(sql) do |result|
162
- all = Array.new(result.ntuples, nil)
163
- result.ntuples.times do |row_idx|
164
- all[row_idx] = engine.connection.
165
- objectify_result_row(from_entity, result, row_idx) do |name|
166
- unless default_projection.empty?
167
- extract_source_info_from_name(name)
168
- end
169
- end
170
- end
171
- end
124
+ private
172
125
 
173
- all
126
+ def column_name_to_entity name
127
+ unless default_projection.empty?
128
+ extract_source_info_from_name(name)
129
+ end
174
130
  end
175
-
176
- private
177
131
 
178
132
  # Transforms a simple symbol into either a field of the last .join table,
179
133
  # or respects field values passed in.
180
134
  #
181
135
  def join_field name
182
- return name if name.kind_of? Field
136
+ return name if name.kind_of? Flounder::Field
183
137
  @last_join[name]
184
138
  end
185
139
 
@@ -201,13 +155,14 @@ module Flounder
201
155
  def map_to_field field_ref
202
156
  case field_ref
203
157
  when Symbol
204
- from_entity[field_ref]
158
+ entity[field_ref]
205
159
  when String
206
160
  Immediate.new(field_ref)
207
- when Field
161
+ when Flounder::Field
208
162
  field_ref
209
163
  else
210
- fail InvalidFieldReference, "Cannot resolve #{field_ref.inspect} to a field."
164
+ fail Flounder::InvalidFieldReference,
165
+ "Cannot resolve #{field_ref.inspect} to a field."
211
166
  end
212
167
  end
213
168
 
@@ -251,47 +206,9 @@ module Flounder
251
206
  when Flounder::Field
252
207
  field.fully_qualified_name
253
208
  when Flounder::SymbolExtensions::Modifier
254
- field.to_arel_field(from_entity).send field.kind
255
- else
256
- from_entity[field].arel_field
257
- end
258
- end
259
-
260
- # Called on each key/value pair of a
261
- # * condition
262
- # * join
263
- # clause, this returns a field that can be passed to Arel
264
- # * #where
265
- # * #on
266
- #
267
- def transform_hash field, value
268
- if value.kind_of? Field
269
- value = value.arel_field
270
- end
271
-
272
- case field
273
- when Symbol
274
- join_and_condition_part(from_entity[field].arel_field, value)
275
- when Flounder::Field
276
- join_and_condition_part(field.arel_field, value)
277
- when Flounder::SymbolExtensions::Modifier
278
- join_and_condition_part(
279
- field.to_arel_field(from_entity),
280
- value,
281
- field.kind)
282
- else
283
- fail "Could not transform condition part. (#{field.inspect}, #{value.inspect})"
284
- end
285
- end
286
- def join_and_condition_part arel_field, value, kind=:eq
287
- case value
288
- when Symbol
289
- value_field = from_entity[value].arel_field
290
- arel_field.send(kind, value_field)
291
- when Range
292
- arel_field.in(value)
209
+ field.to_arel_field(entity).send field.kind
293
210
  else
294
- arel_field.send(kind, value)
211
+ entity[field].arel_field
295
212
  end
296
213
  end
297
214
 
@@ -0,0 +1,45 @@
1
+ require_relative 'base'
2
+ require_relative 'returning'
3
+
4
+ module Flounder::Query
5
+
6
+ # An update obtained by calling any of the chain methods on an entity.
7
+ #
8
+ class Update < Base
9
+ def initialize domain, entity
10
+ super(domain, Arel::UpdateManager, entity)
11
+
12
+ manager.table entity.table
13
+ end
14
+
15
+ # Add one row to the updates.
16
+ #
17
+ def set fields
18
+ manager.set(
19
+ fields.map { |k, v|
20
+ transform_attributes(k, v) })
21
+ end
22
+
23
+ include Returning
24
+
25
+ private
26
+
27
+ # Called on each key/value pair of an update clause, this returns a
28
+ # hash that can be passed to Arel #update.
29
+ #
30
+ def transform_attributes field, value
31
+ if value.kind_of? Flounder::Field
32
+ value = value.arel_field
33
+ end
34
+
35
+ case field
36
+ when Symbol, String
37
+ [entity[field.to_sym].arel_field, value]
38
+ when Flounder::Field
39
+ [field.arel_field, value]
40
+ else
41
+ fail "Could not transform condition part. (#{field.inspect}, #{value.inspect})"
42
+ end
43
+ end
44
+ end # class
45
+ end # module Flounder
@@ -13,6 +13,9 @@ module Flounder
13
13
  end
14
14
  end
15
15
 
16
+ # NOTE mixing comparison ops with asc and desc is nasty, but not really
17
+ # relevant. Errors will get raised later on - we're not in the business of
18
+ # checking your SQL for validity.
16
19
  [:not_eq, :lt, :gt, :gteq, :lteq, :matches, :asc, :desc].each do |kind|
17
20
  define_method kind do
18
21
  Modifier.new(self, kind)
data/lib/flounder.rb CHANGED
@@ -12,10 +12,12 @@ require 'flounder/engine'
12
12
  require 'flounder/entity'
13
13
  require 'flounder/entity_alias'
14
14
  require 'flounder/field'
15
- require 'flounder/immediate'
16
- require 'flounder/query'
17
- require 'flounder/insert'
18
- require 'flounder/update'
15
+
16
+ require 'flounder/query/immediate'
17
+ require 'flounder/query/select'
18
+ require 'flounder/query/insert'
19
+
20
+ require 'flounder/query/update'
19
21
  require 'flounder/exceptions'
20
22
 
21
23
  module Flounder
data/qed/applique/ae.rb CHANGED
@@ -2,6 +2,6 @@ require 'ae'
2
2
 
3
3
  def generates_sql(expected)
4
4
  -> (given) {
5
- given.sql.gsub(/^SELECT.*FROM/, 'SELECT [fields] FROM').assert == expected
5
+ given.to_sql.gsub(/^SELECT.*FROM/, 'SELECT [fields] FROM').assert == expected
6
6
  }
7
7
  end
@@ -0,0 +1,33 @@
1
+
2
+ THIS IS A TODO, NOT A FEATURE
3
+
4
+ # Atomic INSERT, then UPDATE
5
+
6
+ As an experimental feature, `Flounder` allows you to combine `INSERT` and `UPDATE` statements into one statement. This is executed as atomically as possible on the database. Two methods are responsible for this bit of magic, both composed of parts of the word 'INSERT' and 'UPDATE': `#insdate` and `#upsert`. We'll expose the difference between these two later on, for now, let's just jump right ahead and use them as intended.
7
+
8
+ For example, let's look at inserting a user only if it doesn't exist on the database yet.
9
+
10
+ ~~~ruby
11
+ # pre = users.count
12
+ #
13
+ # users.insdate(:name, name: 'upsert')
14
+ # users.insdate(:name, name: 'upsert')
15
+ #
16
+ # post = users.count
17
+ # post.assert == pre+1
18
+ ~~~
19
+
20
+ # Intricacies of design
21
+
22
+ Here's a decomposition of what the insdate is behind the scenes, into its parts: It serves as an illustration for turtles all the way down:
23
+ ~~~ruby
24
+ update = users.update(name: 'FooBar').where(name: 'BarBaz')
25
+
26
+ users.insert(name: 'FooBar').
27
+ with(:upsert, update).
28
+ where(:id.not_in => 'upsert.id').
29
+ assert generates_sql(
30
+ %Q(WITH upsert AS ()))
31
+ ~~~
32
+
33
+ Note that the above code does strictly nothing, since Arel (our SQL generator) currently does not support `WITH` for `INSERT`/`UPDATE`. For this reason, we decide not to implement upsert/insdate at this point.
data/qed/conditions.md CHANGED
@@ -5,9 +5,15 @@ A simple use case.
5
5
  s2013.user.id.assert == 1
6
6
  ~~~
7
7
 
8
- # Using strings in where to allow for cases not covered in Flounder/Arel.
9
- #
10
- # ~~~ruby
11
- # posts = domain[:posts].where('id == (?) OR title == "(?)"', 1, "Hello").all
12
- # posts.first.id.assert == 1
13
- # ~~~
8
+ # Complex Conditions
9
+
10
+ Complex conditions can be written in SQL directly. You can use the postgres positional bind syntax to bind values to your conditions safely.
11
+
12
+ ~~~ruby
13
+ query = domain[:posts].where(["id = ($1) OR title = ($2)", 1, "Hello"])
14
+ query.assert generates_sql(
15
+ %Q(SELECT [fields] FROM "posts" WHERE id = ($1) OR title = ($2)))
16
+
17
+ post = query.first
18
+ post.id.assert == 1
19
+ ~~~
data/qed/exceptions.md CHANGED
@@ -9,4 +9,25 @@ All of these statements raise an `InvalidFieldReference` exception.
9
9
  expect Flounder::InvalidFieldReference do
10
10
  users.project(1, 2, 3)
11
11
  end
12
- ~~~
12
+ ~~~
13
+
14
+ # Double projection
15
+
16
+ You cannot have the same field name twice in a result.
17
+
18
+ ~~~ruby
19
+ expect Flounder::DuplicateField do
20
+ users.project('id, id').all
21
+ end
22
+ ~~~
23
+
24
+ # Bind Index out of Bounds
25
+
26
+ Indices of bind expressions in `#where` need to be relative to the argument list given.
27
+
28
+ ~~~ruby
29
+ expect Flounder::BindIndexOutOfBounds do
30
+ users.where('id = $100', 1)
31
+ end
32
+ ~~~
33
+
data/qed/index.md CHANGED
@@ -55,7 +55,12 @@ Fields can be used fully qualified by going through the entity.
55
55
  assert generates_sql(%Q(SELECT [fields] FROM "users" WHERE "users"."id" = 10))
56
56
 
57
57
  domain[:users].where(domain[:users][:name].matches => 'a%').
58
- assert generates_sql(%Q(SELECT [fields] FROM "users" WHERE "users"."name" LIKE 'a%'))
58
+ assert generates_sql(%Q(SELECT [fields] FROM "users" WHERE "users"."name" ILIKE 'a%'))
59
+ ~~~
60
+
61
+ ~~~ruby
62
+ domain[:users].where(:user_id => :approver_id).
63
+ assert generates_sql("SELECT [fields] FROM \"users\" WHERE \"users\".\"user_id\" = \"users\".\"approver_id\"")
59
64
  ~~~
60
65
 
61
66
  # Some JOINs
@@ -90,7 +95,7 @@ So just doing `A.B.C` will give you the first of the above possibilities. Here's
90
95
 
91
96
  The call to `#anchor` anchors all further joins at that point.
92
97
 
93
- # ORDER BY
98
+ # Ordering records
94
99
 
95
100
  ~~~ruby
96
101
  domain[:users].where(id: 2013).order_by(domain[:users][:id]).
@@ -104,5 +109,34 @@ The call to `#anchor` anchors all further joins at that point.
104
109
 
105
110
  ~~~ruby
106
111
  domain[:users].where(id: 2013).project(domain[:users][:id]).
107
- sql.assert == %Q(SELECT "users"."id" FROM "users" WHERE "users"."id" = 2013)
112
+ to_sql.assert == %Q(SELECT "users"."id" FROM "users" WHERE "users"."id" = 2013)
113
+ ~~~
114
+
115
+ # Transactions
116
+
117
+ Transactions are supported on the domain and on entities. Since you need to make sure that your transaction uses only one connection, you will need to handle connections explicitly.
118
+
119
+ ~~~ruby
120
+ expect RuntimeError do
121
+ domain.with_connection do |conn|
122
+ conn.transaction do
123
+ posts.update(title: 'A single title for everyone').kick(conn)
124
+ fail 'rollback'
125
+ end
126
+ end
127
+ end
128
+
129
+ posts.first.title.assert != 'A single title for everyone'
108
130
  ~~~
131
+
132
+ The same works on any entity from the domain. Please note that there is a shortcut for getting at a transaction.
133
+
134
+ ~~~ruby
135
+ domain.transaction do |conn|
136
+ posts.all(conn)
137
+ end
138
+
139
+ posts.transaction do |conn|
140
+ posts.all(conn)
141
+ end
142
+ ~~~
data/qed/inserts.md CHANGED
@@ -3,13 +3,13 @@ An insert creates a state from which SQL can be extracted.
3
3
  ~~~ruby
4
4
  sql = users.insert(:name => 'Mr. Insert SQL').to_sql
5
5
 
6
- sql.assert == "INSERT INTO \"users\" (\"name\") VALUES ('Mr. Insert SQL')"
6
+ sql.assert == "INSERT INTO \"users\" (\"name\") VALUES ('Mr. Insert SQL') RETURNING *"
7
7
  ~~~
8
8
 
9
9
  Using returning will return the inserted object.
10
10
 
11
11
  ~~~ruby
12
- results = users.insert(:name => 'Mr. Returning Asterisk').returning
12
+ results = users.insert(:name => 'Mr. Returning Asterisk').kick
13
13
 
14
14
  results.first.name.assert == 'Mr. Returning Asterisk'
15
15
  ~~~
@@ -17,7 +17,7 @@ Using returning will return the inserted object.
17
17
  Returning all fields is the default, but you can provide that explicitly.
18
18
 
19
19
  ~~~ruby
20
- results = users.insert(:name => 'Mr. Returning Asterisk').returning '*'
20
+ results = users.insert(:name => 'Mr. Returning Asterisk').returning('*').kick
21
21
 
22
22
  results.first.name.assert == 'Mr. Returning Asterisk'
23
23
  ~~~
@@ -25,7 +25,7 @@ Returning all fields is the default, but you can provide that explicitly.
25
25
  Using returning with a field name will return those fields of the inserted object.
26
26
 
27
27
  ~~~ruby
28
- results = users.insert(:name => 'Mr. Returning').returning(:id)
28
+ results = users.insert(:name => 'Mr. Returning').returning(:id).kick
29
29
 
30
30
  id = results.first.id
31
31
 
@@ -35,7 +35,7 @@ Using returning with a field name will return those fields of the inserted objec
35
35
  Flounder fields can be used as keys.
36
36
 
37
37
  ~~~ruby
38
- results = users.insert(users[:name] => 'Mr. Flounder Field').returning
38
+ results = users.insert(users[:name] => 'Mr. Flounder Field').kick
39
39
 
40
40
  results.first.name.assert == 'Mr. Flounder Field'
41
41
  ~~~
data/qed/ordering.md CHANGED
@@ -17,9 +17,9 @@ They can be ordered ASC.
17
17
  They can also be ordered DESC.
18
18
 
19
19
  ~~~ruby
20
- sql = domain[:posts].order_by(:title.desc).to_sql
21
-
22
- sql.assert == 'SELECT "posts"."id" AS _posts_id, "posts"."title" AS _posts_title, "posts"."text" AS _posts_text, "posts"."user_id" AS _posts_user_id, "posts"."approver_id" AS _posts_approver_id FROM "posts" ORDER BY "posts"."title" DESC'
20
+ domain[:posts].order_by(:title.desc).
21
+ assert generates_sql(
22
+ 'SELECT [fields] FROM "posts" ORDER BY "posts"."title" DESC')
23
23
  ~~~
24
24
 
25
25
  Multiple fields can be used.
@@ -43,15 +43,22 @@ Ordering can be defined in many ways.
43
43
  ~~~ruby
44
44
  user = users.first
45
45
 
46
- inserted = posts.insert(:title => 'ABC', :text => 'Alphabet', :user_id => user.id).returning '*'
47
-
48
- domain[:posts].order_by(:title).map(&:title).assert == ['ABC', 'First Light']
49
- domain[:posts].order_by(:title.asc).map(&:title).assert == ['ABC', 'First Light']
50
- domain[:posts].order_by(:title.desc).map(&:title).assert == ['First Light', 'ABC']
51
- domain[:posts].order_by('title').map(&:title).assert == ['ABC', 'First Light']
52
- domain[:posts].order_by('title ASC').map(&:title).assert == ['ABC', 'First Light']
53
- domain[:posts].order_by('title DESC').map(&:title).assert == ['First Light', 'ABC']
54
- domain[:posts].order_by(posts[:title]).map(&:title).assert == ['ABC', 'First Light']
55
- domain[:posts].order_by(posts[:title].asc).map(&:title).assert == ['ABC', 'First Light']
56
- domain[:posts].order_by(posts[:title].desc).map(&:title).assert == ['First Light', 'ABC']
46
+ inserted = posts.insert(
47
+ title: 'ABC', text: 'Alphabet', user_id: user.id).kick
48
+
49
+ def titles_by *args
50
+ domain[:posts].order_by(*args).map(&:title)
51
+ end
52
+
53
+ titles_by(:title).assert == ['ABC', 'First Light']
54
+ titles_by(:title.asc).assert == ['ABC', 'First Light']
55
+ titles_by(:title.desc).assert == ['First Light', 'ABC']
56
+
57
+ titles_by('title').assert == ['ABC', 'First Light']
58
+ titles_by('title ASC').assert == ['ABC', 'First Light']
59
+ titles_by('title DESC').assert == ['First Light', 'ABC']
60
+
61
+ titles_by(posts[:title]).assert == ['ABC', 'First Light']
62
+ titles_by(posts[:title].asc).assert == ['ABC', 'First Light']
63
+ titles_by(posts[:title].desc).assert == ['First Light', 'ABC']
57
64
  ~~~
data/qed/projection.md CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  Without projection, result objects will be packaged in a subhash.
2
3
 
3
4
  ~~~ruby
data/qed/updates.md CHANGED
@@ -5,7 +5,7 @@ An update creates a state from which SQL can be extracted.
5
5
 
6
6
  sql = posts.update(:title => 'Update SQL').where(:id => post.id).to_sql
7
7
 
8
- sql.assert == 'UPDATE "posts" SET "title" = \'Update SQL\' WHERE "posts"."id" = 1'
8
+ sql.assert == 'UPDATE "posts" SET "title" = \'Update SQL\' WHERE "posts"."id" = 1 RETURNING *'
9
9
  ~~~
10
10
 
11
11
  Flounder fields are ok.
@@ -13,17 +13,20 @@ Flounder fields are ok.
13
13
  ~~~ruby
14
14
  post = posts.first
15
15
 
16
- sql = posts.update(posts[:title] => 'Update Flounder SQL').where(:id => post.id).to_sql
16
+ sql = posts.
17
+ update(posts[:title] => 'Update Flounder SQL').
18
+ where(:id => post.id).to_sql
17
19
 
18
- sql.assert == 'UPDATE "posts" SET "title" = \'Update Flounder SQL\' WHERE "posts"."id" = 1'
20
+ sql.assert == 'UPDATE "posts" SET "title" = \'Update Flounder SQL\' WHERE "posts"."id" = 1 RETURNING *'
19
21
  ~~~
20
22
 
21
23
  It can update a single field.
22
24
 
23
25
  ~~~ruby
24
- post = posts.first
25
-
26
- post = posts.update(:title => 'Update Field').where(:id => post.id).returning.first
26
+ post_id = posts.first.id
27
+ post = posts.
28
+ update(title: 'Update Field').
29
+ where(id: post_id).kick.first
27
30
 
28
31
  post.title.assert == 'Update Field'
29
32
  ~~~
@@ -33,7 +36,9 @@ Flounder fields are ok here too.
33
36
  ~~~ruby
34
37
  post = posts.first
35
38
 
36
- post = posts.update(posts[:title] => 'Update Flounder Field').where(:id => post.id).returning.first
39
+ post = posts.
40
+ update(posts[:title] => 'Update Flounder Field').
41
+ where(:id => post.id).kick.first
37
42
 
38
43
  post.title.assert == 'Update Flounder Field'
39
44
  ~~~
@@ -45,7 +50,7 @@ An update can take multiple fields.
45
50
 
46
51
  sql = posts.update(:title => 'Update SQL', :text => 'Update Multiple Fields Text').where(:id => post.id).to_sql
47
52
 
48
- sql.assert == 'UPDATE "posts" SET "title" = \'Update SQL\', "text" = \'Update Multiple Fields Text\' WHERE "posts"."id" = 1'
53
+ sql.assert == 'UPDATE "posts" SET "title" = \'Update SQL\', "text" = \'Update Multiple Fields Text\' WHERE "posts"."id" = 1 RETURNING *'
49
54
  ~~~
50
55
 
51
56
  Updating a single row is possible.
@@ -53,7 +58,7 @@ Updating a single row is possible.
53
58
  ~~~ruby
54
59
  post = posts.first
55
60
 
56
- post = posts.update(:title => 'Updated Title', :text => 'Update Single Row Possible').where(:id => post.id).returning.first
61
+ post = posts.update(:title => 'Updated Title', :text => 'Update Single Row Possible').where(:id => post.id).kick.first
57
62
 
58
63
  post.title.assert == 'Updated Title'
59
64
  post.text.assert == 'Update Single Row Possible'
@@ -62,7 +67,7 @@ Updating a single row is possible.
62
67
  Updating multiple rows is possible.
63
68
 
64
69
  ~~~ruby
65
- updated = users.update(:name => 'Update Multiple Rows').where(:name.not_eq => nil).returning
70
+ updated = users.update(:name => 'Update Multiple Rows').where(:name.not_eq => nil).kick
66
71
 
67
72
  updated.map(&:name).assert == ['Update Multiple Rows']*6
68
73
  ~~~