flounder 0.18.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|