flounder 0.18.3 → 1.0.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.
- checksums.yaml +4 -4
- data/Gemfile +6 -0
- data/Gemfile.lock +1 -23
- data/HISTORY +12 -0
- data/README +2 -2
- data/benchmark/001.rb +12 -0
- data/benchmark/002.rb +18 -0
- data/benchmark/lib/connection.rb +19 -0
- data/benchmark/lib/random.rb +15 -0
- data/benchmark/load.rb +23 -0
- data/flounder.gemspec +1 -2
- data/lib/flounder.rb +3 -1
- data/lib/flounder/connection.rb +1 -1
- data/lib/flounder/postgres_utils.rb +29 -12
- data/lib/flounder/query/base.rb +20 -13
- data/lib/flounder/query/select.rb +1 -1
- data/lib/flounder/result/accessor/field.rb +18 -0
- data/lib/flounder/result/accessor/node.rb +36 -0
- data/lib/flounder/result/descriptor.rb +70 -0
- data/lib/flounder/result/row.rb +97 -0
- data/qed/flounder.sql +18 -0
- data/qed/projection.md +5 -1
- data/spec/lib/entity_alias_spec.rb +27 -0
- data/spec/lib/result/row_spec.rb +70 -0
- data/spec/spec_helper.rb +11 -0
- metadata +38 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 147eda5bed1bdaba30729f559e90632dcc127004
|
4
|
+
data.tar.gz: 658f4e9765bf292d36ff6966c56679a4d4e8d20f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 56487164cc91a3a6614f70a5a5142ad98309f2b05aef756fd192bb9c641cd32348f79be04a85ad98fa99b3f2d02ab923fdd3332390cba43235f57eb64310cc33
|
7
|
+
data.tar.gz: e630f8f88801e8abee286a1ac3654f41ec7b9a5564e8168815ebd5ac9c74be54f74151005f75caf3036f92167002092d45df514b2d8aeda3ea8e2e3650739208
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,15 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
flounder (0.18.
|
4
|
+
flounder (0.18.3)
|
5
5
|
aggregate (~> 0.2, >= 0.2.2)
|
6
6
|
arel (~> 5, > 5.0.1)
|
7
|
-
blankslate
|
8
7
|
connection_pool (~> 2)
|
9
|
-
hashie (~> 3, >= 3.2)
|
10
8
|
pg (~> 0.17)
|
11
9
|
pg-hstore (~> 1.2, >= 1.2.0)
|
12
|
-
virtus
|
13
10
|
|
14
11
|
GEM
|
15
12
|
remote: https://rubygems.org/
|
@@ -19,24 +16,11 @@ GEM
|
|
19
16
|
aggregate (0.2.2)
|
20
17
|
ansi (1.4.3)
|
21
18
|
arel (5.0.1.20140414130214)
|
22
|
-
axiom-types (0.0.5)
|
23
|
-
descendants_tracker (~> 0.0.1)
|
24
|
-
ice_nine (~> 0.9)
|
25
|
-
backports (3.6.3)
|
26
|
-
blankslate (3.1.3)
|
27
19
|
brass (1.2.1)
|
28
|
-
coercible (0.2.0)
|
29
|
-
backports (~> 3.0, >= 3.1.0)
|
30
|
-
descendants_tracker (~> 0.0.1)
|
31
20
|
connection_pool (2.0.0)
|
32
|
-
descendants_tracker (0.0.4)
|
33
|
-
thread_safe (~> 0.3, >= 0.3.1)
|
34
21
|
diff-lcs (1.2.5)
|
35
|
-
equalizer (0.0.9)
|
36
22
|
facets (2.9.3)
|
37
23
|
flexmock (1.3.3)
|
38
|
-
hashie (3.3.1)
|
39
|
-
ice_nine (0.11.0)
|
40
24
|
pg (0.17.1)
|
41
25
|
pg-hstore (1.2.0)
|
42
26
|
qed (2.9.1)
|
@@ -56,12 +40,6 @@ GEM
|
|
56
40
|
rspec-support (~> 3.1.0)
|
57
41
|
rspec-support (3.1.2)
|
58
42
|
ruby-prof (0.15.2)
|
59
|
-
thread_safe (0.3.4)
|
60
|
-
virtus (1.0.0)
|
61
|
-
axiom-types (~> 0.0.5)
|
62
|
-
coercible (~> 0.2)
|
63
|
-
descendants_tracker (~> 0.0.1)
|
64
|
-
equalizer (~> 0.0.7)
|
65
43
|
|
66
44
|
PLATFORMS
|
67
45
|
ruby
|
data/HISTORY
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
# 1.0
|
2
|
+
|
3
|
+
* Return type from all queries is now not a Hashie::Mash, but a custom row
|
4
|
+
type. The interface it supports is a lot more restricted. This has two
|
5
|
+
benefits:
|
6
|
+
|
7
|
+
a) Speed increase for queries that return many rows
|
8
|
+
b) Maintenance is easier
|
9
|
+
|
10
|
+
In fact, Hashie::Mash is very tolerant with regards to access to keys that
|
11
|
+
do not exist. The new code is not, so expect breakage.
|
12
|
+
|
1
13
|
# 0.18
|
2
14
|
+ Allows usage of relation names in where clauses.
|
3
15
|
+ Remaps entity oids automatically if a mismatch exists.
|
data/README
CHANGED
@@ -19,8 +19,8 @@ SYNOPSIS
|
|
19
19
|
|
20
20
|
STATUS
|
21
21
|
|
22
|
-
|
23
|
-
|
22
|
+
Somewhat cooled off beta phase - Most features are stable now and the API
|
23
|
+
doesn't change radically anymore.
|
24
24
|
|
25
25
|
LICENSE
|
26
26
|
|
data/benchmark/001.rb
ADDED
data/benchmark/002.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
# First benchmark just tests loading of data into array format.
|
3
|
+
|
4
|
+
require_relative 'lib/connection'
|
5
|
+
require 'ruby-prof'
|
6
|
+
|
7
|
+
domain = Benchmark.domain
|
8
|
+
perf01 = domain[:perf01]
|
9
|
+
|
10
|
+
r = perf01.all.to_a
|
11
|
+
RubyProf.start
|
12
|
+
a = r.map(&:foo).inject(0) { |sum, el| sum + el }
|
13
|
+
result = RubyProf.stop
|
14
|
+
|
15
|
+
# p a
|
16
|
+
|
17
|
+
printer = RubyProf::DotPrinter.new(result)
|
18
|
+
printer.print(STDOUT, min_percent: 2)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
# To get this to work, please run the qed tests first, since those load the
|
3
|
+
# database structure.
|
4
|
+
|
5
|
+
require 'flounder'
|
6
|
+
|
7
|
+
module Benchmark
|
8
|
+
|
9
|
+
module_function
|
10
|
+
def domain
|
11
|
+
@domain ||= begin
|
12
|
+
connection = Flounder.connect(dbname: 'flounder')
|
13
|
+
Flounder.domain(connection) do |dom|
|
14
|
+
dom.entity(:perf01, :perf01, 'perf01')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
data/benchmark/load.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
require_relative 'lib/connection'
|
3
|
+
require_relative 'lib/random'
|
4
|
+
|
5
|
+
domain = Benchmark.domain
|
6
|
+
perf01 = domain[:perf01]
|
7
|
+
|
8
|
+
n = 1000
|
9
|
+
puts "Loading perf01 with #{n} records..."
|
10
|
+
n.times do |n|
|
11
|
+
perf01.insert(
|
12
|
+
foo: n,
|
13
|
+
bar: rand(100000),
|
14
|
+
baz: rand(1000),
|
15
|
+
when: Time.now,
|
16
|
+
count: n,
|
17
|
+
iif: rand() < 0.5,
|
18
|
+
type: Benchmark.rand_string(5, 25),
|
19
|
+
bemerkung: Benchmark.rand_string(5, 200)).kick
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
p perf01.all.to_a
|
data/flounder.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = "flounder"
|
5
|
-
s.version = '0.
|
5
|
+
s.version = '1.0.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/oss_flounder"
|
@@ -16,7 +16,6 @@ Gem::Specification.new do |s|
|
|
16
16
|
|
17
17
|
s.add_runtime_dependency 'arel', '~> 5', '> 5.0.1'
|
18
18
|
s.add_runtime_dependency 'pg', '~> 0.17'
|
19
|
-
s.add_runtime_dependency 'hashie', '~> 3', '>= 3.2'
|
20
19
|
s.add_runtime_dependency 'connection_pool', '~> 2'
|
21
20
|
s.add_runtime_dependency 'pg-hstore', '~> 1.2', '>= 1.2.0'
|
22
21
|
s.add_runtime_dependency 'aggregate', '~> 0.2', '>= 0.2.2'
|
data/lib/flounder.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'pg'
|
2
2
|
require 'arel'
|
3
|
-
require 'hashie/mash'
|
4
3
|
|
5
4
|
require 'flounder/symbol_extensions'
|
6
5
|
|
@@ -23,6 +22,9 @@ require 'flounder/exceptions'
|
|
23
22
|
|
24
23
|
require 'flounder/expression'
|
25
24
|
|
25
|
+
require 'flounder/result/descriptor'
|
26
|
+
require 'flounder/result/row'
|
27
|
+
|
26
28
|
module Flounder
|
27
29
|
module_function
|
28
30
|
def connect opts={}
|
data/lib/flounder/connection.rb
CHANGED
@@ -16,14 +16,16 @@ module Flounder
|
|
16
16
|
OID_TIME = 1083
|
17
17
|
OID_TIMESTAMPTZ = 1184
|
18
18
|
|
19
|
-
def typecast type_oid, value
|
19
|
+
def typecast connection, type_oid, value
|
20
|
+
raise ArgumentError, "AF: type_oid must not be nil" unless type_oid
|
21
|
+
|
20
22
|
return nil unless value
|
21
23
|
|
22
24
|
# hstore extension
|
23
|
-
if
|
25
|
+
if type_oid == oid_hstore(connection)
|
24
26
|
return PgHstore.load(value)
|
25
27
|
end
|
26
|
-
|
28
|
+
|
27
29
|
# assert: value is not nil
|
28
30
|
case type_oid
|
29
31
|
when OID_TIMESTAMPTZ
|
@@ -47,12 +49,12 @@ module Flounder
|
|
47
49
|
end
|
48
50
|
end
|
49
51
|
|
50
|
-
def oid_hstore
|
52
|
+
def oid_hstore connection
|
51
53
|
unless @oid_hstore_initialized
|
52
54
|
@oid_hstore_initialized = true
|
53
55
|
|
54
56
|
@oid_hstore = begin
|
55
|
-
result = exec("SELECT oid FROM pg_type WHERE typname='hstore'")
|
57
|
+
result = connection.exec("SELECT oid FROM pg_type WHERE typname='hstore'")
|
56
58
|
row = result.first
|
57
59
|
|
58
60
|
row && row.values.first.to_i
|
@@ -62,8 +64,10 @@ module Flounder
|
|
62
64
|
@oid_hstore
|
63
65
|
end
|
64
66
|
|
65
|
-
def type_oid_to_sym oid
|
66
|
-
|
67
|
+
def type_oid_to_sym connection, oid
|
68
|
+
raise ArgumentError, "AF: oid must not be nil" unless oid
|
69
|
+
|
70
|
+
return :hash if oid==oid_hstore(connection)
|
67
71
|
|
68
72
|
case oid
|
69
73
|
when OID_TIMESTAMP, OID_TIMESTAMPTZ
|
@@ -85,22 +89,35 @@ module Flounder
|
|
85
89
|
end
|
86
90
|
end
|
87
91
|
|
88
|
-
|
92
|
+
# Yields header information for each column in the given result in turn.
|
93
|
+
#
|
94
|
+
# @yieldparam idx [Integer] column index
|
95
|
+
# @yieldparam entity [Flounder::Entity] entity as resolved by table OID
|
96
|
+
# @yieldparam field_name [String] field name as present in the SQL result
|
97
|
+
# @yieldparam type [Integer] type OID
|
98
|
+
# @yieldparam binary [Boolean] is this a binary field?
|
99
|
+
#
|
100
|
+
def each_field entity, result
|
89
101
|
domain = entity.domain
|
90
102
|
|
91
103
|
result.nfields.times do |field_idx|
|
92
104
|
table_oid = result.ftable(field_idx)
|
93
105
|
entity = domain.by_oid(table_oid)
|
94
106
|
|
95
|
-
yield
|
107
|
+
yield(
|
108
|
+
field_idx,
|
109
|
+
entity,
|
96
110
|
result.fname(field_idx),
|
97
|
-
result.getvalue(row_idx, field_idx),
|
98
111
|
result.ftype(field_idx),
|
99
|
-
result.fformat(field_idx) == 1
|
100
|
-
field_idx
|
112
|
+
result.fformat(field_idx) == 1)
|
101
113
|
end
|
102
114
|
end
|
103
115
|
|
116
|
+
def access_value connection, result, type_oid, row_idx, col_idx
|
117
|
+
value = result.getvalue(row_idx, col_idx)
|
118
|
+
typecast(connection, type_oid, value)
|
119
|
+
end
|
120
|
+
|
104
121
|
# Helper function for debugging
|
105
122
|
def type_name ftype, fmod
|
106
123
|
pg.
|
data/lib/flounder/query/base.rb
CHANGED
@@ -52,31 +52,38 @@ module Flounder::Query
|
|
52
52
|
manager.to_sql.tap { |sql|
|
53
53
|
domain.log_sql(sql) }
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
56
|
# Returns all rows of the query result as an array. Individual rows are
|
57
57
|
# mapped to objects using the row mapper.
|
58
58
|
#
|
59
59
|
def kick connection=nil
|
60
60
|
all = nil
|
61
|
+
connection ||= engine
|
61
62
|
|
62
63
|
measure do
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
64
|
+
result = connection.exec(self.to_sql, bind_values)
|
65
|
+
|
66
|
+
descriptor = ::Flounder::Result::Descriptor.new(
|
67
|
+
connection, entity, result, &method(:column_name_to_entity))
|
68
|
+
|
69
|
+
all = Array.new(result.ntuples, nil)
|
70
|
+
result.ntuples.times do |row_idx|
|
71
|
+
all[row_idx] = descriptor.row(row_idx)
|
71
72
|
end
|
72
73
|
end
|
73
74
|
|
74
75
|
all
|
75
76
|
end
|
77
|
+
|
78
|
+
# Implement this if your column names in the query allow inferring
|
79
|
+
# the entity and the column name. Return them as a tuple <entity, name>.
|
80
|
+
#
|
76
81
|
def column_name_to_entity name
|
77
|
-
# Implement this if your column names in the query allow inferring
|
78
|
-
# the entity and the column name. Return them as a tuple <entity, name>.
|
79
82
|
end
|
83
|
+
|
84
|
+
# Measures the block given to it and logs time spent in the block to the
|
85
|
+
# domain.
|
86
|
+
#
|
80
87
|
def measure
|
81
88
|
measure = Benchmark.measure {
|
82
89
|
yield
|
@@ -233,8 +240,8 @@ module Flounder::Query
|
|
233
240
|
end
|
234
241
|
end
|
235
242
|
|
236
|
-
# Called on each key/value pair of an update clause, this returns a
|
237
|
-
#
|
243
|
+
# Called on each key/value pair of an update clause, this returns a hash
|
244
|
+
# that can be passed to Arel #update.
|
238
245
|
#
|
239
246
|
def transform_tuple_for_set field, value
|
240
247
|
if value.kind_of? Hash
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
module Flounder::Result::Accessor
|
3
|
+
class Field
|
4
|
+
attr_reader :descriptor
|
5
|
+
attr_reader :col_idx
|
6
|
+
attr_reader :type_oid
|
7
|
+
|
8
|
+
def initialize descriptor, col_idx, type_oid
|
9
|
+
@descriptor = descriptor
|
10
|
+
@col_idx = col_idx
|
11
|
+
@type_oid = type_oid
|
12
|
+
end
|
13
|
+
|
14
|
+
def produce_value(row_idx)
|
15
|
+
descriptor.value(type_oid, row_idx, col_idx)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
require_relative 'field'
|
3
|
+
|
4
|
+
module Flounder::Result::Accessor
|
5
|
+
class Node
|
6
|
+
attr_reader :children_by_name
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@children_by_name = Hash.new do |hash, name|
|
10
|
+
hash[name.to_sym] = Node.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def [] name
|
15
|
+
children_by_name[name.to_sym]
|
16
|
+
end
|
17
|
+
def has_obj? name
|
18
|
+
children_by_name.has_key? name.to_sym
|
19
|
+
end
|
20
|
+
def names
|
21
|
+
children_by_name.keys
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_field name, *a
|
25
|
+
children_by_name[name.to_sym] = Field.new(*a)
|
26
|
+
end
|
27
|
+
|
28
|
+
def size
|
29
|
+
children_by_name.size
|
30
|
+
end
|
31
|
+
|
32
|
+
def produce_value *_
|
33
|
+
yield self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
module Flounder::Result
|
3
|
+
class Descriptor
|
4
|
+
include ::Flounder::PostgresUtils
|
5
|
+
|
6
|
+
# Result obtained from the connection
|
7
|
+
attr_reader :pg_result
|
8
|
+
|
9
|
+
attr_reader :connection
|
10
|
+
|
11
|
+
# Entity to use for field resolution
|
12
|
+
attr_reader :entity
|
13
|
+
|
14
|
+
# Root of the accessor tree.
|
15
|
+
# @api private
|
16
|
+
attr_reader :accessor_root
|
17
|
+
|
18
|
+
# @api private
|
19
|
+
attr_reader :name_resolver
|
20
|
+
|
21
|
+
def initialize connection, entity, pg_result, &name_resolver
|
22
|
+
@entity = entity
|
23
|
+
@pg_result = pg_result
|
24
|
+
@connection = connection
|
25
|
+
@name_resolver = name_resolver || -> (name) {}
|
26
|
+
|
27
|
+
build_accessors
|
28
|
+
end
|
29
|
+
|
30
|
+
def row idx
|
31
|
+
Row.new(accessor_root, idx)
|
32
|
+
end
|
33
|
+
|
34
|
+
def value type, row_idx, col_idx
|
35
|
+
access_value(connection, pg_result, type, row_idx, col_idx)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Parses and builds accessor structure for the result stored here.
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
#
|
42
|
+
def build_accessors
|
43
|
+
@accessor_root = Accessor::Node.new
|
44
|
+
each_field(entity, pg_result) do |idx, entity, field, type, binary|
|
45
|
+
processed_entity, processed_name = name_resolver.call(field)
|
46
|
+
entity = processed_entity if processed_entity
|
47
|
+
field = processed_name if processed_name
|
48
|
+
|
49
|
+
# JOIN tables are available from the result using their singular names.
|
50
|
+
if entity
|
51
|
+
accessor_root[entity.singular].add_field(field, self, idx, type)
|
52
|
+
end
|
53
|
+
|
54
|
+
# The main entity and custom fields (AS something) are available on the
|
55
|
+
# top-level of the result.
|
56
|
+
if field && (!entity || entity == self.entity)
|
57
|
+
raise ::Flounder::DuplicateField,
|
58
|
+
"#{field.inspect} already defined in result set, aliasing occurs." \
|
59
|
+
if accessor_root.has_obj? field
|
60
|
+
|
61
|
+
accessor_root.add_field(field, self, idx, type)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
require 'flounder/result/accessor/node'
|
70
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Flounder::Result
|
2
|
+
class Row
|
3
|
+
def initialize node, row_idx
|
4
|
+
@root = node
|
5
|
+
@row_idx = row_idx
|
6
|
+
@attributes = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
# Primary resolution mechanism: Lazy lookup of fields.
|
10
|
+
|
11
|
+
def method_missing sym, *args, &block
|
12
|
+
if @root.has_obj?(sym)
|
13
|
+
return cache_attribute(sym)
|
14
|
+
end
|
15
|
+
|
16
|
+
if sym.to_s.end_with?(??)
|
17
|
+
stripped = sym.to_s[0..-2]
|
18
|
+
return @root.has_obj?(stripped) && !value_for(stripped).nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
super
|
22
|
+
end
|
23
|
+
def respond_to? sym, include_all=false
|
24
|
+
@attributes.has_key?(sym) ||
|
25
|
+
@root.has_obj?(sym) ||
|
26
|
+
super
|
27
|
+
end
|
28
|
+
def methods regular=true
|
29
|
+
(__columns__ + super).uniq
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
"flounder/Row(#{__columns__.inspect})"
|
34
|
+
end
|
35
|
+
|
36
|
+
def == other
|
37
|
+
if other.kind_of?(Row)
|
38
|
+
other_root = other.instance_variable_get('@root')
|
39
|
+
|
40
|
+
__columns__ == other.__columns__ &&
|
41
|
+
__columns__.all? { |name|
|
42
|
+
my_value = self[name]
|
43
|
+
other_value = other[name]
|
44
|
+
my_value == other_value }
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def [] name
|
51
|
+
if @root.has_obj?(name)
|
52
|
+
value_for(name)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns values of given keys as an array.
|
57
|
+
#
|
58
|
+
def values_at *keys
|
59
|
+
keys.map { |key| self[key] }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns all column names.
|
63
|
+
#
|
64
|
+
def __columns__
|
65
|
+
@root.names
|
66
|
+
end
|
67
|
+
|
68
|
+
# Turns this row into a hash, performing deep conversion of all field names.
|
69
|
+
# Use this to go into the Hash world and never come back.
|
70
|
+
#
|
71
|
+
def to_h
|
72
|
+
__columns__.map { |name| [name, value_for(name)] }.to_h
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def value_for name
|
78
|
+
if @attributes.has_key?(name)
|
79
|
+
return @attributes[name]
|
80
|
+
end
|
81
|
+
|
82
|
+
node = @root[name]
|
83
|
+
@attributes[name] = node && node.produce_value(@row_idx) { |node|
|
84
|
+
Row.new(node, @row_idx)
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
def cache_attribute name
|
89
|
+
value = value_for(name)
|
90
|
+
|
91
|
+
# Produce a local accessor via Virtus
|
92
|
+
self.define_singleton_method(name) { value }
|
93
|
+
|
94
|
+
value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/qed/flounder.sql
CHANGED
@@ -41,6 +41,24 @@ BEGIN;
|
|
41
41
|
INSERT INTO "comments" (post_id, text, author_id) VALUES (1, 'A silly comment.', 1);
|
42
42
|
COMMIT;
|
43
43
|
|
44
|
+
BEGIN;
|
45
|
+
-- A table with structure to do performance measurements.
|
46
|
+
DROP TABLE IF EXISTS "perf01" CASCADE;
|
47
|
+
CREATE TABLE "perf01" (
|
48
|
+
"id" serial not null primary key,
|
49
|
+
"foo" int4,
|
50
|
+
"bar" int4,
|
51
|
+
"baz" int2 not null,
|
52
|
+
"when" time(6),
|
53
|
+
"count" int4,
|
54
|
+
"iif" bool DEFAULT true,
|
55
|
+
"type" varchar(255),
|
56
|
+
"created_at" timestamp(6) not NULL default(now()),
|
57
|
+
"updated_at" timestamp(6) not NULL default(now()),
|
58
|
+
"bemerkung" text
|
59
|
+
);
|
60
|
+
COMMIT;
|
61
|
+
|
44
62
|
-- import using
|
45
63
|
--
|
46
64
|
-- cat qed/flounder.sql | psql flounder
|
data/qed/projection.md
CHANGED
@@ -18,7 +18,11 @@ When projecting, all result data will be toplevel and only the projected data is
|
|
18
18
|
result = posts.join(users).on(:user_id => :id).
|
19
19
|
project(users[:name]).first
|
20
20
|
|
21
|
-
result.id
|
21
|
+
result.refute.respond_to?(:id) # Not loaded.
|
22
|
+
|
23
|
+
result.refute.id? # Synonymous to obj.respond_to?(:id) && !obj.id.nil?
|
24
|
+
result.user.assert.name?
|
25
|
+
|
22
26
|
result.user.name.assert == 'John Snow'
|
23
27
|
~~~
|
24
28
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Flounder::EntityAlias do
|
6
|
+
class Table
|
7
|
+
def alias *a
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def [] name
|
12
|
+
"field #{name}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:table) { Table.new }
|
17
|
+
let(:entity) { flexmock('entity',
|
18
|
+
table: table, name: 'entity', table_name: 'table') }
|
19
|
+
|
20
|
+
let(:ea) { Flounder::EntityAlias.new(entity, :aliases, :alias) }
|
21
|
+
|
22
|
+
describe '#fields' do
|
23
|
+
it 'returns a list of fields' do
|
24
|
+
ea.fields(:a, :b).assert == [ea[:a], ea[:b]]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
# Run tests using
|
3
|
+
# ruby -Ilib:test TEST_FILE
|
4
|
+
|
5
|
+
require 'spec_helper'
|
6
|
+
|
7
|
+
describe Flounder::Result::Row do
|
8
|
+
class DescriptorStub
|
9
|
+
def initialize hash
|
10
|
+
@hash = hash
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_obj? name
|
14
|
+
@hash.has_key?(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def [] name
|
18
|
+
val = @hash.fetch(name)
|
19
|
+
|
20
|
+
if val.kind_of? Hash
|
21
|
+
else
|
22
|
+
ValueStub.new(val)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def names
|
27
|
+
@hash.keys
|
28
|
+
end
|
29
|
+
end
|
30
|
+
class ValueStub
|
31
|
+
def initialize value
|
32
|
+
@value = value
|
33
|
+
end
|
34
|
+
def produce_value *a
|
35
|
+
@value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
let(:descriptor) { DescriptorStub.new(:foo => :bar, :bar => :baz) }
|
40
|
+
let(:row) { Flounder::Result::Row.new(descriptor, 1) }
|
41
|
+
|
42
|
+
it 'allows field access through methods' do
|
43
|
+
row.foo.assert == :bar
|
44
|
+
end
|
45
|
+
it 'raises when accessing fields that don\'t exist' do
|
46
|
+
NoMethodError.raised? do
|
47
|
+
row.bar
|
48
|
+
end
|
49
|
+
end
|
50
|
+
it 'has a useful inspect' do
|
51
|
+
row.inspect.assert == "flounder/Row([:foo, :bar])"
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'hash-like features' do
|
55
|
+
it 'has [] accessor' do
|
56
|
+
row[:foo].assert == :bar
|
57
|
+
end
|
58
|
+
it 'has __columns__ list' do
|
59
|
+
row.__columns__.assert == [:foo, :bar]
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'converts to a hash (#to_h)' do
|
63
|
+
row.to_h.assert == {:foo => :bar, :bar => :baz}
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'has #values_at accessor' do
|
67
|
+
row.values_at(:foo, :bar).assert == [:bar, :baz]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flounder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kaspar Schiess
|
@@ -9,114 +9,94 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-12-
|
12
|
+
date: 2014-12-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: arel
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - ~>
|
18
|
+
- - "~>"
|
19
19
|
- !ruby/object:Gem::Version
|
20
20
|
version: '5'
|
21
|
-
- -
|
21
|
+
- - ">"
|
22
22
|
- !ruby/object:Gem::Version
|
23
23
|
version: 5.0.1
|
24
24
|
type: :runtime
|
25
25
|
prerelease: false
|
26
26
|
version_requirements: !ruby/object:Gem::Requirement
|
27
27
|
requirements:
|
28
|
-
- - ~>
|
28
|
+
- - "~>"
|
29
29
|
- !ruby/object:Gem::Version
|
30
30
|
version: '5'
|
31
|
-
- -
|
31
|
+
- - ">"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: 5.0.1
|
34
34
|
- !ruby/object:Gem::Dependency
|
35
35
|
name: pg
|
36
36
|
requirement: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.17'
|
41
41
|
type: :runtime
|
42
42
|
prerelease: false
|
43
43
|
version_requirements: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - ~>
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0.17'
|
48
|
-
- !ruby/object:Gem::Dependency
|
49
|
-
name: hashie
|
50
|
-
requirement: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ~>
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '3'
|
55
|
-
- - '>='
|
56
|
-
- !ruby/object:Gem::Version
|
57
|
-
version: '3.2'
|
58
|
-
type: :runtime
|
59
|
-
prerelease: false
|
60
|
-
version_requirements: !ruby/object:Gem::Requirement
|
61
|
-
requirements:
|
62
|
-
- - ~>
|
63
|
-
- !ruby/object:Gem::Version
|
64
|
-
version: '3'
|
65
|
-
- - '>='
|
66
|
-
- !ruby/object:Gem::Version
|
67
|
-
version: '3.2'
|
68
48
|
- !ruby/object:Gem::Dependency
|
69
49
|
name: connection_pool
|
70
50
|
requirement: !ruby/object:Gem::Requirement
|
71
51
|
requirements:
|
72
|
-
- - ~>
|
52
|
+
- - "~>"
|
73
53
|
- !ruby/object:Gem::Version
|
74
54
|
version: '2'
|
75
55
|
type: :runtime
|
76
56
|
prerelease: false
|
77
57
|
version_requirements: !ruby/object:Gem::Requirement
|
78
58
|
requirements:
|
79
|
-
- - ~>
|
59
|
+
- - "~>"
|
80
60
|
- !ruby/object:Gem::Version
|
81
61
|
version: '2'
|
82
62
|
- !ruby/object:Gem::Dependency
|
83
63
|
name: pg-hstore
|
84
64
|
requirement: !ruby/object:Gem::Requirement
|
85
65
|
requirements:
|
86
|
-
- - ~>
|
66
|
+
- - "~>"
|
87
67
|
- !ruby/object:Gem::Version
|
88
68
|
version: '1.2'
|
89
|
-
- -
|
69
|
+
- - ">="
|
90
70
|
- !ruby/object:Gem::Version
|
91
71
|
version: 1.2.0
|
92
72
|
type: :runtime
|
93
73
|
prerelease: false
|
94
74
|
version_requirements: !ruby/object:Gem::Requirement
|
95
75
|
requirements:
|
96
|
-
- - ~>
|
76
|
+
- - "~>"
|
97
77
|
- !ruby/object:Gem::Version
|
98
78
|
version: '1.2'
|
99
|
-
- -
|
79
|
+
- - ">="
|
100
80
|
- !ruby/object:Gem::Version
|
101
81
|
version: 1.2.0
|
102
82
|
- !ruby/object:Gem::Dependency
|
103
83
|
name: aggregate
|
104
84
|
requirement: !ruby/object:Gem::Requirement
|
105
85
|
requirements:
|
106
|
-
- - ~>
|
86
|
+
- - "~>"
|
107
87
|
- !ruby/object:Gem::Version
|
108
88
|
version: '0.2'
|
109
|
-
- -
|
89
|
+
- - ">="
|
110
90
|
- !ruby/object:Gem::Version
|
111
91
|
version: 0.2.2
|
112
92
|
type: :runtime
|
113
93
|
prerelease: false
|
114
94
|
version_requirements: !ruby/object:Gem::Requirement
|
115
95
|
requirements:
|
116
|
-
- - ~>
|
96
|
+
- - "~>"
|
117
97
|
- !ruby/object:Gem::Version
|
118
98
|
version: '0.2'
|
119
|
-
- -
|
99
|
+
- - ">="
|
120
100
|
- !ruby/object:Gem::Version
|
121
101
|
version: 0.2.2
|
122
102
|
description: " Flounder is the missing piece between the database and your Ruby
|
@@ -127,11 +107,19 @@ executables: []
|
|
127
107
|
extensions: []
|
128
108
|
extra_rdoc_files: []
|
129
109
|
files:
|
130
|
-
- flounder.gemspec
|
131
110
|
- Gemfile
|
132
111
|
- Gemfile.lock
|
133
112
|
- HACKING
|
134
113
|
- HISTORY
|
114
|
+
- LICENSE
|
115
|
+
- README
|
116
|
+
- benchmark/001.rb
|
117
|
+
- benchmark/002.rb
|
118
|
+
- benchmark/lib/connection.rb
|
119
|
+
- benchmark/lib/random.rb
|
120
|
+
- benchmark/load.rb
|
121
|
+
- flounder.gemspec
|
122
|
+
- lib/flounder.rb
|
135
123
|
- lib/flounder/connection.rb
|
136
124
|
- lib/flounder/connection_pool.rb
|
137
125
|
- lib/flounder/domain.rb
|
@@ -151,9 +139,11 @@ files:
|
|
151
139
|
- lib/flounder/query/select.rb
|
152
140
|
- lib/flounder/query/update.rb
|
153
141
|
- lib/flounder/relation.rb
|
142
|
+
- lib/flounder/result/accessor/field.rb
|
143
|
+
- lib/flounder/result/accessor/node.rb
|
144
|
+
- lib/flounder/result/descriptor.rb
|
145
|
+
- lib/flounder/result/row.rb
|
154
146
|
- lib/flounder/symbol_extensions.rb
|
155
|
-
- lib/flounder.rb
|
156
|
-
- LICENSE
|
157
147
|
- qed/applique/ae.rb
|
158
148
|
- qed/applique/flounder.rb
|
159
149
|
- qed/applique/setup_domain.rb
|
@@ -172,7 +162,9 @@ files:
|
|
172
162
|
- qed/projection.md
|
173
163
|
- qed/selects.md
|
174
164
|
- qed/updates.md
|
175
|
-
-
|
165
|
+
- spec/lib/entity_alias_spec.rb
|
166
|
+
- spec/lib/result/row_spec.rb
|
167
|
+
- spec/spec_helper.rb
|
176
168
|
homepage: https://bitbucket.org/technologyastronauts/oss_flounder
|
177
169
|
licenses:
|
178
170
|
- MIT
|
@@ -183,17 +175,17 @@ require_paths:
|
|
183
175
|
- lib
|
184
176
|
required_ruby_version: !ruby/object:Gem::Requirement
|
185
177
|
requirements:
|
186
|
-
- -
|
178
|
+
- - ">="
|
187
179
|
- !ruby/object:Gem::Version
|
188
180
|
version: '0'
|
189
181
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
190
182
|
requirements:
|
191
|
-
- -
|
183
|
+
- - ">="
|
192
184
|
- !ruby/object:Gem::Version
|
193
185
|
version: '0'
|
194
186
|
requirements: []
|
195
187
|
rubyforge_project:
|
196
|
-
rubygems_version: 2.
|
188
|
+
rubygems_version: 2.2.2
|
197
189
|
signing_key:
|
198
190
|
specification_version: 4
|
199
191
|
summary: Flounder is a way to write SQL simply in Ruby. It deals with everything BUT
|
@@ -217,3 +209,4 @@ test_files:
|
|
217
209
|
- qed/projection.md
|
218
210
|
- qed/selects.md
|
219
211
|
- qed/updates.md
|
212
|
+
has_rdoc:
|