flounder 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ group :test do
4
+ gem 'qed'
5
+ gem 'ae'
6
+ end
7
+
8
+ gemspec
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
@@ -0,0 +1,13 @@
1
+
2
+
3
+ # Running the tests
4
+
5
+ This project uses QED for testing. Please create a database called 'flounder'
6
+ and then run the SQL in `qed/flounder.sql` in that database context.
7
+
8
+ Then do a
9
+
10
+ qed
11
+
12
+ and the tests will run.
13
+
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)
@@ -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: