flounder 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +33 -0
- data/HACKING +13 -0
- data/LICENSE +23 -0
- data/README +21 -0
- data/flounder.gemspec +22 -0
- data/lib/flounder/connection.rb +81 -0
- data/lib/flounder/connection_pool.rb +30 -0
- data/lib/flounder/domain.rb +74 -0
- data/lib/flounder/engine.rb +25 -0
- data/lib/flounder/entity.rb +57 -0
- data/lib/flounder/field.rb +24 -0
- data/lib/flounder/postgres_utils.rb +76 -0
- data/lib/flounder/query.rb +195 -0
- data/lib/flounder/symbol_extensions.rb +22 -0
- data/lib/flounder.rb +27 -0
- data/qed/applique/ae.rb +1 -0
- data/qed/applique/ahrel.rb +1 -0
- data/qed/flounder.sql +41 -0
- data/qed/index.md +105 -0
- data/qed/selects.md +42 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6b703e899203d9243eed106f9cb6cf3d4b58e2c7
|
4
|
+
data.tar.gz: 1cc85a58b7942aea21f9b684c11320f355e92ab0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e86c577be0793530e9aa64b518c0c62e954728b230434d874e87275e767437050f676b630d6dbc96cbc28ff70390224e9b04336deff504842cdc39b428b83b5f
|
7
|
+
data.tar.gz: 4a2e9d534c6acdd851b7344a75f2099eae066beebbe118045a05f8a6bb1be25633c1d3a1516bff8a3ea89bd7e7b5049575f3eaaf64fae7564319dd4cc9a32731
|
data/Gemfile
ADDED
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/HACKING
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
Copyright (c) 2014 Kaspar Schiess
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person
|
5
|
+
obtaining a copy of this software and associated documentation
|
6
|
+
files (the "Software"), to deal in the Software without
|
7
|
+
restriction, including without limitation the rights to use,
|
8
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the
|
10
|
+
Software is furnished to do so, subject to the following
|
11
|
+
conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
18
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
20
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
21
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
22
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
23
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
Flounder is a way to write SQL simply in Ruby. It deals with everything BUT
|
3
|
+
object relational mapping.
|
4
|
+
|
5
|
+
SYNOPSIS
|
6
|
+
|
7
|
+
require 'flounder'
|
8
|
+
d = Flounder.domain do |dom|
|
9
|
+
dom.entity(:users, 'users')
|
10
|
+
end
|
11
|
+
|
12
|
+
d[:users].where(:id.lt => 10).to_a
|
13
|
+
|
14
|
+
STATUS
|
15
|
+
|
16
|
+
Company-wide private alpha.
|
17
|
+
|
18
|
+
LICENSE
|
19
|
+
|
20
|
+
Private code for now. No use permitted.
|
21
|
+
|
data/flounder.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "flounder"
|
5
|
+
s.version = '0.2.0'
|
6
|
+
s.summary = "Flounder is a way to write SQL simply in Ruby. It deals with everything BUT object relational mapping. "
|
7
|
+
s.email = "kaspar.schiess@technologyastronauts.ch"
|
8
|
+
s.homepage = "https://bitbucket.org/technologyastronauts/laboratory_flounder"
|
9
|
+
s.authors = ['Kaspar Schiess']
|
10
|
+
|
11
|
+
# s.description = <<-EOF
|
12
|
+
# EOF
|
13
|
+
|
14
|
+
s.add_runtime_dependency 'arel', '~> 5', '> 5.0.1'
|
15
|
+
s.add_runtime_dependency 'pg', '> 0.17'
|
16
|
+
s.add_runtime_dependency 'hashie', '~> 3', '>= 3.2'
|
17
|
+
s.add_runtime_dependency 'connection_pool', '~> 2'
|
18
|
+
|
19
|
+
s.files = Dir['**/*']
|
20
|
+
s.test_files = Dir['qed/**/*']
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
module Flounder
|
3
|
+
class Connection
|
4
|
+
attr_reader :pg
|
5
|
+
attr_reader :visitor
|
6
|
+
|
7
|
+
def initialize pg_conn_args
|
8
|
+
@pg = PG.connect(*pg_conn_args)
|
9
|
+
@visitor = Arel::Visitors::ToSql.new(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :visitor
|
13
|
+
|
14
|
+
def exec *args, &block
|
15
|
+
pg.exec *args, &block
|
16
|
+
end
|
17
|
+
|
18
|
+
# ------------------------------------------------ official Connection iface
|
19
|
+
def schema_cache
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
Column = Struct.new(:name, :type)
|
24
|
+
def columns_hash table_name
|
25
|
+
hash = {}
|
26
|
+
pg.exec(%Q(select * from #{quote_table_name(table_name)} limit 0)) do |result|
|
27
|
+
|
28
|
+
# TBD This is a duplicate from the code in Query.
|
29
|
+
result.nfields.times do |idx|
|
30
|
+
name = result.fname(idx)
|
31
|
+
type_oid = result.ftype(idx)
|
32
|
+
mod = result.fmod(idx)
|
33
|
+
typesym = type_oid_to_sym(type_oid)
|
34
|
+
|
35
|
+
unless typesym
|
36
|
+
type_string = type_name(type_oid, mod)
|
37
|
+
fail "No map for oid #{type_oid} found, type(#{type_string})."
|
38
|
+
end
|
39
|
+
|
40
|
+
hash[name] = Column.new(name, typesym)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def primary_key name
|
48
|
+
fail
|
49
|
+
@primary_keys[name.to_s]
|
50
|
+
end
|
51
|
+
|
52
|
+
def table_exists? table_name
|
53
|
+
# TBD Centralize these direct SQL statements in some class
|
54
|
+
! pg.exec(%Q(select count(*) from pg_class where relname = #{quote(table_name)})).getvalue(0,0).nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
def columns name, message = nil
|
58
|
+
fail
|
59
|
+
@columns[name.to_s]
|
60
|
+
end
|
61
|
+
|
62
|
+
def quote_table_name name
|
63
|
+
pg.quote_ident name.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def quote_column_name name
|
67
|
+
pg.quote_ident name.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def schema_cache
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def quote thing, column = nil
|
75
|
+
# p [:quote, thing, column]
|
76
|
+
pg.escape_literal(thing.to_s)
|
77
|
+
end
|
78
|
+
private
|
79
|
+
include PostgresUtils
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'connection_pool'
|
2
|
+
|
3
|
+
module Flounder
|
4
|
+
class ConnectionPool
|
5
|
+
attr_reader :pg_conn_args
|
6
|
+
|
7
|
+
def initialize pg_conn_args
|
8
|
+
@pg_conn_args = pg_conn_args
|
9
|
+
@pool = ::ConnectionPool.new(size: 5, timeout: 5) {
|
10
|
+
Connection.new(pg_conn_args)
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
Spec = Struct.new(:config)
|
15
|
+
|
16
|
+
def with_connection
|
17
|
+
@pool.with do |conn|
|
18
|
+
yield conn
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def checkout
|
23
|
+
@pool.checkout
|
24
|
+
end
|
25
|
+
|
26
|
+
def spec
|
27
|
+
Spec.new(adapter: 'pg')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Flounder
|
5
|
+
# Raised when an entity name cannot be found in this domain.
|
6
|
+
class NoSuchEntity < StandardError
|
7
|
+
end
|
8
|
+
|
9
|
+
class Domain
|
10
|
+
attr_reader :connection_pool
|
11
|
+
attr_accessor :logger
|
12
|
+
|
13
|
+
# A device that discards all logging made to it. This is used to be silent
|
14
|
+
# by default while still allowing the logging of all queries.
|
15
|
+
#
|
16
|
+
class NilDevice
|
17
|
+
def write *args
|
18
|
+
end
|
19
|
+
def close *args
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize connection_pool
|
24
|
+
@connection_pool = connection_pool
|
25
|
+
@plural = {}
|
26
|
+
@singular = {}
|
27
|
+
@oids_entity_map = {}
|
28
|
+
@logger = Logger.new(NilDevice.new)
|
29
|
+
end
|
30
|
+
def [] name
|
31
|
+
raise NoSuchEntity, "No such entity #{name.inspect} in this domain." \
|
32
|
+
unless @plural.has_key?(name)
|
33
|
+
|
34
|
+
@plural.fetch(name)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Logs sql statements that are prepared for execution.
|
38
|
+
#
|
39
|
+
def log_sql sql
|
40
|
+
@logger.info sql
|
41
|
+
end
|
42
|
+
|
43
|
+
# Define a database entity and alias it to plural and singular names that
|
44
|
+
# will be used in the code.
|
45
|
+
#
|
46
|
+
def entity plural, singular, table_name
|
47
|
+
entity = Entity.new(self, plural, singular, table_name).
|
48
|
+
tap { |e| yield e if block_given? }
|
49
|
+
|
50
|
+
@plural[plural] = entity
|
51
|
+
@singular[singular] = entity
|
52
|
+
|
53
|
+
# Also maps OID to entities for field resolution
|
54
|
+
@oids_entity_map[table_oid(table_name)] = entity
|
55
|
+
|
56
|
+
entity
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns an entity by table oid.
|
60
|
+
#
|
61
|
+
def by_oid oid
|
62
|
+
@oids_entity_map[oid]
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def table_oid table_name
|
67
|
+
connection_pool.with_connection do |conn|
|
68
|
+
# TBD
|
69
|
+
conn.exec(%Q(select oid from pg_class where relname = #{conn.quote(table_name)})).
|
70
|
+
getvalue(0,0).to_i
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Flounder
|
2
|
+
class Engine
|
3
|
+
attr_reader :connection_pool
|
4
|
+
attr_reader :connection
|
5
|
+
|
6
|
+
def initialize connection_pool
|
7
|
+
@connection_pool = connection_pool
|
8
|
+
# TBD This connection is currently never returned to the pool, Arel
|
9
|
+
# is designed that way.
|
10
|
+
@connection = connection_pool.checkout
|
11
|
+
end
|
12
|
+
|
13
|
+
def exec *args, &block
|
14
|
+
connection_pool.with_connection do |conn|
|
15
|
+
conn.exec *args, &block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# ---------------------------------------------------- official Engine iface
|
20
|
+
|
21
|
+
def connection_pool
|
22
|
+
@connection_pool
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Flounder
|
2
|
+
class Entity
|
3
|
+
# Domain this entity is defined in.
|
4
|
+
attr_reader :domain
|
5
|
+
|
6
|
+
# Name of the entity in plural form.
|
7
|
+
attr_reader :name
|
8
|
+
# Name of the entity in singular form.
|
9
|
+
attr_reader :singular
|
10
|
+
|
11
|
+
# Arel table that underlies this entity.
|
12
|
+
attr_reader :table
|
13
|
+
# Name of the table that underlies this entity.
|
14
|
+
attr_reader :table_name
|
15
|
+
|
16
|
+
def initialize domain, plural, singular, table_name
|
17
|
+
@domain = domain
|
18
|
+
@name = plural
|
19
|
+
@singular = singular
|
20
|
+
@table_name = table_name
|
21
|
+
@table = Arel::Table.new(table_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a field of the entity.
|
25
|
+
#
|
26
|
+
def [] name
|
27
|
+
Field.new(self, name, table[name])
|
28
|
+
end
|
29
|
+
|
30
|
+
def belongs_to entity, fk
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"entity(#{name}/#{table_name})"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Query initiators
|
38
|
+
[:where, :join, :outer_join, :project].each do |name|
|
39
|
+
define_method name do |*args|
|
40
|
+
Query.new(domain, self).tap { |q| q.send(name, *args) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Kickers
|
45
|
+
[:first, :all, :size].each do |name|
|
46
|
+
define_method name do |*args|
|
47
|
+
q = Query.new(domain, self)
|
48
|
+
q.send(name, *args)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def each &block
|
53
|
+
all.each &block
|
54
|
+
end
|
55
|
+
include Enumerable
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Flounder
|
2
|
+
class Field
|
3
|
+
attr_reader :entity
|
4
|
+
attr_reader :arel_field
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize entity, name, arel_field
|
8
|
+
@entity = entity
|
9
|
+
@name = name
|
10
|
+
@arel_field = arel_field
|
11
|
+
end
|
12
|
+
|
13
|
+
def fully_qualified_name
|
14
|
+
# TBD quoting? Demeter?
|
15
|
+
entity.domain.connection_pool.with_connection do |conn|
|
16
|
+
table = conn.quote_table_name(entity.table_name)
|
17
|
+
column = conn.quote_column_name(name)
|
18
|
+
"#{table}.#{column}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
include SymbolExtensions
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
|
2
|
+
require 'time'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module Flounder
|
6
|
+
module PostgresUtils
|
7
|
+
OID_INTEGER = 23
|
8
|
+
OID_SMALLINT = 21
|
9
|
+
OID_VARCHAR = 1043
|
10
|
+
OID_BOOLEAN = 16
|
11
|
+
OID_TIMESTAMP = 1114
|
12
|
+
OID_DATE = 1082
|
13
|
+
OID_TIME = 1083
|
14
|
+
|
15
|
+
def typecast type_oid, value
|
16
|
+
return nil unless value
|
17
|
+
case type_oid
|
18
|
+
when OID_TIMESTAMP
|
19
|
+
value && DateTime.parse(value)
|
20
|
+
when OID_DATE
|
21
|
+
value && Date.parse(value)
|
22
|
+
when OID_TIME
|
23
|
+
value && Time.parse(value)
|
24
|
+
when OID_INTEGER, OID_SMALLINT
|
25
|
+
value.to_i
|
26
|
+
when OID_BOOLEAN
|
27
|
+
value == 't'
|
28
|
+
else
|
29
|
+
value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def type_oid_to_sym oid
|
34
|
+
case oid
|
35
|
+
when OID_TIMESTAMP
|
36
|
+
:datetime
|
37
|
+
when OID_DATE
|
38
|
+
:date
|
39
|
+
when OID_TIME
|
40
|
+
:time
|
41
|
+
when OID_INTEGER, OID_SMALLINT
|
42
|
+
:integer
|
43
|
+
when OID_VARCHAR
|
44
|
+
:string
|
45
|
+
when OID_BOOLEAN
|
46
|
+
:boolean
|
47
|
+
else
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def each_field from_entity, result, row_idx
|
53
|
+
domain = from_entity.domain
|
54
|
+
|
55
|
+
result.nfields.times do |field_idx|
|
56
|
+
table_oid = result.ftable(field_idx)
|
57
|
+
entity = domain.by_oid(table_oid)
|
58
|
+
|
59
|
+
yield entity,
|
60
|
+
result.fname(field_idx),
|
61
|
+
result.getvalue(row_idx, field_idx),
|
62
|
+
result.ftype(field_idx),
|
63
|
+
result.fformat(field_idx) == 1,
|
64
|
+
field_idx
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Helper function for debugging
|
69
|
+
def type_name ftype, fmod
|
70
|
+
pg.
|
71
|
+
exec( "SELECT format_type($1,$2)",
|
72
|
+
[ftype, fmod] ).
|
73
|
+
getvalue( 0, 0 )
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module Flounder
|
2
|
+
class Query
|
3
|
+
# Domain that this query was issued from.
|
4
|
+
attr_reader :domain
|
5
|
+
# Entity that this query acts on.
|
6
|
+
attr_reader :from_entity
|
7
|
+
|
8
|
+
# Arel SqlManager that accumulates this query.
|
9
|
+
attr_reader :manager
|
10
|
+
# Database engine that links Arel to Postgres.
|
11
|
+
attr_reader :engine
|
12
|
+
|
13
|
+
|
14
|
+
def initialize domain, from_entity
|
15
|
+
@domain = domain
|
16
|
+
@from_entity = from_entity
|
17
|
+
@engine = Engine.new(from_entity.domain.connection_pool)
|
18
|
+
@manager = Arel::SelectManager.new(@engine)
|
19
|
+
@has_projection = false
|
20
|
+
|
21
|
+
manager.from from_entity.table
|
22
|
+
end
|
23
|
+
|
24
|
+
def where conditions={}
|
25
|
+
conditions.each do |k, v|
|
26
|
+
manager.where(transform_hash_condition(k, v))
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def join entity, join_node=Arel::Nodes::InnerJoin
|
32
|
+
@last_join = entity
|
33
|
+
|
34
|
+
manager.join(entity.table, join_node)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
def outer_join entity
|
38
|
+
join(entity, Arel::Nodes::OuterJoin)
|
39
|
+
end
|
40
|
+
def on join_conditions
|
41
|
+
join_conditions.each do |k, v|
|
42
|
+
manager.on(
|
43
|
+
transform_hash_condition(k, join_field(v)))
|
44
|
+
end
|
45
|
+
self
|
46
|
+
end
|
47
|
+
def anchor
|
48
|
+
@from_entity = @last_join
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def project *field_list
|
53
|
+
# TBD: Clean up
|
54
|
+
@has_projection = true
|
55
|
+
manager.project *map_to_arel(field_list)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def group_by *field_list
|
60
|
+
manager.group *map_to_arel(field_list)
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# Transforms a simple symbol into either a field of the last .join table,
|
65
|
+
# or respects field values passed in.
|
66
|
+
#
|
67
|
+
def join_field name
|
68
|
+
return name if name.kind_of? Field
|
69
|
+
@last_join[name]
|
70
|
+
end
|
71
|
+
|
72
|
+
def order *field_list
|
73
|
+
field_list.each do |field|
|
74
|
+
field = from_entity[field] unless field.kind_of?(Field)
|
75
|
+
manager.order field.fully_qualified_name
|
76
|
+
end
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
# Kickers
|
81
|
+
def to_sql
|
82
|
+
prepare_kick
|
83
|
+
|
84
|
+
manager.to_sql.tap { |sql|
|
85
|
+
domain.log_sql(sql) }
|
86
|
+
end
|
87
|
+
alias sql to_sql
|
88
|
+
|
89
|
+
def each &block
|
90
|
+
all.each(&block)
|
91
|
+
end
|
92
|
+
include Enumerable
|
93
|
+
|
94
|
+
def first
|
95
|
+
manager.take(1)
|
96
|
+
|
97
|
+
all.first
|
98
|
+
end
|
99
|
+
def size
|
100
|
+
manager.projections = []
|
101
|
+
project 'count(*) as count'
|
102
|
+
|
103
|
+
all.first.count
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns all rows of the query result as an array. Individual rows are
|
107
|
+
# mapped to objects using the row mapper.
|
108
|
+
#
|
109
|
+
def all
|
110
|
+
all = nil
|
111
|
+
engine.exec(sql) do |result|
|
112
|
+
all = Array.new(result.ntuples, nil)
|
113
|
+
result.ntuples.times do |row_idx|
|
114
|
+
all[row_idx] = objectify_result_row(result, row_idx)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
all
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def map_to_arel field_list
|
124
|
+
field_list.map { |field|
|
125
|
+
if field.kind_of? Field
|
126
|
+
field.arel_field
|
127
|
+
else
|
128
|
+
field
|
129
|
+
end
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def prepare_kick
|
134
|
+
unless @has_projection
|
135
|
+
manager.project Arel.star
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Transforms things like :a => 1 into field('a').eq(1).
|
140
|
+
#
|
141
|
+
def transform_hash_condition field, value
|
142
|
+
if value.kind_of? Field
|
143
|
+
value = value.arel_field
|
144
|
+
end
|
145
|
+
|
146
|
+
case field
|
147
|
+
when Symbol
|
148
|
+
condition_part(from_entity[field].arel_field, value)
|
149
|
+
when Flounder::Field
|
150
|
+
condition_part(field.arel_field, value)
|
151
|
+
when Flounder::SymbolExtensions::Modifier
|
152
|
+
condition_part(
|
153
|
+
field.to_arel_field(from_entity),
|
154
|
+
value,
|
155
|
+
field.kind)
|
156
|
+
else
|
157
|
+
fail "Could not transform condition part. (#{field.inspect}, #{value.inspect})"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
def condition_part arel_field, value, kind=:eq
|
161
|
+
case value
|
162
|
+
when Symbol
|
163
|
+
value_field = from_entity[value].arel_field
|
164
|
+
arel_field.send(kind, value_field)
|
165
|
+
when Range
|
166
|
+
arel_field.in(value)
|
167
|
+
else
|
168
|
+
arel_field.send(kind, value)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Turns row (a hash) into something that can be treated like an object.
|
173
|
+
# Also performs type coercion.
|
174
|
+
#
|
175
|
+
def objectify_result_row result, row_idx
|
176
|
+
obj = Hashie::Mash.new
|
177
|
+
|
178
|
+
engine.connection.each_field(from_entity, result, row_idx) do
|
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
|
191
|
+
|
192
|
+
return obj
|
193
|
+
end
|
194
|
+
end # class
|
195
|
+
end # module Flounder
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Flounder
|
2
|
+
module SymbolExtensions
|
3
|
+
class Modifier < Struct.new(:sym, :kind)
|
4
|
+
def to_arel_field from_entity
|
5
|
+
af = case sym
|
6
|
+
when Symbol
|
7
|
+
from_entity[sym].arel_field
|
8
|
+
when Flounder::Field
|
9
|
+
sym.arel_field
|
10
|
+
else
|
11
|
+
fail "ASSERTION FAILURE: Unknown type in field.sym: #{field.sym.inspect}."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
[:not_eq, :lt, :gt, :gteq, :lteq, :matches].each do |kind|
|
17
|
+
define_method kind do
|
18
|
+
Modifier.new(self, kind)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/flounder.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'pg'
|
2
|
+
require 'arel'
|
3
|
+
require 'hashie/mash'
|
4
|
+
|
5
|
+
require 'flounder/symbol_extensions'
|
6
|
+
|
7
|
+
require 'flounder/postgres_utils'
|
8
|
+
require 'flounder/connection'
|
9
|
+
require 'flounder/connection_pool'
|
10
|
+
require 'flounder/domain'
|
11
|
+
require 'flounder/engine'
|
12
|
+
require 'flounder/entity'
|
13
|
+
require 'flounder/field'
|
14
|
+
require 'flounder/query'
|
15
|
+
|
16
|
+
module Flounder
|
17
|
+
module_function
|
18
|
+
def connect *args
|
19
|
+
ConnectionPool.new(args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def domain connection, &block
|
23
|
+
Domain.new(connection).tap { |d| yield d if block_given? }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Symbol.send(:include, Flounder::SymbolExtensions)
|
data/qed/applique/ae.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ae'
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'flounder'
|
data/qed/flounder.sql
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
-- Database fixture for these tests. Please improt into a database that
|
2
|
+
-- should also be called 'flounder'.
|
3
|
+
|
4
|
+
DROP TABLE IF EXISTS "users" CASCADE;
|
5
|
+
CREATE TABLE "users" (
|
6
|
+
"id" serial PRIMARY KEY,
|
7
|
+
"name" varchar(40) NOT NULL
|
8
|
+
);
|
9
|
+
|
10
|
+
BEGIN;
|
11
|
+
INSERT INTO "users" (name) VALUES ('John Snow');
|
12
|
+
COMMIT;
|
13
|
+
|
14
|
+
DROP TABLE IF EXISTS "posts" CASCADE;
|
15
|
+
CREATE TABLE "posts" (
|
16
|
+
"id" serial PRIMARY KEY,
|
17
|
+
"title" varchar(100) NOT NULL,
|
18
|
+
"text" text NOT NULL,
|
19
|
+
"user_id" int NOT NULL REFERENCES users("id")
|
20
|
+
);
|
21
|
+
|
22
|
+
BEGIN;
|
23
|
+
INSERT INTO "posts" (title, text, user_id) VALUES (
|
24
|
+
'First Light', 'This is the first post in our test system.', '1');
|
25
|
+
COMMIT;
|
26
|
+
|
27
|
+
DROP TABLE IF EXISTS "comments" CASCADE;
|
28
|
+
CREATE TABLE "comments" (
|
29
|
+
"id" serial PRIMARY KEY,
|
30
|
+
"post_id" int NOT NULL REFERENCES posts("id"),
|
31
|
+
"text" text NOT NULL
|
32
|
+
);
|
33
|
+
|
34
|
+
BEGIN;
|
35
|
+
INSERT INTO "comments" (post_id, text) VALUES (1, 'A silly comment.');
|
36
|
+
COMMIT;
|
37
|
+
|
38
|
+
-- import using
|
39
|
+
--
|
40
|
+
-- cat qed/flounder.sql | psql flounder
|
41
|
+
--
|
data/qed/index.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
|
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
|
+
|
4
|
+
* DSL should be close to SQL; we're not imitating Enumerables.
|
5
|
+
* Not the multitude of chain methods, but the composability of these methods makes Flounders power.
|
6
|
+
|
7
|
+
# A simple Domain
|
8
|
+
|
9
|
+
Here's our domain definition. In which - yes - you specify both singular and plural variants for each entity. Once.
|
10
|
+
|
11
|
+
~~~ruby
|
12
|
+
connection = Flounder.connect(dbname: 'flounder')
|
13
|
+
domain = Flounder.domain(connection) do |dom|
|
14
|
+
dom.entity(:users, :user, 'users')
|
15
|
+
dom.entity(:posts, :post, 'posts')
|
16
|
+
dom.entity(:comments, :comment, 'comments')
|
17
|
+
end
|
18
|
+
~~~
|
19
|
+
|
20
|
+
Now very simple selects should work as expected.
|
21
|
+
|
22
|
+
~~~ruby
|
23
|
+
sql = domain[:users].where(id: 1).sql
|
24
|
+
sql.assert == %Q(SELECT * FROM "users" WHERE "users"."id" = 1)
|
25
|
+
~~~
|
26
|
+
|
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.
|
28
|
+
|
29
|
+
~~~ruby
|
30
|
+
domain[:users][:id].assert.kind_of? Flounder::Field
|
31
|
+
~~~
|
32
|
+
|
33
|
+
Also, several conditions work as one would expect from DataMapper.
|
34
|
+
|
35
|
+
~~~ruby
|
36
|
+
domain[:users].where(id: 1..10).sql.assert ==
|
37
|
+
%Q(SELECT * FROM "users" WHERE "users"."id" BETWEEN 1 AND 10)
|
38
|
+
|
39
|
+
domain[:users].where(:id.lt => 10).sql.assert ==
|
40
|
+
"SELECT * FROM \"users\" WHERE \"users\".\"id\" < 10"
|
41
|
+
domain[:users].where(:id.lteq => 10).sql.assert ==
|
42
|
+
"SELECT * FROM \"users\" WHERE \"users\".\"id\" <= 10"
|
43
|
+
domain[:users].where(:id.gt => 10).sql.assert ==
|
44
|
+
"SELECT * FROM \"users\" WHERE \"users\".\"id\" > 10"
|
45
|
+
domain[:users].where(:id.gteq => 10).sql.assert ==
|
46
|
+
"SELECT * FROM \"users\" WHERE \"users\".\"id\" >= 10"
|
47
|
+
domain[:users].where(:id.not_eq => 10).sql.assert ==
|
48
|
+
"SELECT * FROM \"users\" WHERE \"users\".\"id\" != 10"
|
49
|
+
~~~
|
50
|
+
|
51
|
+
Fields can be used fully qualified by going through the entity.
|
52
|
+
|
53
|
+
~~~ruby
|
54
|
+
domain[:users].where(domain[:users][:id] => 10).
|
55
|
+
sql.assert == %Q(SELECT * FROM "users" WHERE "users"."id" = 10)
|
56
|
+
|
57
|
+
domain[:users].where(domain[:users][:name].matches => 'a%').
|
58
|
+
sql.assert == %Q(SELECT * FROM "users" WHERE "users"."name" LIKE 'a%')
|
59
|
+
~~~
|
60
|
+
|
61
|
+
# Some JOINs
|
62
|
+
|
63
|
+
Here are some non-crazy joins that also work.
|
64
|
+
|
65
|
+
~~~ruby
|
66
|
+
sql = domain[:users].join(domain[:posts]).on(:id => :user_id).sql
|
67
|
+
sql.assert == %Q(SELECT * FROM "users" INNER JOIN "posts" ON "users"."id" = "posts"."user_id")
|
68
|
+
|
69
|
+
sql = domain[:users].outer_join(domain[:posts]).on(:id => :user_id).sql
|
70
|
+
sql.assert == %Q(SELECT * FROM "users" LEFT OUTER JOIN "posts" ON "users"."id" = "posts"."user_id")
|
71
|
+
~~~
|
72
|
+
|
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.
|
74
|
+
|
75
|
+
~~~ruby
|
76
|
+
domain[:users].
|
77
|
+
join(domain[:posts]).on(:id => :user_id).
|
78
|
+
join(domain[:comments]).on(:id => :post_id).
|
79
|
+
sql.assert == %Q(SELECT * FROM "users" INNER JOIN "posts" ON "users"."id" = "posts"."user_id" INNER JOIN "comments" ON "users"."id" = "comments"."post_id")
|
80
|
+
~~~
|
81
|
+
|
82
|
+
So just doing `A.B.C` will give you the first of the above possibilities. Here's how to achive the second effect.
|
83
|
+
|
84
|
+
~~~ruby
|
85
|
+
domain[:users].
|
86
|
+
join(domain[:posts]).on(:id => :user_id).anchor.
|
87
|
+
join(domain[:comments]).on(:id => :post_id).
|
88
|
+
sql.assert == %Q(SELECT * FROM "users" INNER JOIN "posts" ON "users"."id" = "posts"."user_id" INNER JOIN "comments" ON "posts"."id" = "comments"."post_id")
|
89
|
+
~~~
|
90
|
+
|
91
|
+
The call to `#anchor` anchors all further joins at that point.
|
92
|
+
|
93
|
+
# ORDER BY
|
94
|
+
|
95
|
+
~~~ruby
|
96
|
+
domain[:users].where(id: 2013).order(domain[:users][:id]).
|
97
|
+
sql.assert == %Q(SELECT * FROM "users" WHERE "users"."id" = 2013 ORDER BY "users"."id")
|
98
|
+
~~~
|
99
|
+
|
100
|
+
# Selective projection
|
101
|
+
|
102
|
+
~~~ruby
|
103
|
+
domain[:users].where(id: 2013).project(domain[:users][:id]).
|
104
|
+
sql.assert == %Q(SELECT "users"."id" FROM "users" WHERE "users"."id" = 2013)
|
105
|
+
~~~
|
data/qed/selects.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
A simple domain definition.
|
4
|
+
|
5
|
+
~~~ruby
|
6
|
+
connection = Flounder.connect(dbname: 'flounder')
|
7
|
+
domain = Flounder.domain(connection) do |dom|
|
8
|
+
dom.entity(:users, :user, 'users')
|
9
|
+
end
|
10
|
+
|
11
|
+
# Enable this line if you want to see all statements executed.
|
12
|
+
# domain.logger = Logger.new(STDOUT)
|
13
|
+
~~~
|
14
|
+
|
15
|
+
And a simple use case.
|
16
|
+
|
17
|
+
~~~ruby
|
18
|
+
s2013 = domain[:users].where(:id => 1).first
|
19
|
+
s2013.user.id.assert == 1
|
20
|
+
~~~
|
21
|
+
|
22
|
+
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
|
+
|
24
|
+
~~~ruby
|
25
|
+
users = domain[:users].all
|
26
|
+
users.size.assert == 1
|
27
|
+
users.assert.kind_of? Array
|
28
|
+
|
29
|
+
domain[:users].map(&:id).assert == users.map(&:id)
|
30
|
+
~~~
|
31
|
+
|
32
|
+
# Treat it like an array
|
33
|
+
|
34
|
+
You can treat the entities and the queries like an array.
|
35
|
+
|
36
|
+
~~~ruby
|
37
|
+
entity = domain[:users]
|
38
|
+
entity.size.assert > 0
|
39
|
+
|
40
|
+
query = entity.where(:id => :id)
|
41
|
+
query.size.assert == entity.size
|
42
|
+
~~~
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: flounder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kaspar Schiess
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: arel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5'
|
20
|
+
- - ">"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 5.0.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '5'
|
30
|
+
- - ">"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 5.0.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: pg
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.17'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0.17'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: hashie
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3'
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '3.2'
|
57
|
+
type: :runtime
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - "~>"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '3'
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '3.2'
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: connection_pool
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '2'
|
74
|
+
type: :runtime
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '2'
|
81
|
+
description:
|
82
|
+
email: kaspar.schiess@technologyastronauts.ch
|
83
|
+
executables: []
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files: []
|
86
|
+
files:
|
87
|
+
- Gemfile
|
88
|
+
- Gemfile.lock
|
89
|
+
- HACKING
|
90
|
+
- LICENSE
|
91
|
+
- README
|
92
|
+
- flounder.gemspec
|
93
|
+
- lib/flounder.rb
|
94
|
+
- lib/flounder/connection.rb
|
95
|
+
- lib/flounder/connection_pool.rb
|
96
|
+
- lib/flounder/domain.rb
|
97
|
+
- lib/flounder/engine.rb
|
98
|
+
- lib/flounder/entity.rb
|
99
|
+
- lib/flounder/field.rb
|
100
|
+
- lib/flounder/postgres_utils.rb
|
101
|
+
- lib/flounder/query.rb
|
102
|
+
- lib/flounder/symbol_extensions.rb
|
103
|
+
- qed/applique/ae.rb
|
104
|
+
- qed/applique/ahrel.rb
|
105
|
+
- qed/flounder.sql
|
106
|
+
- qed/index.md
|
107
|
+
- qed/selects.md
|
108
|
+
homepage: https://bitbucket.org/technologyastronauts/laboratory_flounder
|
109
|
+
licenses: []
|
110
|
+
metadata: {}
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 2.2.2
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: Flounder is a way to write SQL simply in Ruby. It deals with everything BUT
|
131
|
+
object relational mapping.
|
132
|
+
test_files:
|
133
|
+
- qed/applique/ae.rb
|
134
|
+
- qed/applique/ahrel.rb
|
135
|
+
- qed/flounder.sql
|
136
|
+
- qed/index.md
|
137
|
+
- qed/selects.md
|
138
|
+
has_rdoc:
|