flounder 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d747ac7404138eb5d0dd5adf3e1a936f70381c84
4
- data.tar.gz: 6186aeaf643c3f6a2e7f42d18bc7d3b903d3038c
3
+ metadata.gz: 97e8380edd41a2232f500fc61ceb3f49e2dd3211
4
+ data.tar.gz: e0330758a5aac5eba5d509cdffa778f0dca41735
5
5
  SHA512:
6
- metadata.gz: 54e0c5140cbef2595cd5b32333166220f233098c8e5f158f8499538009f2f8a306f5fd6fee026297f509b401e2662998aac17e02b6de4a3ffc6b87b1f21f2a75
7
- data.tar.gz: 5bf99fed4005c03b21df75d39adffe4099b83ab032bb207689cdbf249434237db92f577a2927668a15b4a997b0e5061faf8407959d0fe3ed5b9a1deecb491240
6
+ metadata.gz: c17571110ffbe588b794f026e5eddfc244bd479c37d1a09e4f01639caa4e6031bbb82a1ff5512a616fc4c0069cab4c0165eeddcd5a39535275376c8e8dcf5509
7
+ data.tar.gz: 76e6e016a4ac6bcaa9f2c8679909a63cd1564efeed5b5ce970d6a41e845cdc77d43c09cc188e0fbd3e367bff6725c382fae2fa223b07b15ce25c15947766417e
data/HISTORY ADDED
@@ -0,0 +1,7 @@
1
+
2
+ # 0.9
3
+
4
+ + Allows expressions of the form `where('id=$1', 1)`.
5
+ + You now need to call `#kick` when performing an insert or an update
6
+ to get it to really happen. Please see our documentation in qed/*.
7
+ + `domain.transaction do |conn| ... end`
Binary file
data/flounder.gemspec CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "flounder"
5
- s.version = '0.8.1'
5
+ s.version = '0.9.0'
6
6
  s.summary = "Flounder is a way to write SQL simply in Ruby. It deals with everything BUT object relational mapping. "
7
7
  s.email = "kaspar.schiess@technologyastronauts.ch"
8
- s.homepage = "https://bitbucket.org/technologyastronauts/laboratory_flounder"
8
+ s.homepage = "https://bitbucket.org/technologyastronauts/oss_flounder"
9
9
  s.authors = ['Kaspar Schiess', 'Florian Hanke']
10
10
 
11
11
  # s.description = <<-EOF
@@ -6,11 +6,14 @@ module Flounder
6
6
 
7
7
  def initialize pg_conn_args
8
8
  @pg = PG.connect(*pg_conn_args)
9
- @visitor = Arel::Visitors::ToSql.new(self)
9
+ @visitor = Arel::Visitors::PostgreSQL.new(self)
10
10
  end
11
11
 
12
12
  attr_reader :visitor
13
13
 
14
+ def transaction &block
15
+ pg.transaction(&block)
16
+ end
14
17
  def exec *args, &block
15
18
  pg.exec *args, &block
16
19
  end
@@ -42,7 +45,6 @@ module Flounder
42
45
 
43
46
  def primary_key name
44
47
  fail NotImplementedError
45
- @primary_keys[name.to_s]
46
48
  end
47
49
 
48
50
  def table_exists? table_name
@@ -52,7 +54,6 @@ module Flounder
52
54
 
53
55
  def columns name, message = nil
54
56
  fail NotImplementedError
55
- @columns[name.to_s]
56
57
  end
57
58
 
58
59
  def quote_table_name name
@@ -63,25 +64,51 @@ module Flounder
63
64
  pg.quote_ident name.to_s
64
65
  end
65
66
 
66
- def schema_cache
67
+ def schema_cache
67
68
  self
68
69
  end
69
70
 
70
71
  def quote thing, column = nil
72
+ # require 'pp '
71
73
  # p [:quote, thing, column]
74
+ # pp caller.first(10)
72
75
  pg.escape_literal(thing.to_s)
73
76
  end
74
-
77
+
78
+ # ------------------------------------------------------------------------
79
+
80
+ # Turns a PG result row into a hash-like object. There are some transformation
81
+ # rules that govern this conversion:
82
+ #
83
+ # * All data types are converted to their closest Ruby equivalent
84
+ # (type conversion)
85
+ # * Fields from the main entity (the entity that started the select)
86
+ # are returned on the top level of the hash.
87
+ # * Fields from joined entities are returned in a subhash stored under the
88
+ # singular name of the joined entity.
89
+ #
90
+ # Example:
91
+ # row = users.join(posts).on(:id => :user_id).first
92
+ # row[:id] # refers to users.id, also as row.id
93
+ # row[:post][:id] # refers to posts.id, also as row.post.id
94
+ #
95
+ # row.keys # hash keys of the row, not equal to row[:keys]!
96
+ #
97
+ # @param ent [Entity] entity that the query originated from
98
+ # @param result [PG::Result]
99
+ # @param row_idx [Fixnum] row we're interested in
100
+ # @return [Hashie::Mash] result row as hash-like object
101
+ #
75
102
  def objectify_result_row ent, result, row_idx
76
103
  obj = Hashie::Mash.new
77
104
 
78
105
  each_field(ent, result, row_idx) do
79
106
  |entity, name, value, type_oid, binary, idx|
80
- # TODO remove entity resolution from each_field?
81
107
 
82
- # TODO This is currently here to cover the special case of a query.
83
- # Certainly needs refactoring.
84
- #
108
+ # NOTE entity resolution is done both through aliasing and through
109
+ # postgres column reporting. The above entity variable carries what
110
+ # postgres reports to us; the block below resolves aliased entity
111
+ # names:
85
112
  processed_entity, processed_name = yield name if block_given?
86
113
  entity = processed_entity if processed_entity
87
114
  name = processed_name if processed_name
@@ -100,8 +127,9 @@ module Flounder
100
127
  # The main entity and custom fields (AS something) are available on the
101
128
  # top-level of the result.
102
129
  if !entity || entity == ent
103
- warn "#{name.inspect} already defined in result set, aliasing occurs." \
130
+ raise DuplicateField, "#{name.inspect} already defined in result set, aliasing occurs." \
104
131
  if obj.has_key? name
132
+
105
133
  obj[name] = typecast_value
106
134
  end
107
135
  end
@@ -11,18 +11,27 @@ module Flounder
11
11
  }
12
12
  end
13
13
 
14
- Spec = Struct.new(:config)
15
-
14
+ # Checks out a connection from the pool and yields it to the block. The
15
+ # connection is returned to the pool at the end of the block; don't hold
16
+ # on to it.
17
+ #
16
18
  def with_connection
17
19
  @pool.with do |conn|
18
20
  yield conn
19
21
  end
20
22
  end
21
23
 
24
+ # Checks out a connection from the pool. You have to return this
25
+ # connection manually.
26
+ #
22
27
  def checkout
23
28
  @pool.checkout
24
29
  end
25
30
 
31
+ Spec = Struct.new(:config)
32
+
33
+ # This is needed to conform to arels interface.
34
+ #
26
35
  def spec
27
36
  Spec.new(adapter: 'pg')
28
37
  end
@@ -1,4 +1,5 @@
1
1
 
2
+ require 'forwardable'
2
3
  require 'logger'
3
4
 
4
5
  module Flounder
@@ -19,15 +20,33 @@ module Flounder
19
20
 
20
21
  def initialize connection_pool
21
22
  @connection_pool = connection_pool
23
+
24
+ # maps from plural/singular names to entities in this domain
22
25
  @plural = {}
23
26
  @singular = {}
27
+
28
+ # maps OIDs of entities to entities
24
29
  @oids_entity_map = {}
30
+
25
31
  @logger = Logger.new(NilDevice.new)
26
32
  end
27
33
 
28
34
  attr_reader :connection_pool
29
35
  attr_accessor :logger
30
36
 
37
+ extend Forwardable
38
+ def_delegators :connection_pool, :with_connection
39
+
40
+ def transaction &block
41
+ with_connection do |conn|
42
+ conn.transaction do
43
+ block.call(conn)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Returns the entity with the given plural name.
49
+ #
31
50
  def [] name
32
51
  raise NoSuchEntity, "No such entity #{name.inspect} in this domain." \
33
52
  unless @plural.has_key?(name)
@@ -1,4 +1,8 @@
1
1
  module Flounder
2
+
3
+ # Intermediary class that arel wants us to create. Mostly supports the
4
+ # #connection_pool message returning our connection pool.
5
+ #
2
6
  class Engine
3
7
  attr_reader :connection_pool
4
8
  attr_reader :connection
@@ -18,6 +22,10 @@ module Flounder
18
22
 
19
23
  # ---------------------------------------------------- official Engine iface
20
24
 
25
+ # Returns the connection pool.
26
+ #
27
+ # @return [ConnectionPool]
28
+ #
21
29
  def connection_pool
22
30
  @connection_pool
23
31
  end
@@ -1,33 +1,58 @@
1
+ require 'forwardable'
2
+
1
3
  module Flounder
4
+
5
+ # An entity corresponds to a table in the database. On top of its table
6
+ # name, an entity will know its code name (plural) and its singular name,
7
+ # what to call a single row returned from this entity.
8
+ #
9
+ # An entity is not a model. In fact, it is what you need to completely
10
+ # hand-roll your models, without being the model itself.
11
+ #
12
+ # Entities are mainly used to start a query via one of the query initiators.
13
+ # Almost all of the chain methods on the Query object can be used here as
14
+ # well - they will return a query object. Entities, like queries, can be
15
+ # used as enumerables.
16
+ #
17
+ # Example:
18
+ #
19
+ # foo = domain.entity(:plural, :singular, 'table_name')
20
+ #
21
+ # foo.all # SELECT * FROM table_name
22
+ # foo.where(id: 1).first # SELECT * FROM table_name WHERE "table_name"."id" = 1
23
+ #
2
24
  class Entity
3
- # Domain this entity is defined in.
25
+ def initialize domain, plural, singular, table_name
26
+ @domain = domain
27
+ @name = plural
28
+ @singular = singular
29
+ @table_name = table_name
30
+ @columns_hash = nil
31
+
32
+ @table = Arel::Table.new(table_name)
33
+ end
34
+
35
+ # @return [Domain] Domain this entity is defined in.
4
36
  attr_reader :domain
5
37
 
6
- # Name of the entity in plural form.
38
+ # @return [Symbol] Name of the entity in plural form.
7
39
  attr_reader :name
8
40
 
9
41
  # Also, the name is the plural, so we'll support that as well.
10
42
  alias plural name
11
43
 
12
- # Name of the entity in singular form.
44
+ # @return [Symbol] Name of the entity in singular form.
13
45
  attr_reader :singular
14
46
 
15
- # Arel table that underlies this entity.
47
+ # @return [Arel::Table] Arel table that underlies this entity.
16
48
  attr_reader :table
17
- # Name of the table that underlies this entity.
49
+ # @return [String] Name of the table that underlies this entity.
18
50
  attr_reader :table_name
19
51
 
20
- def initialize domain, plural, singular, table_name
21
- @domain = domain
22
- @name = plural
23
- @singular = singular
24
- @table_name = table_name
25
- @columns_hash = nil
26
-
27
- @table = Arel::Table.new(table_name)
28
- end
52
+ extend Forwardable
53
+ def_delegators :domain, :transaction, :with_connection
29
54
 
30
- # Returns a field of the entity.
55
+ # @return [Field] Field with name of the entity.
31
56
  #
32
57
  def [] name
33
58
  Field.new(self, name, table[name])
@@ -37,51 +62,28 @@ module Flounder
37
62
  "entity(#{name}/#{table_name})"
38
63
  end
39
64
 
40
- def query
41
- Query.new(domain, self).tap { |q|
65
+ # Starts a new select query and yields it to the block. Note that you don't
66
+ # need to call this method to obtain a select query - any of the methods
67
+ # on Query::Select should work on the entity and return a new query object.
68
+ #
69
+ # @return [Query::Select]
70
+ #
71
+ def select
72
+ Query::Select.new(domain, self).tap { |q|
42
73
  yield q if block_given?
43
74
  }
44
75
  end
45
76
 
46
77
  def insert hash
47
- Insert.new(domain, self).tap { |i|
48
- yield i if block_given?
49
- i.insert(hash)
50
- }
78
+ Query::Insert.new(domain, self).tap { |q|
79
+ q.row(hash) }
51
80
  end
52
81
 
53
82
  def update hash
54
- Update.new(domain, self).tap { |u|
55
- yield u if block_given?
56
- u.update(hash)
57
- }
83
+ Query::Update.new(domain, self).tap { |u|
84
+ u.set(hash) }
58
85
  end
59
-
60
- # Insert or update.
61
- #
62
- def insdate fields, hash
63
- with_transaction do
64
- found = where(hash.select { |k, _| fields.include?(k) }).first
65
- if found
66
- update(hash).where(:id => found.id).returning
67
- else
68
- result = insert(hash).returning
69
- yield result if block_given?
70
- result
71
- end
72
- end
73
- end
74
-
75
- def with_transaction &block
76
- result = nil
77
- domain.connection_pool.with_connection do |conn|
78
- conn.exec 'START TRANSACTION'
79
- result = block.call
80
- conn.exec 'COMMIT TRANSACTION'
81
- end
82
- result
83
- end
84
-
86
+
85
87
  # Temporarily creates a new entity that is available as if it was declared
86
88
  # with the given plural and singular, but referencing to the same
87
89
  # underlying relation.
@@ -104,14 +106,14 @@ module Flounder
104
106
  # Query initiators
105
107
  [:where, :join, :outer_join, :project, :order_by].each do |name|
106
108
  define_method name do |*args|
107
- query { |q| q.send(name, *args) }
109
+ select { |q| q.send(name, *args) }
108
110
  end
109
111
  end
110
112
 
111
113
  # Kickers
112
114
  [:first, :all, :size, :delete].each do |name|
113
115
  define_method name do |*args|
114
- q = query
116
+ q = select
115
117
  q.send(name, *args)
116
118
  end
117
119
  end
@@ -5,4 +5,12 @@ module Flounder
5
5
  # to a real field on the underlying relation.
6
6
  #
7
7
  class InvalidFieldReference < StandardError; end
8
+
9
+ # Indicates that a where clause with bind variables went out of bounds.
10
+ class BindIndexOutOfBounds < StandardError; end
11
+
12
+ # Exception raised whenever a result set contains two columns with the
13
+ # same name.
14
+ #
15
+ class DuplicateField < StandardError; end
8
16
  end
@@ -6,13 +6,21 @@ module Flounder
6
6
  @arel_field = arel_field
7
7
  end
8
8
 
9
+ # @return [Entity] entity this field belongs to
9
10
  attr_reader :entity
11
+
12
+ # @return [Arel::Attribute] arel attribute that corresponds to this field
10
13
  attr_reader :arel_field
14
+
15
+ # @return [String] name of this field
11
16
  attr_reader :name
12
17
 
18
+ # Returns a fully qualified name (table.field).
19
+ #
20
+ # @return [String] fully qualified field name
21
+ #
13
22
  def fully_qualified_name
14
- # TBD quoting? Demeter?
15
- entity.domain.connection_pool.with_connection do |conn|
23
+ entity.with_connection do |conn|
16
24
  table = conn.quote_table_name(entity.table_name)
17
25
  column = conn.quote_column_name(name)
18
26
  "#{table}.#{column}"
@@ -0,0 +1,157 @@
1
+
2
+ module Flounder::Query
3
+ class Base
4
+ def initialize domain, manager_klass, entity
5
+ @domain = domain
6
+ @engine = Flounder::Engine.new(domain.connection_pool)
7
+ @manager = manager_klass.new(engine)
8
+ @entity = entity
9
+
10
+ @bind_values = []
11
+ end
12
+
13
+ # Domain that this query was issued from.
14
+ attr_reader :domain
15
+ # Database engine that links Arel to Postgres.
16
+ attr_reader :engine
17
+ # Bound values in this query.
18
+ attr_reader :bind_values
19
+ # Arel *Manager that accumulates this query.
20
+ attr_reader :manager
21
+ # Entity this query operates on.
22
+ attr_reader :entity
23
+
24
+ # Restricts the result returned to only those records that match the
25
+ # conditions.
26
+ #
27
+ # Example:
28
+ #
29
+ # query.where(id: 1) # ... WHERE id = 1 ...
30
+ # query.where(:id => :user_id) # ... WHERE id = user_id
31
+ # query.where(:id.noteq => 1) # ... WHERE id != 1
32
+ #
33
+ def where *conditions
34
+ # is this a hash? extract the first element
35
+ if conditions.size == 1 && conditions.first.kind_of?(Hash)
36
+ conditions = conditions.first
37
+
38
+ conditions.each do |k, v|
39
+ manager.where(transform_tuple(k, v))
40
+ end
41
+ return self
42
+ end
43
+
44
+ # maybe conditions is of the second form?
45
+ conditions.each do |cond_str, *values|
46
+ manager.where(
47
+ Arel::Nodes::SqlLiteral.new(
48
+ rewrite_bind_variables(cond_str, bind_values.size, values.size)))
49
+ bind_values.concat values
50
+ end
51
+ end
52
+
53
+ def with name, query
54
+ # Nodes::TableAlias.new(relation, name)
55
+ manager.with(query.manager)
56
+ end
57
+
58
+ # Kickers
59
+ def to_sql
60
+ prepare_kick
61
+
62
+ manager.to_sql.tap { |sql|
63
+ domain.log_sql(sql) }
64
+ end
65
+
66
+ # Returns all rows of the query result as an array. Individual rows are
67
+ # mapped to objects using the row mapper.
68
+ #
69
+ def kick connection=nil
70
+ all = nil
71
+
72
+ (connection || engine).exec(self.to_sql, bind_values) do |result|
73
+ all = Array.new(result.ntuples, nil)
74
+ result.ntuples.times do |row_idx|
75
+ all[row_idx] = engine.connection.
76
+ objectify_result_row(entity, result, row_idx) do |name|
77
+ column_name_to_entity(name)
78
+ end
79
+ end
80
+ end
81
+
82
+ all
83
+ end
84
+ def column_name_to_entity name
85
+ # Implement this if your column names in the query allow inferring
86
+ # the entity and the column name. Return them as a tuple <entity, name>.
87
+ end
88
+
89
+ private
90
+ # Prepares a kick - meaning an execution on the database or a transform
91
+ # to an sql string. Ready Set...
92
+ #
93
+ def prepare_kick
94
+ # should be overridden
95
+ end
96
+
97
+ # Rewrites a statement that contains bind placeholders like '$1' to
98
+ # contain placeholders starting at offset+1. Also checks that no
99
+ # placeholder exceeds the limit.
100
+ #
101
+ def rewrite_bind_variables str, offset, limit
102
+ str.gsub(%r(\$(?<idx>\d+))) do |match|
103
+ idx = Integer($~[:idx])
104
+
105
+ raise Flounder::BindIndexOutOfBounds,
106
+ "Binding to $#{idx} in #{str.inspect}, but only #{limit} variables provided" \
107
+ if idx-1 >= limit
108
+
109
+ "$#{idx + offset}"
110
+ end
111
+ end
112
+
113
+ # Called on each key/value pair of a
114
+ # * condition
115
+ # * join
116
+ # clause, this returns a field that can be passed to Arel
117
+ # * #where
118
+ # * #on
119
+ #
120
+ def transform_tuple field, value
121
+ if value.kind_of? Flounder::Field
122
+ value = value.arel_field
123
+ end
124
+
125
+ case field
126
+ # covers: :field_a => ...
127
+ when Symbol
128
+ join_and_condition_part(entity[field].arel_field, value)
129
+ # covers: entity[:field] => ...
130
+ when Flounder::Field
131
+ join_and_condition_part(field.arel_field, value)
132
+ # covers: :field_a.noteq => ...
133
+ when Flounder::SymbolExtensions::Modifier
134
+ join_and_condition_part(
135
+ field.to_arel_field(entity),
136
+ value,
137
+ field.kind)
138
+ else
139
+ fail "Could not transform condition part. (#{field.inspect}, #{value.inspect})"
140
+ end
141
+ end
142
+ def join_and_condition_part arel_field, value, kind=:eq
143
+ case value
144
+ # covers :field_a => :field_b
145
+ when Symbol
146
+ value_field = entity[value].arel_field
147
+ arel_field.send(kind, value_field)
148
+ # covers: :field => (1..100)
149
+ when Range
150
+ arel_field.in(value)
151
+ else
152
+ arel_field.send(kind, value)
153
+ end
154
+ end
155
+
156
+ end
157
+ end
@@ -1,5 +1,5 @@
1
1
 
2
- module Flounder
2
+ module Flounder::Query
3
3
  # An immmediate string that needs to be passed around and _not_ quoted or
4
4
  # escaped when going to the database.
5
5
  #
@@ -0,0 +1,41 @@
1
+
2
+ require_relative 'base'
3
+ require_relative 'returning'
4
+
5
+ module Flounder::Query
6
+
7
+ # An insert.
8
+ #
9
+ class Insert < Base
10
+ def initialize domain, into_entity
11
+ super domain, Arel::InsertManager, into_entity
12
+
13
+ manager.into entity.table
14
+ end
15
+
16
+ # Add one row to the inserts.
17
+ #
18
+ def row fields
19
+ manager.insert(
20
+ fields.map { |k, v|
21
+ transform_values(k, v) })
22
+ end
23
+
24
+ include Returning
25
+ private
26
+
27
+ # Called on each key/value pair of an insert clause, this returns a
28
+ # hash that can be passed to Arel #insert.
29
+ #
30
+ def transform_values field, value
31
+ case field
32
+ when Symbol, String
33
+ [entity[field.to_sym].arel_field, value]
34
+ when Flounder::Field
35
+ [field.arel_field, value]
36
+ else
37
+ fail "Could not transform value. (#{field.inspect}, #{value.inspect})"
38
+ end
39
+ end
40
+ end # class
41
+ end # module Flounder
@@ -0,0 +1,19 @@
1
+
2
+
3
+ module Flounder::Query
4
+ module Returning
5
+ def returning_fields
6
+ @returning_fields || '*'
7
+ end
8
+
9
+ def returning fields
10
+ @returning_fields = fields
11
+
12
+ self
13
+ end
14
+
15
+ def to_sql
16
+ super << " RETURNING #{returning_fields}"
17
+ end
18
+ end
19
+ end