rom-sql 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +23 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +153 -0
- data/Rakefile +3 -0
- data/lib/rom-sql.rb +1 -0
- data/lib/rom/sql.rb +10 -0
- data/lib/rom/sql/adapter.rb +62 -0
- data/lib/rom/sql/header.rb +54 -0
- data/lib/rom/sql/relation_extension.rb +15 -0
- data/lib/rom/sql/relation_inclusion.rb +133 -0
- data/lib/rom/sql/spec/support.rb +24 -0
- data/lib/rom/sql/support/sequel_dataset_ext.rb +33 -0
- data/lib/rom/sql/version.rb +5 -0
- data/rom-sql.gemspec +27 -0
- data/spec/shared/users_and_tasks.rb +37 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/unit/combined_associations_spec.rb +72 -0
- data/spec/unit/many_to_many_spec.rb +42 -0
- data/spec/unit/many_to_one_spec.rb +27 -0
- data/spec/unit/one_to_many_spec.rb +27 -0
- data/spec/unit/relation_spec.rb +60 -0
- data/spec/unit/schema_spec.rb +32 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b33ebf8637ba61f769f9ef29bef9f8669600a5ec
|
4
|
+
data.tar.gz: e6aa49e9b572394bd11cabfb2af4901a2f7c2d49
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7cbd522a297fad97d735728bdbb501a96a5660a41d4129a56d7cd9e829bd04f18dae86ff58db1f92d32b2d445a9dc30d1f71a9fb5faacbb70ba682921e827a50
|
7
|
+
data.tar.gz: c7a8943d94016f88a5782cfa0b1a83c802649831841be8fe04093e1b0a49e76d5a74502dedb5742fc76852fafe205824af56145164244eeb17aa812a68ffaf72
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
language: ruby
|
2
|
+
bundler_args: --without yard guard benchmarks
|
3
|
+
env:
|
4
|
+
- CODECLIMATE_REPO_TOKEN=03d7f66589572702b12426d2bc71c4de6281a96139e33b335b894264b1f8f0b0
|
5
|
+
before_script:
|
6
|
+
- psql -c 'create database rom;' -U postgres
|
7
|
+
script: "bundle exec rake spec"
|
8
|
+
rvm:
|
9
|
+
- 2.0
|
10
|
+
- 2.1
|
11
|
+
- rbx-2
|
12
|
+
- jruby
|
13
|
+
- ruby-head
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: ruby-head
|
17
|
+
notifications:
|
18
|
+
webhooks:
|
19
|
+
urls:
|
20
|
+
- https://webhooks.gitter.im/e/39e1225f489f38b0bd09
|
21
|
+
on_success: change
|
22
|
+
on_failure: always
|
23
|
+
on_start: false
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
group :test do
|
6
|
+
gem 'rom', git: 'https://github.com/rom-rb/rom.git', branch: 'master'
|
7
|
+
gem 'rspec', '~> 3.1'
|
8
|
+
gem 'codeclimate-test-reporter', require: false
|
9
|
+
gem 'pg', platforms: [:mri, :rbx]
|
10
|
+
gem 'pg_jruby', platforms: :jruby
|
11
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Piotr Solnica
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following 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 OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
[gem]: https://rubygems.org/gems/rom-sql
|
2
|
+
[travis]: https://travis-ci.org/rom-rb/rom-sql
|
3
|
+
[gemnasium]: https://gemnasium.com/rom-rb/rom-sql
|
4
|
+
[codeclimate]: https://codeclimate.com/github/rom-rb/rom-sql
|
5
|
+
[inchpages]: http://inch-ci.org/github/rom-rb/rom-sql
|
6
|
+
|
7
|
+
# ROM::SQL
|
8
|
+
|
9
|
+
[![Gem Version](https://badge.fury.io/rb/rom-sql.svg)][gem]
|
10
|
+
[![Build Status](https://travis-ci.org/rom-rb/rom-sql.svg?branch=master)][travis]
|
11
|
+
[![Dependency Status](https://gemnasium.com/rom-rb/rom-sql.png)][gemnasium]
|
12
|
+
[![Code Climate](https://codeclimate.com/github/rom-rb/rom-sql/badges/gpa.svg)][codeclimate]
|
13
|
+
[![Test Coverage](https://codeclimate.com/github/rom-rb/rom-sql/badges/coverage.svg)][codeclimate]
|
14
|
+
[![Inline docs](http://inch-ci.org/github/rom-rb/rom-sql.svg?branch=master)][inchpages]
|
15
|
+
|
16
|
+
|
17
|
+
RDBMS suport for [Ruby Object Mapper](https://github.com/rom-rb/rom).
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'rom-sql'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
$ bundle
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
|
33
|
+
$ gem install rom-sql
|
34
|
+
|
35
|
+
## Setup
|
36
|
+
|
37
|
+
ROM uses [Sequel](http://sequel.jeremyevans.net) under the hood and exposes its
|
38
|
+
[Dataset API](http://sequel.jeremyevans.net/rdoc/files/doc/dataset_basics_rdoc.html)
|
39
|
+
in relation objects. For schema migrations you can use its
|
40
|
+
[Migration API](http://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html)
|
41
|
+
which is available via repositories.
|
42
|
+
|
43
|
+
``` ruby
|
44
|
+
setup = ROM.setup(sqlite: "memory::sqlite")
|
45
|
+
|
46
|
+
setup.sqlite.connection.create_table(:users) do
|
47
|
+
primary_key :id
|
48
|
+
String :name
|
49
|
+
Boolean :admin
|
50
|
+
end
|
51
|
+
|
52
|
+
setup.sqlite.connection.create_table(:tasks) do
|
53
|
+
primary_key :id
|
54
|
+
Integer :user_id
|
55
|
+
String :title
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
## Relations
|
60
|
+
|
61
|
+
ROM doesn't have a relationship concept like in ActiveRecord or Sequel. Instead
|
62
|
+
it provides a convenient interface for building joined relations that can be
|
63
|
+
mapped to [aggregate objects](http://martinfowler.com/bliki/Aggregate.html).
|
64
|
+
|
65
|
+
There's no lazy-loading, eager-loading or any other magic happening behind the
|
66
|
+
scenes. You're in full control of how data are fetched from the database and it's
|
67
|
+
an explicit operation.
|
68
|
+
|
69
|
+
``` ruby
|
70
|
+
|
71
|
+
setup.relation(:tasks)
|
72
|
+
|
73
|
+
setup.relation(:users) do
|
74
|
+
one_to_many :tasks, key: :user_id
|
75
|
+
|
76
|
+
def by_name(name)
|
77
|
+
where(name: name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def with_tasks
|
81
|
+
association_join(:tasks)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
rom = setup.finalize
|
86
|
+
|
87
|
+
users = rom.relations.users
|
88
|
+
tasks = rom.relations.tasks
|
89
|
+
|
90
|
+
users.insert(name: "Piotr")
|
91
|
+
tasks.insert(title: "Be happy")
|
92
|
+
|
93
|
+
puts users.by_name("Piotr").with_tasks.to_a.inspect
|
94
|
+
# => [{:id=>1, :name=>"Piotr", :user_id=>1, :title=>"Be happy"}]
|
95
|
+
```
|
96
|
+
|
97
|
+
## Mapping
|
98
|
+
|
99
|
+
Mapping joined relations can be simplified using `wrap` and `group` in-memory
|
100
|
+
operations:
|
101
|
+
|
102
|
+
``` ruby
|
103
|
+
setup.relation(:tasks)
|
104
|
+
|
105
|
+
setup.relation(:users) do
|
106
|
+
one_to_many :tasks, key: :user_id
|
107
|
+
|
108
|
+
def by_name(name)
|
109
|
+
where(name: name)
|
110
|
+
end
|
111
|
+
|
112
|
+
def with_tasks
|
113
|
+
in_memory { group(association_join(:tasks), tasks: [:title]) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
setup.mappers do
|
118
|
+
define(:users) do
|
119
|
+
model name: 'User'
|
120
|
+
|
121
|
+
group :tasks do
|
122
|
+
attribute :title
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
rom = setup.finalize
|
128
|
+
|
129
|
+
users = rom.relations.users
|
130
|
+
tasks = rom.relations.tasks
|
131
|
+
|
132
|
+
users.insert(name: "Piotr")
|
133
|
+
tasks.insert(title: "Be happy")
|
134
|
+
|
135
|
+
rom.read(:users).with_tasks.by_name("Piotr").to_a
|
136
|
+
# => [#<User:0x007fb31542a098 @id=1, @name="Piotr", @tasks=[{:title=>"Be happy"}]>]
|
137
|
+
```
|
138
|
+
|
139
|
+
## ROADMAP
|
140
|
+
|
141
|
+
On a very high level:
|
142
|
+
|
143
|
+
* Make it dead simple to join relations and support all types of joins
|
144
|
+
* Make it dead simple to select and alias column names
|
145
|
+
* Discover conventions for typical query scenarios and make common things trivial,
|
146
|
+
less common things simple and super rare cases possible
|
147
|
+
* Make mapper interface smart enough to figure out when columns are aliased
|
148
|
+
|
149
|
+
For details please refer to [issues](https://github.com/rom-rb/rom-sql/issues).
|
150
|
+
|
151
|
+
## License
|
152
|
+
|
153
|
+
See `LICENSE` file.
|
data/Rakefile
ADDED
data/lib/rom-sql.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'rom/sql'
|
data/lib/rom/sql.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module ROM
|
2
|
+
module SQL
|
3
|
+
|
4
|
+
class Adapter < ROM::Adapter
|
5
|
+
attr_reader :connection
|
6
|
+
|
7
|
+
def self.schemes
|
8
|
+
[:ado, :amalgalite, :cubrid, :db2, :dbi, :do, :fdbsql, :firebird, :ibmdb,
|
9
|
+
:informix, :jdbc, :mysql, :mysql2, :odbc, :openbase, :oracle, :postgres,
|
10
|
+
:sqlanywhere, :sqlite, :swift, :tinytds]
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
super
|
15
|
+
@connection = ::Sequel.connect(uri.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](name)
|
19
|
+
connection[name]
|
20
|
+
end
|
21
|
+
|
22
|
+
def schema
|
23
|
+
tables.map { |table| [table, dataset(table), dataset(table).columns] }
|
24
|
+
end
|
25
|
+
|
26
|
+
def extend_relation_class(klass)
|
27
|
+
klass.send(:include, RelationInclusion)
|
28
|
+
end
|
29
|
+
|
30
|
+
def extend_relation_instance(relation)
|
31
|
+
relation.extend(RelationExtension)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def tables
|
37
|
+
connection.tables
|
38
|
+
end
|
39
|
+
|
40
|
+
def dataset(table)
|
41
|
+
connection[table]
|
42
|
+
end
|
43
|
+
|
44
|
+
def attributes(table)
|
45
|
+
map_attribute_types connection.schema(table)
|
46
|
+
end
|
47
|
+
|
48
|
+
def map_attribute_types(attrs)
|
49
|
+
attrs.map do |column, opts|
|
50
|
+
[column, { type: map_schema_type(opts[:type]) }]
|
51
|
+
end.to_h
|
52
|
+
end
|
53
|
+
|
54
|
+
def map_schema_type(type)
|
55
|
+
connection.class::SCHEMA_TYPE_CLASSES.fetch(type)
|
56
|
+
end
|
57
|
+
|
58
|
+
ROM::Adapter.register(self)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ROM
|
2
|
+
module SQL
|
3
|
+
|
4
|
+
class Header
|
5
|
+
include Charlatan.new(:columns)
|
6
|
+
include Equalizer.new(:columns, :name)
|
7
|
+
|
8
|
+
attr_reader :table
|
9
|
+
|
10
|
+
def initialize(columns, table)
|
11
|
+
super
|
12
|
+
@table = table
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_ary
|
16
|
+
columns
|
17
|
+
end
|
18
|
+
alias_method :to_a, :to_ary
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
columns.each_with_object({}) { |col, h|
|
22
|
+
left, right = col.to_s.split('___')
|
23
|
+
h[left.to_sym] = (right || left).to_sym
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def project(*names)
|
28
|
+
find_all { |col| names.include?(col) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def qualified
|
32
|
+
map { |col| :"#{table}__#{col}" }
|
33
|
+
end
|
34
|
+
|
35
|
+
def rename(options)
|
36
|
+
map { |col|
|
37
|
+
new_name = options[col]
|
38
|
+
|
39
|
+
if new_name
|
40
|
+
:"#{col}___#{new_name}"
|
41
|
+
else
|
42
|
+
col
|
43
|
+
end
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def prefix(col_prefix)
|
48
|
+
rename(Hash[map { |col| [col, :"#{col_prefix}_#{col}"] }])
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module ROM
|
2
|
+
module SQL
|
3
|
+
|
4
|
+
# Sequel-specific relation extensions
|
5
|
+
#
|
6
|
+
module RelationInclusion
|
7
|
+
|
8
|
+
def self.included(klass)
|
9
|
+
klass.extend(AssociationDSL)
|
10
|
+
|
11
|
+
klass.send(:undef_method, :select)
|
12
|
+
|
13
|
+
klass.class_eval {
|
14
|
+
class << self
|
15
|
+
attr_accessor :model
|
16
|
+
end
|
17
|
+
|
18
|
+
self.model = Class.new(Sequel::Model)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(*args)
|
23
|
+
super
|
24
|
+
@model = self.class.model
|
25
|
+
@header = dataset.header
|
26
|
+
end
|
27
|
+
|
28
|
+
# Join configured association.
|
29
|
+
#
|
30
|
+
# Uses INNER JOIN type.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
#
|
34
|
+
# setup.relation(:tasks)
|
35
|
+
#
|
36
|
+
# setup.relations(:users) do
|
37
|
+
# one_to_many :tasks, key: :user_id
|
38
|
+
#
|
39
|
+
# def with_tasks
|
40
|
+
# association_join(:tasks, select: [:title])
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def association_join(*args)
|
46
|
+
send(:append_association, __method__, *args)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Join configured association
|
50
|
+
#
|
51
|
+
# Uses LEFT JOIN type.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
#
|
55
|
+
# setup.relation(:tasks)
|
56
|
+
#
|
57
|
+
# setup.relations(:users) do
|
58
|
+
# one_to_many :tasks, key: :user_id
|
59
|
+
#
|
60
|
+
# def with_tasks
|
61
|
+
# association_left_join(:tasks, select: [:title])
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
def association_left_join(*args)
|
67
|
+
send(:append_association, __method__, *args)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# @api private
|
73
|
+
def append_association(type, name, options = {})
|
74
|
+
self.class.new(
|
75
|
+
dataset.public_send(type, name).
|
76
|
+
select_append(*columns_for_association(name, options))
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
# @api private
|
81
|
+
def columns_for_association(name, options)
|
82
|
+
col_names = options[:select]
|
83
|
+
|
84
|
+
return send(Inflecto.pluralize(name)).qualified_columns unless col_names
|
85
|
+
|
86
|
+
relations = col_names.is_a?(Hash) ? col_names.keys : [name]
|
87
|
+
|
88
|
+
columns = relations.each_with_object([]) do |rel_name, a|
|
89
|
+
relation = send(Inflecto.pluralize(rel_name))
|
90
|
+
names = col_names.is_a?(Hash) ? col_names[rel_name] : col_names
|
91
|
+
|
92
|
+
a.concat(relation.select(*names).prefix.qualified_columns)
|
93
|
+
end
|
94
|
+
|
95
|
+
columns
|
96
|
+
end
|
97
|
+
|
98
|
+
module AssociationDSL
|
99
|
+
|
100
|
+
def one_to_many(name, options)
|
101
|
+
associations << [__method__, name, options.merge(relation: name)]
|
102
|
+
end
|
103
|
+
|
104
|
+
def many_to_many(name, options = {})
|
105
|
+
associations << [__method__, name, options.merge(relation: name)]
|
106
|
+
end
|
107
|
+
|
108
|
+
def many_to_one(name, options = {})
|
109
|
+
associations << [__method__, name, options.merge(relation: Inflecto.pluralize(name).to_sym)]
|
110
|
+
end
|
111
|
+
|
112
|
+
def finalize(relations, relation)
|
113
|
+
associations.each do |*args, options|
|
114
|
+
model = relation.model
|
115
|
+
other = relations[options.fetch(:relation)].model
|
116
|
+
|
117
|
+
model.public_send(*args, options.merge(class: other))
|
118
|
+
end
|
119
|
+
|
120
|
+
model.freeze
|
121
|
+
|
122
|
+
super
|
123
|
+
end
|
124
|
+
|
125
|
+
def associations
|
126
|
+
@associations ||= []
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
if defined? JRUBY_VERSION
|
2
|
+
USING_JRUBY = true
|
3
|
+
else
|
4
|
+
USING_JRUBY = false
|
5
|
+
end
|
6
|
+
|
7
|
+
if USING_JRUBY
|
8
|
+
SEQUEL_TEST_DB_URI = "jdbc:sqlite::memory:"
|
9
|
+
else
|
10
|
+
SEQUEL_TEST_DB_URI = "sqlite::memory"
|
11
|
+
end
|
12
|
+
|
13
|
+
DB = Sequel.connect(SEQUEL_TEST_DB_URI)
|
14
|
+
|
15
|
+
def seed(db = DB)
|
16
|
+
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name STRING)")
|
17
|
+
|
18
|
+
db[:users].insert(id: 1, name: 'Jane')
|
19
|
+
db[:users].insert(id:2, name: 'Joe')
|
20
|
+
end
|
21
|
+
|
22
|
+
def deseed(db = DB)
|
23
|
+
db.drop_table? :users
|
24
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Sequel::Dataset
|
2
|
+
|
3
|
+
def header
|
4
|
+
ROM::SQL::Header.new(opts.fetch(:select) { columns }, opts[:from].first)
|
5
|
+
end
|
6
|
+
|
7
|
+
def project(*names)
|
8
|
+
select(*header.project(*names))
|
9
|
+
end
|
10
|
+
|
11
|
+
def rename(options)
|
12
|
+
select(*header.rename(options))
|
13
|
+
end
|
14
|
+
|
15
|
+
def prefix(col_prefix = default_prefix)
|
16
|
+
rename(header.prefix(col_prefix).to_h)
|
17
|
+
end
|
18
|
+
|
19
|
+
def qualified
|
20
|
+
select(*qualified_columns)
|
21
|
+
end
|
22
|
+
|
23
|
+
def qualified_columns
|
24
|
+
header.qualified.to_a
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def default_prefix
|
30
|
+
Inflecto.singularize(opts[:from].first)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/rom-sql.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rom/sql/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rom-sql"
|
8
|
+
spec.version = ROM::SQL::VERSION
|
9
|
+
spec.authors = ["Piotr Solnica"]
|
10
|
+
spec.email = ["piotr.solnica@gmail.com"]
|
11
|
+
spec.summary = %q{RDBMS support for ROM}
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = "http://rom-rb.org"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "sequel", "~> 4.16"
|
22
|
+
spec.add_runtime_dependency "equalizer", "~> 0.0", ">= 0.0.9"
|
23
|
+
spec.add_runtime_dependency "rom", "~> 0.3", "~> 0.3.0"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
shared_context 'users and tasks' do
|
2
|
+
subject(:rom) { setup.finalize }
|
3
|
+
|
4
|
+
let(:setup) { ROM.setup(postgres: 'postgres://localhost/rom') }
|
5
|
+
let(:conn) { setup.postgres.connection }
|
6
|
+
|
7
|
+
before do
|
8
|
+
[:users, :tasks, :tags, :task_tags].each { |name| conn.drop_table?(name) }
|
9
|
+
|
10
|
+
conn.create_table :users do
|
11
|
+
primary_key :id
|
12
|
+
String :name
|
13
|
+
end
|
14
|
+
|
15
|
+
conn.create_table :tasks do
|
16
|
+
primary_key :id
|
17
|
+
Integer :user_id
|
18
|
+
String :title
|
19
|
+
end
|
20
|
+
|
21
|
+
conn.create_table :tags do
|
22
|
+
primary_key :id
|
23
|
+
String :name
|
24
|
+
end
|
25
|
+
|
26
|
+
conn.create_table :task_tags do
|
27
|
+
primary_key :tag_id, :task_id
|
28
|
+
Integer :tag_id
|
29
|
+
Integer :task_id
|
30
|
+
end
|
31
|
+
|
32
|
+
conn[:users].insert id: 1, name: 'Piotr'
|
33
|
+
conn[:tasks].insert id: 1, user_id: 1, title: 'Finish ROM'
|
34
|
+
conn[:tags].insert id: 1, name: 'important'
|
35
|
+
conn[:task_tags].insert(tag_id: 1, task_id: 1)
|
36
|
+
end
|
37
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Defining multiple associations' do
|
4
|
+
include_context 'users and tasks'
|
5
|
+
|
6
|
+
before do
|
7
|
+
conn[:tasks].insert id: 2, user_id: 1, title: 'Go to sleep'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'extends relation with association methods' do
|
11
|
+
setup.relation(:users)
|
12
|
+
setup.relation(:tags)
|
13
|
+
|
14
|
+
setup.relation(:tasks) do
|
15
|
+
|
16
|
+
many_to_one :users, key: :user_id
|
17
|
+
|
18
|
+
many_to_many :tags,
|
19
|
+
join_table: :task_tags,
|
20
|
+
left_key: :task_id,
|
21
|
+
right_key: :tag_id
|
22
|
+
|
23
|
+
def with_user_and_tags
|
24
|
+
all.with_tags.with_user
|
25
|
+
end
|
26
|
+
|
27
|
+
def all
|
28
|
+
select(:id, :title).qualified
|
29
|
+
end
|
30
|
+
|
31
|
+
def by_tag(name)
|
32
|
+
where(tags__name: name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_tags
|
36
|
+
association_left_join(:tags, select: :name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def with_user
|
40
|
+
association_join(:users, select: :name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def sorted_by_tag_name
|
44
|
+
order(Sequel.desc(:tasks__title))
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
expect(rom.relations.tasks.with_user_and_tags.to_a).to eql([
|
50
|
+
{ id: 1, title: 'Finish ROM', user_name: 'Piotr', tag_name: 'important' },
|
51
|
+
{ id: 2, title: 'Go to sleep', user_name: 'Piotr', tag_name: nil }
|
52
|
+
])
|
53
|
+
|
54
|
+
expect(rom.relations.tasks.with_user_and_tags.sorted_by_tag_name.to_a).to eql([
|
55
|
+
{ id: 2, title: 'Go to sleep', user_name: 'Piotr', tag_name: nil },
|
56
|
+
{ id: 1, title: 'Finish ROM', user_name: 'Piotr', tag_name: 'important' }
|
57
|
+
])
|
58
|
+
|
59
|
+
expect(rom.relations.tasks.with_user_and_tags.by_tag('important').to_a).to eql([
|
60
|
+
{ id: 1, title: 'Finish ROM', user_name: 'Piotr', tag_name: 'important' }
|
61
|
+
])
|
62
|
+
|
63
|
+
expect(rom.relations.tasks.all.with_user.to_a).to eql([
|
64
|
+
{ id: 1, title: 'Finish ROM', user_name: 'Piotr' },
|
65
|
+
{ id: 2, title: 'Go to sleep', user_name: 'Piotr' }
|
66
|
+
])
|
67
|
+
|
68
|
+
expect(rom.relations.tasks.where(title: 'Go to sleep').to_a).to eql(
|
69
|
+
[{ id: 2, user_id: 1, title: 'Go to sleep'}]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Defining many-to-one association' do
|
4
|
+
include_context 'users and tasks'
|
5
|
+
|
6
|
+
before do
|
7
|
+
conn[:tasks].insert id: 2, user_id: 1, title: 'Go to sleep'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'extends relation with association methods' do
|
11
|
+
setup.relation(:tasks) do
|
12
|
+
|
13
|
+
many_to_many :tags,
|
14
|
+
join_table: :task_tags,
|
15
|
+
left_key: :task_id,
|
16
|
+
right_key: :tag_id
|
17
|
+
|
18
|
+
def with_tags
|
19
|
+
association_left_join(:tags).select(:tasks__id, :tasks__title, :tags__name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def by_tag(name)
|
23
|
+
with_tags.where(tags__name: name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
setup.relation(:tags)
|
28
|
+
|
29
|
+
tasks = rom.relations.tasks
|
30
|
+
|
31
|
+
expect(tasks.with_tags.to_a).to eql([
|
32
|
+
{ id: 1, title: 'Finish ROM', name: 'important' },
|
33
|
+
{ id: 2, title: 'Go to sleep', name: nil }
|
34
|
+
])
|
35
|
+
|
36
|
+
expect(tasks.by_tag("important").to_a).to eql([
|
37
|
+
{ id: 1, title: 'Finish ROM', name: 'important' }
|
38
|
+
])
|
39
|
+
|
40
|
+
expect(tasks.by_tag("not-here").to_a).to be_empty
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Defining many-to-one association' do
|
4
|
+
include_context 'users and tasks'
|
5
|
+
|
6
|
+
it 'extends relation with association methods' do
|
7
|
+
setup.relation(:tasks) do
|
8
|
+
many_to_one :users, key: :user_id
|
9
|
+
|
10
|
+
def all
|
11
|
+
select(:id, :title).rename(title: :task_title).qualified
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_user
|
15
|
+
association_join(:users, select: [:name])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
setup.relation(:users)
|
20
|
+
|
21
|
+
tasks = rom.relations.tasks
|
22
|
+
|
23
|
+
expect(tasks.all.with_user.to_a).to eql(
|
24
|
+
[{ id: 1, user_name: 'Piotr', task_title: 'Finish ROM' }]
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Defining one-to-many association' do
|
4
|
+
include_context 'users and tasks'
|
5
|
+
|
6
|
+
it 'extends relation with association methods' do
|
7
|
+
setup.relation(:tasks)
|
8
|
+
|
9
|
+
setup.relation(:users) do
|
10
|
+
one_to_many :tasks, key: :user_id
|
11
|
+
|
12
|
+
def by_name(name)
|
13
|
+
where(name: name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_tasks
|
17
|
+
association_join(:tasks)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
users = rom.relations.users
|
22
|
+
|
23
|
+
expect(users.with_tasks.by_name("Piotr").to_a).to eql(
|
24
|
+
[{ id: 1, user_id: 1, name: 'Piotr', title: 'Finish ROM' }]
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ROM::Relation do
|
4
|
+
include_context 'users and tasks'
|
5
|
+
|
6
|
+
subject(:users) { rom.relations.users }
|
7
|
+
|
8
|
+
before do
|
9
|
+
setup.relation(:users) do
|
10
|
+
def sorted
|
11
|
+
order(:id)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#project' do
|
17
|
+
it 'projects the dataset using new column names' do
|
18
|
+
projected = users.sorted.project(:name)
|
19
|
+
|
20
|
+
expect(projected.header).to match_array([:name])
|
21
|
+
expect(projected.to_a).to eql([{ name: 'Piotr'}])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#rename' do
|
26
|
+
it 'projects the dataset using new column names' do
|
27
|
+
renamed = users.sorted.rename(id: :user_id, name: :user_name)
|
28
|
+
|
29
|
+
expect(renamed.to_a).to eql([{ user_id: 1, user_name: 'Piotr'}])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#prefix' do
|
34
|
+
it 'projects the dataset using new column names' do
|
35
|
+
prefixed = users.sorted.prefix(:user)
|
36
|
+
|
37
|
+
expect(prefixed.to_a).to eql([{ user_id: 1, user_name: 'Piotr'}])
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'uses singularized table name as the default prefix' do
|
41
|
+
prefixed = users.sorted.prefix
|
42
|
+
|
43
|
+
expect(prefixed.to_a).to eql([{ user_id: 1, user_name: 'Piotr'}])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#qualified_columns' do
|
48
|
+
it 'returns qualified column names' do
|
49
|
+
columns = users.sorted.prefix(:user).qualified_columns
|
50
|
+
|
51
|
+
expect(columns).to eql([:users__id___user_id, :users__name___user_name])
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'returns projected qualified column names' do
|
55
|
+
columns = users.sorted.project(:id).prefix(:user).qualified_columns
|
56
|
+
|
57
|
+
expect(columns).to eql([:users__id___user_id])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Inferring schema from database' do
|
4
|
+
let(:setup) { ROM.setup(postgres: "postgres://localhost/rom") }
|
5
|
+
|
6
|
+
context "when database schema exists" do
|
7
|
+
after { rom.postgres.connection.drop_table?(:people) }
|
8
|
+
|
9
|
+
let(:rom) { setup.finalize }
|
10
|
+
|
11
|
+
it "infers the schema from the database relations" do
|
12
|
+
setup.postgres.connection.create_table :people do
|
13
|
+
primary_key :id
|
14
|
+
String :name
|
15
|
+
end
|
16
|
+
|
17
|
+
schema = rom.schema
|
18
|
+
|
19
|
+
expect(schema.people.to_a).to eql(rom.postgres.people.to_a)
|
20
|
+
expect(schema.people.header).to eql([:id, :name])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "for empty database schemas" do
|
25
|
+
it "returns an empty schema" do
|
26
|
+
rom = setup.finalize
|
27
|
+
schema = rom.schema
|
28
|
+
|
29
|
+
expect(schema.postgres).to be(nil)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rom-sql
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Piotr Solnica
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sequel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.16'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: equalizer
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.0'
|
34
|
+
- - '>='
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 0.0.9
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0.0'
|
44
|
+
- - '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.0.9
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rom
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.3'
|
54
|
+
- - ~>
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 0.3.0
|
57
|
+
type: :runtime
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ~>
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0.3'
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 0.3.0
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: bundler
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: rake
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '10.0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ~>
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '10.0'
|
95
|
+
description: RDBMS support for ROM
|
96
|
+
email:
|
97
|
+
- piotr.solnica@gmail.com
|
98
|
+
executables: []
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- .gitignore
|
103
|
+
- .rspec
|
104
|
+
- .travis.yml
|
105
|
+
- CHANGELOG.md
|
106
|
+
- Gemfile
|
107
|
+
- LICENSE.txt
|
108
|
+
- README.md
|
109
|
+
- Rakefile
|
110
|
+
- lib/rom-sql.rb
|
111
|
+
- lib/rom/sql.rb
|
112
|
+
- lib/rom/sql/adapter.rb
|
113
|
+
- lib/rom/sql/header.rb
|
114
|
+
- lib/rom/sql/relation_extension.rb
|
115
|
+
- lib/rom/sql/relation_inclusion.rb
|
116
|
+
- lib/rom/sql/spec/support.rb
|
117
|
+
- lib/rom/sql/support/sequel_dataset_ext.rb
|
118
|
+
- lib/rom/sql/version.rb
|
119
|
+
- rom-sql.gemspec
|
120
|
+
- spec/shared/users_and_tasks.rb
|
121
|
+
- spec/spec_helper.rb
|
122
|
+
- spec/unit/combined_associations_spec.rb
|
123
|
+
- spec/unit/many_to_many_spec.rb
|
124
|
+
- spec/unit/many_to_one_spec.rb
|
125
|
+
- spec/unit/one_to_many_spec.rb
|
126
|
+
- spec/unit/relation_spec.rb
|
127
|
+
- spec/unit/schema_spec.rb
|
128
|
+
homepage: http://rom-rb.org
|
129
|
+
licenses:
|
130
|
+
- MIT
|
131
|
+
metadata: {}
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - '>='
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 2.0.14
|
149
|
+
signing_key:
|
150
|
+
specification_version: 4
|
151
|
+
summary: RDBMS support for ROM
|
152
|
+
test_files:
|
153
|
+
- spec/shared/users_and_tasks.rb
|
154
|
+
- spec/spec_helper.rb
|
155
|
+
- spec/unit/combined_associations_spec.rb
|
156
|
+
- spec/unit/many_to_many_spec.rb
|
157
|
+
- spec/unit/many_to_one_spec.rb
|
158
|
+
- spec/unit/one_to_many_spec.rb
|
159
|
+
- spec/unit/relation_spec.rb
|
160
|
+
- spec/unit/schema_spec.rb
|