flounder 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +33 -0
- data/flounder.gemspec +2 -2
- data/lib/flounder/connection.rb +36 -7
- data/lib/flounder/domain.rb +10 -3
- data/lib/flounder/entity.rb +48 -6
- data/lib/flounder/entity_alias.rb +39 -0
- data/lib/flounder/exceptions.rb +8 -0
- data/lib/flounder/field.rb +8 -4
- data/lib/flounder/immediate.rb +18 -0
- data/lib/flounder/insert.rb +73 -0
- data/lib/flounder/postgres_utils.rb +8 -5
- data/lib/flounder/query.rb +132 -53
- data/lib/flounder/update.rb +114 -0
- data/lib/flounder.rb +5 -0
- data/qed/applique/ae.rb +7 -1
- data/qed/applique/{ahrel.rb → flounder.rb} +0 -0
- data/qed/applique/setup_domain.rb +27 -0
- data/qed/exceptions.md +12 -0
- data/qed/flounder.sql +6 -4
- data/qed/index.md +30 -27
- data/qed/inserts.md +43 -0
- data/qed/results.md +21 -0
- data/qed/selects.md +10 -6
- data/qed/updates.md +68 -0
- metadata +22 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9860bdc14eff074ea0e340f278593cc4e489599b
|
4
|
+
data.tar.gz: 830bccfe9f8204a93e9eacb9c72b3b53191f82fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c93426c7de1ac2a10368dcf8f85cdd269a20d00f71229706bbd2a42ff928d2415bf157217d52167c7d2e308a6dd59cb50ad505cacf04f4425eb7210e5380d94
|
7
|
+
data.tar.gz: 4799ae63203ad83d6059c87efcb5c86619a054628591594e5c5cae93a10d3d1156136481b4387b4ddffb6defb0715d66d9ef31c3d01146435ecd6829973e16d0
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
flounder (0.2.0)
|
5
|
+
arel (~> 5, > 5.0.1)
|
6
|
+
connection_pool (~> 2)
|
7
|
+
hashie (~> 3, >= 3.2)
|
8
|
+
pg (> 0.17)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
ae (1.8.2)
|
14
|
+
ansi
|
15
|
+
ansi (1.4.3)
|
16
|
+
arel (5.0.1.20140414130214)
|
17
|
+
brass (1.2.1)
|
18
|
+
connection_pool (2.0.0)
|
19
|
+
facets (2.9.3)
|
20
|
+
hashie (3.2.0)
|
21
|
+
pg (0.17.1)
|
22
|
+
qed (2.9.1)
|
23
|
+
ansi
|
24
|
+
brass
|
25
|
+
facets (>= 2.8)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
ruby
|
29
|
+
|
30
|
+
DEPENDENCIES
|
31
|
+
ae
|
32
|
+
flounder!
|
33
|
+
qed
|
data/flounder.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = "flounder"
|
5
|
-
s.version = '0.
|
5
|
+
s.version = '0.4.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
8
|
s.homepage = "https://bitbucket.org/technologyastronauts/laboratory_flounder"
|
9
|
-
s.authors = ['Kaspar Schiess']
|
9
|
+
s.authors = ['Kaspar Schiess', 'Florian Hanke']
|
10
10
|
|
11
11
|
# s.description = <<-EOF
|
12
12
|
# EOF
|
data/lib/flounder/connection.rb
CHANGED
@@ -16,10 +16,6 @@ module Flounder
|
|
16
16
|
end
|
17
17
|
|
18
18
|
# ------------------------------------------------ official Connection iface
|
19
|
-
def schema_cache
|
20
|
-
self
|
21
|
-
end
|
22
|
-
|
23
19
|
Column = Struct.new(:name, :type)
|
24
20
|
def columns_hash table_name
|
25
21
|
hash = {}
|
@@ -40,12 +36,12 @@ module Flounder
|
|
40
36
|
hash[name] = Column.new(name, typesym)
|
41
37
|
end
|
42
38
|
end
|
43
|
-
|
39
|
+
|
44
40
|
hash
|
45
41
|
end
|
46
42
|
|
47
43
|
def primary_key name
|
48
|
-
fail
|
44
|
+
fail NotImplementedError
|
49
45
|
@primary_keys[name.to_s]
|
50
46
|
end
|
51
47
|
|
@@ -55,7 +51,7 @@ module Flounder
|
|
55
51
|
end
|
56
52
|
|
57
53
|
def columns name, message = nil
|
58
|
-
fail
|
54
|
+
fail NotImplementedError
|
59
55
|
@columns[name.to_s]
|
60
56
|
end
|
61
57
|
|
@@ -75,6 +71,39 @@ module Flounder
|
|
75
71
|
# p [:quote, thing, column]
|
76
72
|
pg.escape_literal(thing.to_s)
|
77
73
|
end
|
74
|
+
|
75
|
+
def objectify_result_row ent, result, row_idx
|
76
|
+
obj = Hashie::Mash.new
|
77
|
+
|
78
|
+
each_field(ent, result, row_idx) do
|
79
|
+
|entity, name, value, type_oid, binary, idx|
|
80
|
+
# TODO remove entity resolution from each_field?
|
81
|
+
|
82
|
+
entity, name = yield name if block_given?
|
83
|
+
|
84
|
+
typecast_value = typecast(type_oid, value)
|
85
|
+
|
86
|
+
# JOIN tables are available from the result using their singular
|
87
|
+
# names.
|
88
|
+
if entity
|
89
|
+
obj[entity.singular] ||= {}
|
90
|
+
|
91
|
+
sub_obj = obj[entity.singular]
|
92
|
+
sub_obj[name] = typecast_value
|
93
|
+
end
|
94
|
+
|
95
|
+
# The main entity and custom fields (AS something) are available on the
|
96
|
+
# top-level of the result.
|
97
|
+
if !entity || entity == ent
|
98
|
+
warn "#{name.inspect} already defined in result set, aliasing occurs." \
|
99
|
+
if obj.has_key? name
|
100
|
+
obj[name] = typecast_value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
return obj
|
105
|
+
end
|
106
|
+
|
78
107
|
private
|
79
108
|
include PostgresUtils
|
80
109
|
end
|
data/lib/flounder/domain.rb
CHANGED
@@ -7,9 +7,6 @@ module Flounder
|
|
7
7
|
end
|
8
8
|
|
9
9
|
class Domain
|
10
|
-
attr_reader :connection_pool
|
11
|
-
attr_accessor :logger
|
12
|
-
|
13
10
|
# A device that discards all logging made to it. This is used to be silent
|
14
11
|
# by default while still allowing the logging of all queries.
|
15
12
|
#
|
@@ -27,6 +24,10 @@ module Flounder
|
|
27
24
|
@oids_entity_map = {}
|
28
25
|
@logger = Logger.new(NilDevice.new)
|
29
26
|
end
|
27
|
+
|
28
|
+
attr_reader :connection_pool
|
29
|
+
attr_accessor :logger
|
30
|
+
|
30
31
|
def [] name
|
31
32
|
raise NoSuchEntity, "No such entity #{name.inspect} in this domain." \
|
32
33
|
unless @plural.has_key?(name)
|
@@ -34,6 +35,12 @@ module Flounder
|
|
34
35
|
@plural.fetch(name)
|
35
36
|
end
|
36
37
|
|
38
|
+
# Returns all entities as an array.
|
39
|
+
#
|
40
|
+
def entities
|
41
|
+
@plural.values
|
42
|
+
end
|
43
|
+
|
37
44
|
# Logs sql statements that are prepared for execution.
|
38
45
|
#
|
39
46
|
def log_sql sql
|
data/lib/flounder/entity.rb
CHANGED
@@ -5,6 +5,10 @@ module Flounder
|
|
5
5
|
|
6
6
|
# Name of the entity in plural form.
|
7
7
|
attr_reader :name
|
8
|
+
|
9
|
+
# Also, the name is the plural, so we'll support that as well.
|
10
|
+
alias plural name
|
11
|
+
|
8
12
|
# Name of the entity in singular form.
|
9
13
|
attr_reader :singular
|
10
14
|
|
@@ -18,6 +22,8 @@ module Flounder
|
|
18
22
|
@name = plural
|
19
23
|
@singular = singular
|
20
24
|
@table_name = table_name
|
25
|
+
@columns_hash = nil
|
26
|
+
|
21
27
|
@table = Arel::Table.new(table_name)
|
22
28
|
end
|
23
29
|
|
@@ -27,24 +33,60 @@ module Flounder
|
|
27
33
|
Field.new(self, name, table[name])
|
28
34
|
end
|
29
35
|
|
30
|
-
def belongs_to entity, fk
|
31
|
-
end
|
32
|
-
|
33
36
|
def to_s
|
34
37
|
"entity(#{name}/#{table_name})"
|
35
38
|
end
|
36
39
|
|
40
|
+
def query
|
41
|
+
Query.new(domain, self).tap { |q|
|
42
|
+
yield q if block_given?
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def insert hash
|
47
|
+
Insert.new(domain, self).tap { |i|
|
48
|
+
yield i if block_given?
|
49
|
+
i.insert(hash)
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def update hash
|
54
|
+
Update.new(domain, self).tap { |u|
|
55
|
+
yield u if block_given?
|
56
|
+
u.update(hash)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Temporarily creates a new entity that is available as if it was declared
|
61
|
+
# with the given plural and singular, but referencing to the same
|
62
|
+
# underlying relation.
|
63
|
+
#
|
64
|
+
def as plural, singular
|
65
|
+
EntityAlias.new(self, plural, singular)
|
66
|
+
end
|
67
|
+
|
68
|
+
def columns_hash
|
69
|
+
@columns_hash ||= begin
|
70
|
+
domain.connection_pool.with_connection do |conn|
|
71
|
+
conn.columns_hash(table_name)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
def column_names
|
76
|
+
columns_hash.keys
|
77
|
+
end
|
78
|
+
|
37
79
|
# Query initiators
|
38
|
-
[:where, :join, :outer_join, :project].each do |name|
|
80
|
+
[:where, :join, :outer_join, :project, :order_by].each do |name|
|
39
81
|
define_method name do |*args|
|
40
|
-
|
82
|
+
query { |q| q.send(name, *args) }
|
41
83
|
end
|
42
84
|
end
|
43
85
|
|
44
86
|
# Kickers
|
45
87
|
[:first, :all, :size].each do |name|
|
46
88
|
define_method name do |*args|
|
47
|
-
q =
|
89
|
+
q = query
|
48
90
|
q.send(name, *args)
|
49
91
|
end
|
50
92
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
module Flounder
|
3
|
+
# Alias for an Entity, implementing roughly the same interface.
|
4
|
+
#
|
5
|
+
# @see Entity
|
6
|
+
#
|
7
|
+
class EntityAlias
|
8
|
+
def initialize entity, plural, singular
|
9
|
+
@entity = entity
|
10
|
+
@plural = plural
|
11
|
+
@singular = singular
|
12
|
+
end
|
13
|
+
|
14
|
+
# Entity this alias refers to
|
15
|
+
attr_reader :entity
|
16
|
+
|
17
|
+
# Plural name of the alias
|
18
|
+
attr_reader :plural
|
19
|
+
|
20
|
+
# Singular name of the alias
|
21
|
+
attr_reader :singular
|
22
|
+
|
23
|
+
# Plural is also available as #name
|
24
|
+
alias name plural
|
25
|
+
|
26
|
+
def table
|
27
|
+
table = entity.table
|
28
|
+
table.alias(plural)
|
29
|
+
end
|
30
|
+
|
31
|
+
def column_names
|
32
|
+
entity.column_names
|
33
|
+
end
|
34
|
+
|
35
|
+
def [] name
|
36
|
+
Field.new(self, name, table[name])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/flounder/field.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
module Flounder
|
2
2
|
class Field
|
3
|
-
attr_reader :entity
|
4
|
-
attr_reader :arel_field
|
5
|
-
attr_reader :name
|
6
|
-
|
7
3
|
def initialize entity, name, arel_field
|
8
4
|
@entity = entity
|
9
5
|
@name = name
|
10
6
|
@arel_field = arel_field
|
11
7
|
end
|
12
8
|
|
9
|
+
attr_reader :entity
|
10
|
+
attr_reader :arel_field
|
11
|
+
attr_reader :name
|
12
|
+
|
13
13
|
def fully_qualified_name
|
14
14
|
# TBD quoting? Demeter?
|
15
15
|
entity.domain.connection_pool.with_connection do |conn|
|
@@ -19,6 +19,10 @@ module Flounder
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
def to_arel_field
|
23
|
+
arel_field
|
24
|
+
end
|
25
|
+
|
22
26
|
include SymbolExtensions
|
23
27
|
end
|
24
28
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
module Flounder
|
3
|
+
# An immmediate string that needs to be passed around and _not_ quoted or
|
4
|
+
# escaped when going to the database.
|
5
|
+
#
|
6
|
+
# @private
|
7
|
+
class Immediate
|
8
|
+
def initialize string
|
9
|
+
@string = string
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :string
|
13
|
+
|
14
|
+
def to_arel_field
|
15
|
+
string
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Flounder
|
2
|
+
|
3
|
+
# An insert.
|
4
|
+
#
|
5
|
+
class Insert
|
6
|
+
def initialize domain, into_entity
|
7
|
+
@domain = domain
|
8
|
+
@into_entity = into_entity
|
9
|
+
@engine = Engine.new(into_entity.domain.connection_pool)
|
10
|
+
@manager = Arel::InsertManager.new(@engine)
|
11
|
+
|
12
|
+
manager.into into_entity.table
|
13
|
+
end
|
14
|
+
|
15
|
+
# Domain that this insert was issued for.
|
16
|
+
attr_reader :domain
|
17
|
+
# Entity that this insert acts on.
|
18
|
+
attr_reader :into_entity
|
19
|
+
|
20
|
+
# Arel SqlManager that accumulates this insert.
|
21
|
+
attr_reader :manager
|
22
|
+
# Database engine that links Arel to Postgres.
|
23
|
+
attr_reader :engine
|
24
|
+
|
25
|
+
# Add one row to the inserts.
|
26
|
+
#
|
27
|
+
def insert fields
|
28
|
+
manager.insert fields.map { |k, v| transform_hash_keys(k, v) }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Kickers
|
32
|
+
def to_sql
|
33
|
+
manager.to_sql.tap { |sql|
|
34
|
+
domain.log_sql(sql) }
|
35
|
+
end
|
36
|
+
alias sql to_sql
|
37
|
+
|
38
|
+
# Returns all rows of the insert result as an array. Individual rows are
|
39
|
+
# mapped to objects using the row mapper.
|
40
|
+
#
|
41
|
+
def returning fields = '*'
|
42
|
+
inserted = []
|
43
|
+
engine.exec(sql + " RETURNING #{fields}") do |result|
|
44
|
+
inserted = Array.new(result.ntuples, nil)
|
45
|
+
result.ntuples.times do |row_idx|
|
46
|
+
inserted[row_idx] = engine.connection.
|
47
|
+
objectify_result_row(into_entity, result, row_idx)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
inserted
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Called on each key/value pair of an insert clause, this returns a
|
56
|
+
# hash that can be passed to Arel #insert.
|
57
|
+
#
|
58
|
+
def transform_hash_keys field, value
|
59
|
+
if value.kind_of? Field
|
60
|
+
value = value.arel_field
|
61
|
+
end
|
62
|
+
|
63
|
+
case field
|
64
|
+
when Symbol
|
65
|
+
[into_entity[field].arel_field, value]
|
66
|
+
when Flounder::Field
|
67
|
+
[field.arel_field, value]
|
68
|
+
else
|
69
|
+
fail "Could not transform condition part. (#{field.inspect}, #{value.inspect})"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end # class
|
73
|
+
end # module Flounder
|
@@ -4,10 +4,11 @@ require 'date'
|
|
4
4
|
|
5
5
|
module Flounder
|
6
6
|
module PostgresUtils
|
7
|
-
|
7
|
+
OID_BOOLEAN = 16
|
8
8
|
OID_SMALLINT = 21
|
9
|
+
OID_INTEGER = 23
|
10
|
+
OID_TEXT = 25
|
9
11
|
OID_VARCHAR = 1043
|
10
|
-
OID_BOOLEAN = 16
|
11
12
|
OID_TIMESTAMP = 1114
|
12
13
|
OID_DATE = 1082
|
13
14
|
OID_TIME = 1083
|
@@ -25,6 +26,8 @@ module Flounder
|
|
25
26
|
value.to_i
|
26
27
|
when OID_BOOLEAN
|
27
28
|
value == 't'
|
29
|
+
when OID_TEXT
|
30
|
+
value.to_s
|
28
31
|
else
|
29
32
|
value
|
30
33
|
end
|
@@ -40,7 +43,7 @@ module Flounder
|
|
40
43
|
:time
|
41
44
|
when OID_INTEGER, OID_SMALLINT
|
42
45
|
:integer
|
43
|
-
when OID_VARCHAR
|
46
|
+
when OID_VARCHAR, OID_TEXT
|
44
47
|
:string
|
45
48
|
when OID_BOOLEAN
|
46
49
|
:boolean
|
@@ -49,8 +52,8 @@ module Flounder
|
|
49
52
|
end
|
50
53
|
end
|
51
54
|
|
52
|
-
def each_field
|
53
|
-
domain =
|
55
|
+
def each_field entity, result, row_idx
|
56
|
+
domain = entity.domain
|
54
57
|
|
55
58
|
result.nfields.times do |field_idx|
|
56
59
|
table_oid = result.ftable(field_idx)
|
data/lib/flounder/query.rb
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
module Flounder
|
2
|
+
|
3
|
+
# A query obtained by calling any of the chain methods on an entity.
|
4
|
+
#
|
2
5
|
class Query
|
6
|
+
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
|
+
|
12
|
+
@has_projection = false
|
13
|
+
|
14
|
+
@projection_prefixes = Hash.new
|
15
|
+
@default_projection = []
|
16
|
+
|
17
|
+
add_fields_to_default from_entity
|
18
|
+
|
19
|
+
manager.from from_entity.table
|
20
|
+
end
|
21
|
+
|
3
22
|
# Domain that this query was issued from.
|
4
23
|
attr_reader :domain
|
5
24
|
# Entity that this query acts on.
|
@@ -9,17 +28,14 @@ module Flounder
|
|
9
28
|
attr_reader :manager
|
10
29
|
# Database engine that links Arel to Postgres.
|
11
30
|
attr_reader :engine
|
12
|
-
|
13
31
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@engine = Engine.new(from_entity.domain.connection_pool)
|
18
|
-
@manager = Arel::SelectManager.new(@engine)
|
19
|
-
@has_projection = false
|
32
|
+
# All projected fields if no custom projection is made. Fields are encoded
|
33
|
+
# so that they can be traced back to the entity that contributed them.
|
34
|
+
attr_reader :default_projection
|
20
35
|
|
21
|
-
|
22
|
-
|
36
|
+
# Each projection has a unique prefix mapping to the entity that uses this
|
37
|
+
# prefix during this query.
|
38
|
+
attr_reader :projection_prefixes
|
23
39
|
|
24
40
|
def where conditions={}
|
25
41
|
conditions.each do |k, v|
|
@@ -28,15 +44,38 @@ module Flounder
|
|
28
44
|
self
|
29
45
|
end
|
30
46
|
|
31
|
-
def
|
47
|
+
def _join join_node, entity
|
32
48
|
@last_join = entity
|
33
49
|
|
34
|
-
|
50
|
+
table = entity.table
|
51
|
+
manager.join(table, join_node)
|
52
|
+
add_fields_to_default(entity)
|
53
|
+
|
35
54
|
self
|
36
55
|
end
|
37
|
-
def
|
38
|
-
|
56
|
+
def add_fields_to_default entity
|
57
|
+
prefix = entity.name.to_s
|
58
|
+
table = entity.table
|
59
|
+
|
60
|
+
warn "Table alias #{prefix} already used in select; field aliasing will occur!" \
|
61
|
+
if projection_prefixes.has_key? prefix
|
62
|
+
|
63
|
+
projection_prefixes[prefix] = entity
|
64
|
+
|
65
|
+
entity.column_names.each do |name|
|
66
|
+
default_projection << table[name].as("_#{prefix}_#{name}")
|
67
|
+
end
|
39
68
|
end
|
69
|
+
|
70
|
+
def inner_join *args
|
71
|
+
_join(Arel::Nodes::InnerJoin, *args)
|
72
|
+
end
|
73
|
+
alias join inner_join
|
74
|
+
|
75
|
+
def outer_join *args
|
76
|
+
_join(Arel::Nodes::OuterJoin, *args)
|
77
|
+
end
|
78
|
+
|
40
79
|
def on join_conditions
|
41
80
|
join_conditions.each do |k, v|
|
42
81
|
manager.on(
|
@@ -49,9 +88,14 @@ module Flounder
|
|
49
88
|
self
|
50
89
|
end
|
51
90
|
|
91
|
+
# Adds a field to the projection clause of the SQL statement (the part
|
92
|
+
# between SELECT and FROM). Projection of '*' is the default, so you can
|
93
|
+
# omit this call entirely if you want that.
|
94
|
+
#
|
52
95
|
def project *field_list
|
53
|
-
# TBD: Clean up
|
54
96
|
@has_projection = true
|
97
|
+
@default_projection = {}
|
98
|
+
|
55
99
|
manager.project *map_to_arel(field_list)
|
56
100
|
self
|
57
101
|
end
|
@@ -61,15 +105,9 @@ module Flounder
|
|
61
105
|
self
|
62
106
|
end
|
63
107
|
|
64
|
-
#
|
65
|
-
# or respects field values passed in.
|
108
|
+
# Orders by a list of field references.
|
66
109
|
#
|
67
|
-
def
|
68
|
-
return name if name.kind_of? Field
|
69
|
-
@last_join[name]
|
70
|
-
end
|
71
|
-
|
72
|
-
def order *field_list
|
110
|
+
def order_by *field_list
|
73
111
|
field_list.each do |field|
|
74
112
|
field = from_entity[field] unless field.kind_of?(Field)
|
75
113
|
manager.order field.fully_qualified_name
|
@@ -80,7 +118,7 @@ module Flounder
|
|
80
118
|
# Kickers
|
81
119
|
def to_sql
|
82
120
|
prepare_kick
|
83
|
-
|
121
|
+
|
84
122
|
manager.to_sql.tap { |sql|
|
85
123
|
domain.log_sql(sql) }
|
86
124
|
end
|
@@ -111,7 +149,12 @@ module Flounder
|
|
111
149
|
engine.exec(sql) do |result|
|
112
150
|
all = Array.new(result.ntuples, nil)
|
113
151
|
result.ntuples.times do |row_idx|
|
114
|
-
all[row_idx] =
|
152
|
+
all[row_idx] = engine.connection.
|
153
|
+
objectify_result_row(from_entity, result, row_idx) do |name|
|
154
|
+
unless default_projection.empty?
|
155
|
+
extract_source_info_from_name(name)
|
156
|
+
end
|
157
|
+
end
|
115
158
|
end
|
116
159
|
end
|
117
160
|
|
@@ -120,23 +163,71 @@ module Flounder
|
|
120
163
|
|
121
164
|
private
|
122
165
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
166
|
+
# Transforms a simple symbol into either a field of the last .join table,
|
167
|
+
# or respects field values passed in.
|
168
|
+
#
|
169
|
+
def join_field name
|
170
|
+
return name if name.kind_of? Field
|
171
|
+
@last_join[name]
|
172
|
+
end
|
173
|
+
|
174
|
+
# Maps an array of field references to Flounder::Field objects. A field
|
175
|
+
# reference can be:
|
176
|
+
#
|
177
|
+
# * a symbol, interpreted as a field name of the main enity of the
|
178
|
+
# operation
|
179
|
+
# * a string, interpreted as something to be passed into the SQL statement
|
180
|
+
# as is. Caution: Don't expose this to unsecure channels!
|
181
|
+
# * a Flounder::Field, left alone (obtained through calling #[] on any
|
182
|
+
# entity)
|
183
|
+
#
|
184
|
+
def map_to_fields field_list
|
185
|
+
field_list.map { |x|
|
186
|
+
map_to_field(x)
|
130
187
|
}
|
131
188
|
end
|
189
|
+
def map_to_field field_ref
|
190
|
+
case field_ref
|
191
|
+
when Symbol
|
192
|
+
from_entity[field_ref]
|
193
|
+
when String
|
194
|
+
Immediate.new(field_ref)
|
195
|
+
when Field
|
196
|
+
field_ref
|
197
|
+
else
|
198
|
+
fail InvalidFieldReference, "Cannot resolve #{field_ref.inspect} to a field."
|
199
|
+
end
|
200
|
+
end
|
132
201
|
|
202
|
+
# Maps a field reference (see #fields) to an Arel::Field.
|
203
|
+
#
|
204
|
+
def map_to_arel field_list
|
205
|
+
map_to_fields(field_list).map(&:to_arel_field)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Prepares a kick (aka transformation into sql/result). This should include
|
209
|
+
# all actions that need to be performed to validate the query.
|
210
|
+
#
|
133
211
|
def prepare_kick
|
134
212
|
unless @has_projection
|
135
|
-
|
213
|
+
@has_projection = true
|
214
|
+
|
215
|
+
# Prepare the regular expression that we'll use to extract entities
|
216
|
+
# from column names.
|
217
|
+
@re_field = %r(
|
218
|
+
^_ # indicates one of our own
|
219
|
+
(?<prefix>#{projection_prefixes.keys.join('|')})
|
220
|
+
_
|
221
|
+
(?<field_name>.*)
|
222
|
+
$
|
223
|
+
)x
|
224
|
+
|
225
|
+
manager.project *default_projection
|
136
226
|
end
|
137
227
|
end
|
138
228
|
|
139
|
-
#
|
229
|
+
# Called on each key/value pair of a condition clause, this returns a
|
230
|
+
# condition that can be passed to Arel #where.
|
140
231
|
#
|
141
232
|
def transform_hash_condition field, value
|
142
233
|
if value.kind_of? Field
|
@@ -169,27 +260,15 @@ module Flounder
|
|
169
260
|
end
|
170
261
|
end
|
171
262
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|entity, name, value, type_oid, binary, idx|
|
180
|
-
|
181
|
-
# p [entity.name, name, type_oid, type_name(result, idx)]
|
182
|
-
typecast_value = engine.connection.typecast(type_oid, value)
|
183
|
-
|
184
|
-
if entity
|
185
|
-
obj[entity.singular] ||= {}
|
186
|
-
obj[entity.singular][name] = typecast_value
|
187
|
-
else
|
188
|
-
obj[name] = typecast_value
|
189
|
-
end
|
190
|
-
end
|
263
|
+
def extract_source_info_from_name name
|
264
|
+
md = name.match(@re_field)
|
265
|
+
fail "ASSERTION FAILURE Source info extraction failed." unless md
|
266
|
+
|
267
|
+
entity = projection_prefixes[md[:prefix]]
|
268
|
+
fail "ASSERTION FAILURE entity cannot be nil" unless entity
|
269
|
+
name = md[:field_name]
|
191
270
|
|
192
|
-
return
|
271
|
+
return entity, name
|
193
272
|
end
|
194
273
|
end # class
|
195
274
|
end # module Flounder
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Flounder
|
2
|
+
|
3
|
+
# An update obtained by calling any of the chain methods on an entity.
|
4
|
+
#
|
5
|
+
class Update
|
6
|
+
def initialize domain, entity
|
7
|
+
@domain = domain
|
8
|
+
@entity = entity
|
9
|
+
@engine = Engine.new(entity.domain.connection_pool)
|
10
|
+
@manager = Arel::UpdateManager.new(@engine)
|
11
|
+
|
12
|
+
manager.table entity.table
|
13
|
+
end
|
14
|
+
|
15
|
+
# Domain that this update was issued from.
|
16
|
+
attr_reader :domain
|
17
|
+
# Entity that this update acts on.
|
18
|
+
attr_reader :entity
|
19
|
+
|
20
|
+
# Arel SqlManager that accumulates this query.
|
21
|
+
attr_reader :manager
|
22
|
+
# Database engine that links Arel to Postgres.
|
23
|
+
attr_reader :engine
|
24
|
+
|
25
|
+
# Add one row to the updates.
|
26
|
+
#
|
27
|
+
def update fields
|
28
|
+
manager.set fields.map { |k, v| transform_hash_keys(k, v) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def where conditions={}
|
32
|
+
conditions.each do |k, v|
|
33
|
+
manager.where(transform_hash_condition(k, v))
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Kickers
|
39
|
+
def to_sql
|
40
|
+
manager.to_sql.tap { |sql|
|
41
|
+
domain.log_sql(sql) }
|
42
|
+
end
|
43
|
+
alias sql to_sql
|
44
|
+
|
45
|
+
# Returns all rows of the update result as an array. Individual rows are
|
46
|
+
# mapped to objects using the row mapper.
|
47
|
+
#
|
48
|
+
def returning fields = '*'
|
49
|
+
updated = nil
|
50
|
+
engine.exec(sql + " RETURNING #{fields}") do |result|
|
51
|
+
updated = Array.new(result.ntuples, nil)
|
52
|
+
result.ntuples.times do |row_idx|
|
53
|
+
updated[row_idx] = engine.connection.
|
54
|
+
objectify_result_row(entity, result, row_idx)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
updated
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Called on each key/value pair of an insert clause, this returns a
|
63
|
+
# hash that can be passed to Arel #insert.
|
64
|
+
#
|
65
|
+
def transform_hash_keys field, value
|
66
|
+
if value.kind_of? Field
|
67
|
+
value = value.arel_field
|
68
|
+
end
|
69
|
+
|
70
|
+
case field
|
71
|
+
when Symbol
|
72
|
+
[entity[field].arel_field, value]
|
73
|
+
when Flounder::Field
|
74
|
+
[field.arel_field, value]
|
75
|
+
else
|
76
|
+
fail "Could not transform condition part. (#{field.inspect}, #{value.inspect})"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Called on each key/value pair of a condition clause, this returns a
|
81
|
+
# condition that can be passed to Arel #where.
|
82
|
+
#
|
83
|
+
def transform_hash_condition field, value
|
84
|
+
if value.kind_of? Field
|
85
|
+
value = value.arel_field
|
86
|
+
end
|
87
|
+
|
88
|
+
case field
|
89
|
+
when Symbol
|
90
|
+
condition_part(entity[field].arel_field, value)
|
91
|
+
when Flounder::Field
|
92
|
+
condition_part(field.arel_field, value)
|
93
|
+
when Flounder::SymbolExtensions::Modifier
|
94
|
+
condition_part(
|
95
|
+
field.to_arel_field(entity),
|
96
|
+
value,
|
97
|
+
field.kind)
|
98
|
+
else
|
99
|
+
fail "Could not transform condition part. (#{field.inspect}, #{value.inspect})"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
def condition_part arel_field, value, kind=:eq
|
103
|
+
case value
|
104
|
+
when Symbol
|
105
|
+
value_field = entity[value].arel_field
|
106
|
+
arel_field.send(kind, value_field)
|
107
|
+
when Range
|
108
|
+
arel_field.in(value)
|
109
|
+
else
|
110
|
+
arel_field.send(kind, value)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end # class
|
114
|
+
end # module Flounder
|
data/lib/flounder.rb
CHANGED
@@ -10,8 +10,13 @@ require 'flounder/connection_pool'
|
|
10
10
|
require 'flounder/domain'
|
11
11
|
require 'flounder/engine'
|
12
12
|
require 'flounder/entity'
|
13
|
+
require 'flounder/entity_alias'
|
13
14
|
require 'flounder/field'
|
15
|
+
require 'flounder/immediate'
|
14
16
|
require 'flounder/query'
|
17
|
+
require 'flounder/insert'
|
18
|
+
require 'flounder/update'
|
19
|
+
require 'flounder/exceptions'
|
15
20
|
|
16
21
|
module Flounder
|
17
22
|
module_function
|
data/qed/applique/ae.rb
CHANGED
File without changes
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
def domain
|
4
|
+
@domain ||= begin
|
5
|
+
connection = Flounder.connect(dbname: 'flounder')
|
6
|
+
Flounder.domain(connection) do |dom|
|
7
|
+
dom.entity(:users, :user, 'users')
|
8
|
+
dom.entity(:posts, :post, 'posts')
|
9
|
+
dom.entity(:comments, :comment, 'comments')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Allows using entities directly, without indirection via domain. (ie `users`)
|
15
|
+
domain.entities.each do |entity|
|
16
|
+
define_method entity.plural do
|
17
|
+
entity
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Load the SQL fixtures.
|
22
|
+
#
|
23
|
+
# One fine day we will use transactional features.
|
24
|
+
#
|
25
|
+
File.open File.expand_path '../../qed/flounder.sql' do |file|
|
26
|
+
Flounder::Engine.new(domain.connection_pool).exec file.read
|
27
|
+
end
|
data/qed/exceptions.md
ADDED
data/qed/flounder.sql
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
-- Database fixture for these tests. Please
|
1
|
+
-- Database fixture for these tests. Please import into a database that
|
2
2
|
-- should also be called 'flounder'.
|
3
3
|
|
4
4
|
DROP TABLE IF EXISTS "users" CASCADE;
|
@@ -9,6 +9,7 @@ CREATE TABLE "users" (
|
|
9
9
|
|
10
10
|
BEGIN;
|
11
11
|
INSERT INTO "users" (name) VALUES ('John Snow');
|
12
|
+
INSERT INTO "users" (name) VALUES ('John Doe');
|
12
13
|
COMMIT;
|
13
14
|
|
14
15
|
DROP TABLE IF EXISTS "posts" CASCADE;
|
@@ -16,12 +17,13 @@ CREATE TABLE "posts" (
|
|
16
17
|
"id" serial PRIMARY KEY,
|
17
18
|
"title" varchar(100) NOT NULL,
|
18
19
|
"text" text NOT NULL,
|
19
|
-
"user_id" int NOT NULL REFERENCES users("id")
|
20
|
+
"user_id" int NOT NULL REFERENCES users("id"),
|
21
|
+
"approver_id" int REFERENCES users("id")
|
20
22
|
);
|
21
23
|
|
22
24
|
BEGIN;
|
23
|
-
INSERT INTO "posts" (title, text, user_id) VALUES (
|
24
|
-
'First Light', 'This is the first post in our test system.',
|
25
|
+
INSERT INTO "posts" (title, text, user_id, approver_id) VALUES (
|
26
|
+
'First Light', 'This is the first post in our test system.', 1, 2);
|
25
27
|
COMMIT;
|
26
28
|
|
27
29
|
DROP TABLE IF EXISTS "comments" CASCADE;
|
data/qed/index.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
|
2
2
|
Flounder is a simple layer between you and the database. It abstracts syntax details and removes the need for string manipulation and parsing, but it does not 'map to objects' in the traditional sense. It still returns objects from queries, though - and it certainly allows influencing the mapping, but the core of flounder is about making querying and manipulating the database look nice and be maintainable. Here are our guiding principles:
|
3
3
|
|
4
|
-
*
|
5
|
-
*
|
4
|
+
* Given the choice, we'll chose the method name close to SQL. Flounder should make that domain knowledge more useful, not introduce another domain.
|
5
|
+
* Composability over complexity.
|
6
6
|
|
7
7
|
# A simple Domain
|
8
8
|
|
@@ -20,8 +20,8 @@ Here's our domain definition. In which - yes - you specify both singular and plu
|
|
20
20
|
Now very simple selects should work as expected.
|
21
21
|
|
22
22
|
~~~ruby
|
23
|
-
|
24
|
-
|
23
|
+
domain[:users].where(id: 1).assert generates_sql(
|
24
|
+
%Q(SELECT [fields] FROM "users" WHERE "users"."id" = 1))
|
25
25
|
~~~
|
26
26
|
|
27
27
|
The `domain[:users]` bit refers to a relation in the database and when you append another square bracket pair, you'll refer to a field in the database - everywhere, in all flounder clauses.
|
@@ -33,29 +33,29 @@ The `domain[:users]` bit refers to a relation in the database and when you appen
|
|
33
33
|
Also, several conditions work as one would expect from DataMapper.
|
34
34
|
|
35
35
|
~~~ruby
|
36
|
-
domain[:users].where(id: 1..10).
|
37
|
-
%Q(SELECT
|
38
|
-
|
39
|
-
domain[:users].where(:id.lt => 10).
|
40
|
-
"SELECT
|
41
|
-
domain[:users].where(:id.lteq => 10).
|
42
|
-
"SELECT
|
43
|
-
domain[:users].where(:id.gt => 10).
|
44
|
-
"SELECT
|
45
|
-
domain[:users].where(:id.gteq => 10).
|
46
|
-
"SELECT
|
47
|
-
domain[:users].where(:id.not_eq => 10).
|
48
|
-
"SELECT
|
36
|
+
domain[:users].where(id: 1..10).assert generates_sql(
|
37
|
+
%Q(SELECT [fields] FROM "users" WHERE "users"."id" BETWEEN 1 AND 10))
|
38
|
+
|
39
|
+
domain[:users].where(:id.lt => 10).assert generates_sql(
|
40
|
+
"SELECT [fields] FROM \"users\" WHERE \"users\".\"id\" < 10")
|
41
|
+
domain[:users].where(:id.lteq => 10).assert generates_sql(
|
42
|
+
"SELECT [fields] FROM \"users\" WHERE \"users\".\"id\" <= 10")
|
43
|
+
domain[:users].where(:id.gt => 10).assert generates_sql(
|
44
|
+
"SELECT [fields] FROM \"users\" WHERE \"users\".\"id\" > 10")
|
45
|
+
domain[:users].where(:id.gteq => 10).assert generates_sql(
|
46
|
+
"SELECT [fields] FROM \"users\" WHERE \"users\".\"id\" >= 10")
|
47
|
+
domain[:users].where(:id.not_eq => 10).assert generates_sql(
|
48
|
+
"SELECT [fields] FROM \"users\" WHERE \"users\".\"id\" != 10")
|
49
49
|
~~~
|
50
50
|
|
51
51
|
Fields can be used fully qualified by going through the entity.
|
52
52
|
|
53
53
|
~~~ruby
|
54
54
|
domain[:users].where(domain[:users][:id] => 10).
|
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
|
-
|
58
|
+
assert generates_sql(%Q(SELECT [fields] FROM "users" WHERE "users"."name" LIKE 'a%'))
|
59
59
|
~~~
|
60
60
|
|
61
61
|
# Some JOINs
|
@@ -63,11 +63,11 @@ Fields can be used fully qualified by going through the entity.
|
|
63
63
|
Here are some non-crazy joins that also work.
|
64
64
|
|
65
65
|
~~~ruby
|
66
|
-
|
67
|
-
|
66
|
+
domain[:users].join(domain[:posts]).on(:id => :user_id).
|
67
|
+
assert generates_sql(%Q(SELECT [fields] FROM "users" INNER JOIN "posts" ON "users"."id" = "posts"."user_id"))
|
68
68
|
|
69
|
-
|
70
|
-
|
69
|
+
domain[:users].outer_join(domain[:posts]).on(:id => :user_id).
|
70
|
+
assert generates_sql(%Q(SELECT [fields] FROM "users" LEFT OUTER JOIN "posts" ON "users"."id" = "posts"."user_id"))
|
71
71
|
~~~
|
72
72
|
|
73
73
|
Joining presents an interesting dilemma. There are two ways of joining things together, given three tables. The sequence A.B.C might mean to join A to B and C; it might also be interpreted to mean to join A to B and B to C. Here's how we solve this.
|
@@ -76,7 +76,7 @@ Joining presents an interesting dilemma. There are two ways of joining things to
|
|
76
76
|
domain[:users].
|
77
77
|
join(domain[:posts]).on(:id => :user_id).
|
78
78
|
join(domain[:comments]).on(:id => :post_id).
|
79
|
-
|
79
|
+
assert generates_sql(%Q(SELECT [fields] FROM "users" INNER JOIN "posts" ON "users"."id" = "posts"."user_id" INNER JOIN "comments" ON "users"."id" = "comments"."post_id"))
|
80
80
|
~~~
|
81
81
|
|
82
82
|
So just doing `A.B.C` will give you the first of the above possibilities. Here's how to achive the second effect.
|
@@ -85,7 +85,7 @@ So just doing `A.B.C` will give you the first of the above possibilities. Here's
|
|
85
85
|
domain[:users].
|
86
86
|
join(domain[:posts]).on(:id => :user_id).anchor.
|
87
87
|
join(domain[:comments]).on(:id => :post_id).
|
88
|
-
|
88
|
+
assert generates_sql(%Q(SELECT [fields] FROM "users" INNER JOIN "posts" ON "users"."id" = "posts"."user_id" INNER JOIN "comments" ON "posts"."id" = "comments"."post_id"))
|
89
89
|
~~~
|
90
90
|
|
91
91
|
The call to `#anchor` anchors all further joins at that point.
|
@@ -93,8 +93,11 @@ The call to `#anchor` anchors all further joins at that point.
|
|
93
93
|
# ORDER BY
|
94
94
|
|
95
95
|
~~~ruby
|
96
|
-
domain[:users].where(id: 2013).
|
97
|
-
|
96
|
+
domain[:users].where(id: 2013).order_by(domain[:users][:id]).
|
97
|
+
assert generates_sql(%Q(SELECT [fields] FROM "users" WHERE "users"."id" = 2013 ORDER BY "users"."id"))
|
98
|
+
|
99
|
+
domain[:users].order_by('id').
|
100
|
+
assert generates_sql(%Q(SELECT [fields] FROM "users" ORDER BY "users"."id"))
|
98
101
|
~~~
|
99
102
|
|
100
103
|
# Selective projection
|
data/qed/inserts.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
An insert creates a state from which SQL can be extracted.
|
2
|
+
|
3
|
+
~~~ruby
|
4
|
+
sql = users.insert(:name => 'Mr. Insert SQL').to_sql
|
5
|
+
|
6
|
+
sql.assert == "INSERT INTO \"users\" (\"name\") VALUES ('Mr. Insert SQL')"
|
7
|
+
~~~
|
8
|
+
|
9
|
+
Using returning will return the inserted object.
|
10
|
+
|
11
|
+
~~~ruby
|
12
|
+
results = users.insert(:name => 'Mr. Returning Asterisk').returning
|
13
|
+
|
14
|
+
results.first.name.assert == 'Mr. Returning Asterisk'
|
15
|
+
~~~
|
16
|
+
|
17
|
+
Returning all fields is the default, but you can provide that explicitly.
|
18
|
+
|
19
|
+
~~~ruby
|
20
|
+
results = users.insert(:name => 'Mr. Returning Asterisk').returning '*'
|
21
|
+
|
22
|
+
results.first.name.assert == 'Mr. Returning Asterisk'
|
23
|
+
~~~
|
24
|
+
|
25
|
+
Using returning with a field name will return those fields of the inserted object.
|
26
|
+
|
27
|
+
~~~ruby
|
28
|
+
results = users.insert(:name => 'Mr. Returning').returning(:id)
|
29
|
+
|
30
|
+
id = results.first.id
|
31
|
+
|
32
|
+
users.where(:id => id).first.name.assert == 'Mr. Returning'
|
33
|
+
~~~
|
34
|
+
|
35
|
+
Flounder fields can be used as keys.
|
36
|
+
|
37
|
+
~~~ruby
|
38
|
+
results = users.insert(users[:name] => 'Mr. Flounder Field').returning
|
39
|
+
|
40
|
+
results.first.name.assert == 'Mr. Flounder Field'
|
41
|
+
~~~
|
42
|
+
|
43
|
+
TODO It does not yet support multi-row inserts.
|
data/qed/results.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
~~~ruby
|
5
|
+
post = posts.
|
6
|
+
join(users).on(:user_id => :id).
|
7
|
+
join(users.as(:approvers, :approver)).on(:approver_id => :id).
|
8
|
+
first
|
9
|
+
|
10
|
+
post.user.name.assert == 'John Snow'
|
11
|
+
post.approver.name.assert == 'John Doe'
|
12
|
+
~~~
|
13
|
+
|
14
|
+
# Custom Projections
|
15
|
+
|
16
|
+
TBD: Describes how to do custom projections and how these results will be available in the resulting object.
|
17
|
+
|
18
|
+
|
19
|
+
# Aliasing
|
20
|
+
|
21
|
+
TBD: Describes to join a table multiple times and how to deal with the results.
|
data/qed/selects.md
CHANGED
@@ -3,10 +3,14 @@
|
|
3
3
|
A simple domain definition.
|
4
4
|
|
5
5
|
~~~ruby
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
# See 'setup_domain.rb' for the domain definition. Repeated here, as a
|
7
|
+
# comment:
|
8
|
+
#
|
9
|
+
# Flounder.domain(connection) do |dom|
|
10
|
+
# dom.entity(:users, :user, 'users')
|
11
|
+
# dom.entity(:posts, :post, 'posts')
|
12
|
+
# dom.entity(:comments, :comment, 'comments')
|
13
|
+
# end
|
10
14
|
|
11
15
|
# Enable this line if you want to see all statements executed.
|
12
16
|
# domain.logger = Logger.new(STDOUT)
|
@@ -22,8 +26,8 @@ And a simple use case.
|
|
22
26
|
If we want to see all records, we use the `all` kicker, which has some synonyms. You best discover those by treating the domain entity as an array.
|
23
27
|
|
24
28
|
~~~ruby
|
25
|
-
users = domain[:users].all
|
26
|
-
users.size.assert ==
|
29
|
+
users = domain[:users].all
|
30
|
+
users.size.assert == 6
|
27
31
|
users.assert.kind_of? Array
|
28
32
|
|
29
33
|
domain[:users].map(&:id).assert == users.map(&:id)
|
data/qed/updates.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
An update creates a state from which SQL can be extracted.
|
2
|
+
|
3
|
+
~~~ruby
|
4
|
+
post = posts.first
|
5
|
+
|
6
|
+
sql = posts.update(:title => 'Update SQL').where(:id => post.id).to_sql
|
7
|
+
|
8
|
+
sql.assert == 'UPDATE "posts" SET "title" = \'Update SQL\' WHERE "posts"."id" = 1'
|
9
|
+
~~~
|
10
|
+
|
11
|
+
Flounder fields are ok.
|
12
|
+
|
13
|
+
~~~ruby
|
14
|
+
post = posts.first
|
15
|
+
|
16
|
+
sql = posts.update(posts[:title] => 'Update Flounder SQL').where(:id => post.id).to_sql
|
17
|
+
|
18
|
+
sql.assert == 'UPDATE "posts" SET "title" = \'Update Flounder SQL\' WHERE "posts"."id" = 1'
|
19
|
+
~~~
|
20
|
+
|
21
|
+
It can update a single field.
|
22
|
+
|
23
|
+
~~~ruby
|
24
|
+
post = posts.first
|
25
|
+
|
26
|
+
post = posts.update(:title => 'Update Field').where(:id => post.id).returning.first
|
27
|
+
|
28
|
+
post.title.assert == 'Update Field'
|
29
|
+
~~~
|
30
|
+
|
31
|
+
Flounder fields are ok here too.
|
32
|
+
|
33
|
+
~~~ruby
|
34
|
+
post = posts.first
|
35
|
+
|
36
|
+
post = posts.update(posts[:title] => 'Update Flounder Field').where(:id => post.id).returning.first
|
37
|
+
|
38
|
+
post.title.assert == 'Update Flounder Field'
|
39
|
+
~~~
|
40
|
+
|
41
|
+
An update can take multiple fields.
|
42
|
+
|
43
|
+
~~~ruby
|
44
|
+
post = posts.first
|
45
|
+
|
46
|
+
sql = posts.update(:title => 'Update SQL', :text => 'Update Multiple Fields Text').where(:id => post.id).to_sql
|
47
|
+
|
48
|
+
sql.assert == 'UPDATE "posts" SET "title" = \'Update SQL\', "text" = \'Update Multiple Fields Text\' WHERE "posts"."id" = 1'
|
49
|
+
~~~
|
50
|
+
|
51
|
+
Updating a single row is possible.
|
52
|
+
|
53
|
+
~~~ruby
|
54
|
+
post = posts.first
|
55
|
+
|
56
|
+
post = posts.update(:title => 'Updated Title', :text => 'Update Single Row Possible').where(:id => post.id).returning.first
|
57
|
+
|
58
|
+
post.title.assert == 'Updated Title'
|
59
|
+
post.text.assert == 'Update Single Row Possible'
|
60
|
+
~~~
|
61
|
+
|
62
|
+
Updating multiple rows is possible.
|
63
|
+
|
64
|
+
~~~ruby
|
65
|
+
updated = users.update(:name => 'Update Multiple Rows').where(:name.not_eq => nil).returning
|
66
|
+
|
67
|
+
updated.map(&:name).assert == ['Update Multiple Rows']*6
|
68
|
+
~~~
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flounder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kaspar Schiess
|
8
|
+
- Florian Hanke
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2014-
|
12
|
+
date: 2014-08-01 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: arel
|
@@ -85,6 +86,7 @@ extensions: []
|
|
85
86
|
extra_rdoc_files: []
|
86
87
|
files:
|
87
88
|
- Gemfile
|
89
|
+
- Gemfile.lock
|
88
90
|
- HACKING
|
89
91
|
- LICENSE
|
90
92
|
- README
|
@@ -95,15 +97,25 @@ files:
|
|
95
97
|
- lib/flounder/domain.rb
|
96
98
|
- lib/flounder/engine.rb
|
97
99
|
- lib/flounder/entity.rb
|
100
|
+
- lib/flounder/entity_alias.rb
|
101
|
+
- lib/flounder/exceptions.rb
|
98
102
|
- lib/flounder/field.rb
|
103
|
+
- lib/flounder/immediate.rb
|
104
|
+
- lib/flounder/insert.rb
|
99
105
|
- lib/flounder/postgres_utils.rb
|
100
106
|
- lib/flounder/query.rb
|
101
107
|
- lib/flounder/symbol_extensions.rb
|
108
|
+
- lib/flounder/update.rb
|
102
109
|
- qed/applique/ae.rb
|
103
|
-
- qed/applique/
|
110
|
+
- qed/applique/flounder.rb
|
111
|
+
- qed/applique/setup_domain.rb
|
112
|
+
- qed/exceptions.md
|
104
113
|
- qed/flounder.sql
|
105
114
|
- qed/index.md
|
115
|
+
- qed/inserts.md
|
116
|
+
- qed/results.md
|
106
117
|
- qed/selects.md
|
118
|
+
- qed/updates.md
|
107
119
|
homepage: https://bitbucket.org/technologyastronauts/laboratory_flounder
|
108
120
|
licenses: []
|
109
121
|
metadata: {}
|
@@ -123,15 +135,20 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
135
|
version: '0'
|
124
136
|
requirements: []
|
125
137
|
rubyforge_project:
|
126
|
-
rubygems_version: 2.2.
|
138
|
+
rubygems_version: 2.2.0
|
127
139
|
signing_key:
|
128
140
|
specification_version: 4
|
129
141
|
summary: Flounder is a way to write SQL simply in Ruby. It deals with everything BUT
|
130
142
|
object relational mapping.
|
131
143
|
test_files:
|
132
144
|
- qed/applique/ae.rb
|
133
|
-
- qed/applique/
|
145
|
+
- qed/applique/flounder.rb
|
146
|
+
- qed/applique/setup_domain.rb
|
147
|
+
- qed/exceptions.md
|
134
148
|
- qed/flounder.sql
|
135
149
|
- qed/index.md
|
150
|
+
- qed/inserts.md
|
151
|
+
- qed/results.md
|
136
152
|
- qed/selects.md
|
153
|
+
- qed/updates.md
|
137
154
|
has_rdoc:
|