static-record 1.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +112 -0
- data/Rakefile +22 -0
- data/app/assets/javascripts/static_record/application.js +13 -0
- data/app/assets/stylesheets/static_record/application.css +15 -0
- data/app/controllers/static_record/application_controller.rb +4 -0
- data/app/helpers/static_record/application_helper.rb +4 -0
- data/app/models/concerns/query_building_concern.rb +103 -0
- data/app/models/concerns/sqlite_storing_concern.rb +61 -0
- data/app/models/static_record/base.rb +61 -0
- data/app/models/static_record/predicates.rb +100 -0
- data/app/models/static_record/querying.rb +25 -0
- data/app/models/static_record/relation.rb +110 -0
- data/app/views/layouts/static_record/application.html.erb +14 -0
- data/config/routes.rb +2 -0
- data/lib/static_record.rb +5 -0
- data/lib/static_record/engine.rb +5 -0
- data/lib/static_record/exceptions.rb +5 -0
- data/lib/static_record/version.rb +3 -0
- data/lib/tasks/static_record_tasks.rake +4 -0
- data/spec/models/static_record/base_spec.rb +10 -0
- data/spec/models/static_record/querying_spec.rb +8 -0
- data/spec/models/static_record/relation_spec.rb +242 -0
- data/spec/rails_helper.rb +15 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/test_app/app/controllers/application_controller.rb +5 -0
- data/spec/test_app/app/helpers/application_helper.rb +2 -0
- data/spec/test_app/app/models/article.rb +6 -0
- data/spec/test_app/app/models/articles/article_four.rb +5 -0
- data/spec/test_app/app/models/articles/article_one.rb +5 -0
- data/spec/test_app/app/models/articles/article_three.rb +5 -0
- data/spec/test_app/app/models/articles/article_two.rb +5 -0
- data/spec/test_app/app/models/role.rb +5 -0
- data/spec/test_app/app/models/roles/role_one.rb +4 -0
- data/spec/test_app/config/application.rb +32 -0
- data/spec/test_app/config/boot.rb +5 -0
- data/spec/test_app/config/environment.rb +5 -0
- data/spec/test_app/config/environments/development.rb +41 -0
- data/spec/test_app/config/environments/production.rb +79 -0
- data/spec/test_app/config/environments/test.rb +42 -0
- data/spec/test_app/config/initializers/assets.rb +11 -0
- data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/test_app/config/initializers/cookies_serializer.rb +3 -0
- data/spec/test_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/test_app/config/initializers/inflections.rb +16 -0
- data/spec/test_app/config/initializers/mime_types.rb +4 -0
- data/spec/test_app/config/initializers/session_store.rb +3 -0
- data/spec/test_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/test_app/config/routes.rb +4 -0
- data/spec/test_app/db/schema.rb +16 -0
- metadata +199 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module StaticRecord
|
2
|
+
module Querying # :nodoc:
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods # :nodoc:
|
8
|
+
def method_missing(method_sym, *arguments, &block)
|
9
|
+
if Relation.new(nil, store: store).respond_to?(method_sym, true)
|
10
|
+
Relation.new(nil, store: store, primary_key: pkey).send(method_sym, *arguments, &block)
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to?(method_sym, include_private = false)
|
17
|
+
if Relation.new(nil, store: store).respond_to?(method_sym, true)
|
18
|
+
true
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module StaticRecord
|
2
|
+
class Relation # :nodoc:
|
3
|
+
attr_reader :columns,
|
4
|
+
:sql_limit,
|
5
|
+
:sql_offset,
|
6
|
+
:where_clauses,
|
7
|
+
:chain,
|
8
|
+
:result_type,
|
9
|
+
:order_by,
|
10
|
+
:only_sql
|
11
|
+
|
12
|
+
include Predicates
|
13
|
+
include StaticRecord::QueryBuildingConcern
|
14
|
+
|
15
|
+
def initialize(previous_node, params)
|
16
|
+
@store = params[:store]
|
17
|
+
@table = params[:store]
|
18
|
+
@primary_key = params[:primary_key]
|
19
|
+
|
20
|
+
@columns = '*'
|
21
|
+
@sql_limit = nil
|
22
|
+
@sql_offset = nil
|
23
|
+
@where_clauses = []
|
24
|
+
@chain = :and
|
25
|
+
@result_type = :array
|
26
|
+
@order_by = []
|
27
|
+
@only_sql = false
|
28
|
+
|
29
|
+
chained_from(previous_node) if previous_node
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(method_sym, *arguments, &block)
|
33
|
+
if respond_to?(method_sym, true)
|
34
|
+
Relation.new(self, store: @store, primary_key: @primary_key).send(method_sym, *arguments, &block)
|
35
|
+
elsif [].respond_to?(method_sym)
|
36
|
+
to_a.send(method_sym)
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def respond_to?(method_sym, include_private = false)
|
43
|
+
if !include_private && [].respond_to?(method_sym, include_private)
|
44
|
+
true
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def chained_from(relation)
|
53
|
+
@columns = relation.columns
|
54
|
+
@sql_limit = relation.sql_limit
|
55
|
+
@sql_offset = relation.sql_offset
|
56
|
+
@where_clauses = relation.where_clauses.deep_dup
|
57
|
+
@chain = relation.chain
|
58
|
+
@result_type = relation.result_type
|
59
|
+
@order_by = relation.order_by.deep_dup
|
60
|
+
@only_sql = relation.only_sql
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_subclause(clause, params = nil)
|
64
|
+
params ||= {}
|
65
|
+
|
66
|
+
clause[:chain] = @chain unless clause[:chain].present?
|
67
|
+
clause[:operator] = :eq unless clause[:operator].present?
|
68
|
+
clause[:parameters] = params
|
69
|
+
|
70
|
+
@where_clauses << clause
|
71
|
+
@chain = :and
|
72
|
+
end
|
73
|
+
|
74
|
+
def exec_request(expectancy = :result_set)
|
75
|
+
return build_query if @only_sql
|
76
|
+
|
77
|
+
error = nil
|
78
|
+
result = nil
|
79
|
+
begin
|
80
|
+
dbname = Rails.root.join('db', "static_#{@store}.sqlite3").to_s
|
81
|
+
db = SQLite3::Database.open(dbname)
|
82
|
+
if expectancy == :integer
|
83
|
+
result = db.get_first_value(build_query)
|
84
|
+
else
|
85
|
+
statement = db.prepare(build_query)
|
86
|
+
result_set = statement.execute
|
87
|
+
result = result_set.map { |row| row[1].constantize.new }
|
88
|
+
end
|
89
|
+
rescue SQLite3::Exception => e
|
90
|
+
error = e
|
91
|
+
ensure
|
92
|
+
statement.close if statement
|
93
|
+
db.close if db
|
94
|
+
end
|
95
|
+
|
96
|
+
raise error if error
|
97
|
+
|
98
|
+
if expectancy == :result_set
|
99
|
+
case @result_type
|
100
|
+
when :array
|
101
|
+
result = [] if result.nil?
|
102
|
+
when :record
|
103
|
+
result = result.empty? ? nil : result.first
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
result
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>StaticRecord</title>
|
5
|
+
<%= stylesheet_link_tag "static_record/application", media: "all" %>
|
6
|
+
<%= javascript_include_tag "static_record/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rails_helper'
|
3
|
+
|
4
|
+
RSpec.describe StaticRecord::Base, :type => :model do
|
5
|
+
it 'allows to store primary key' do
|
6
|
+
Article.primary_key :author
|
7
|
+
expect(Article.pkey).to eql(:author)
|
8
|
+
Article.primary_key :name # restoring primary key for other tests
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rails_helper'
|
3
|
+
|
4
|
+
RSpec.describe StaticRecord::Relation, :type => :model do
|
5
|
+
|
6
|
+
it 'returns a StaticRecord::Relation while used method allows chaining' do
|
7
|
+
expect(Article.where(author: 'The author')).to be_a(StaticRecord::Relation)
|
8
|
+
expect(Article.find_by(author: 'The author')).not_to be_a(StaticRecord::Relation)
|
9
|
+
end
|
10
|
+
|
11
|
+
context '.all' do
|
12
|
+
it 'returns all results' do
|
13
|
+
expect(Article.all.count).to eql(4)
|
14
|
+
expect(Article.see_sql_of.all).to eql("SELECT * FROM articles")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context '.order' do
|
19
|
+
context 'with a symbol' do
|
20
|
+
it 'returns results ordered' do
|
21
|
+
expected = [ArticleFour, ArticleOne, ArticleThree, ArticleTwo]
|
22
|
+
expect(Article.order(:name).all.map(&:class)).to eql(expected)
|
23
|
+
expect(Article.order(:name).to_sql).to eql("SELECT * FROM articles ORDER BY articles.name ASC")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with a string' do
|
28
|
+
it 'returns results ordered' do
|
29
|
+
expected = [ArticleTwo, ArticleThree, ArticleOne, ArticleFour]
|
30
|
+
expect(Article.order("name DESC").all.map(&:class)).to eql(expected)
|
31
|
+
expect(Article.order("name DESC").to_sql).to eql("SELECT * FROM articles ORDER BY name DESC")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with a hash' do
|
36
|
+
it 'returns results ordered with one key' do
|
37
|
+
expected = [ArticleFour, ArticleOne, ArticleThree, ArticleTwo]
|
38
|
+
expect(Article.order(name: :asc).all.map(&:class)).to eql(expected)
|
39
|
+
expect(Article.order(name: :asc).to_sql).to eql("SELECT * FROM articles ORDER BY articles.name ASC")
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'returns results ordered with serveral keys' do
|
43
|
+
expected = [ArticleThree, ArticleOne, ArticleTwo, ArticleFour]
|
44
|
+
expect(Article.order(rank: :asc, name: :desc).all.map(&:class)).to eql(expected)
|
45
|
+
expect(Article.order(rank: :asc, name: :desc).to_sql).to eql("SELECT * FROM articles ORDER BY articles.rank ASC, articles.name DESC")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'with an array' do
|
50
|
+
it 'returns results ordered with one key' do
|
51
|
+
expected = [ArticleFour, ArticleOne, ArticleThree, ArticleTwo]
|
52
|
+
expect(Article.order([:name]).all.map(&:class)).to eql(expected)
|
53
|
+
expect(Article.order([:name]).to_sql).to eql("SELECT * FROM articles ORDER BY articles.name ASC")
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns results ordered with several keys' do
|
57
|
+
expected = [ArticleThree, ArticleOne, ArticleFour, ArticleTwo]
|
58
|
+
expect(Article.order([:rank, :name]).all.map(&:class)).to eql(expected)
|
59
|
+
expect(Article.order([:rank, :name]).to_sql).to eql("SELECT * FROM articles ORDER BY articles.rank ASC, articles.name ASC")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context '.first' do
|
65
|
+
context 'without parameter' do
|
66
|
+
it 'returns first record ordered by primary key' do
|
67
|
+
expect(Article.first.class).to eql(ArticleFour)
|
68
|
+
expect(Article.see_sql_of.first).to eql("SELECT * FROM articles ORDER BY articles.name ASC LIMIT 1")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'with a parameter' do
|
73
|
+
it 'orders records by primary key and returns up to specified number of records from the beginning' do
|
74
|
+
expect(Article.first(2).map(&:class)).to eql([ArticleFour, ArticleOne])
|
75
|
+
expect(Article.see_sql_of.first(2)).to eql("SELECT * FROM articles ORDER BY articles.name ASC LIMIT 2")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
#TODO: implement .first!
|
81
|
+
|
82
|
+
context '.last' do
|
83
|
+
context 'without parameter' do
|
84
|
+
it 'returns last record ordered by primary key' do
|
85
|
+
expect(Article.last.class).to eql(ArticleTwo)
|
86
|
+
expect(Article.see_sql_of.last).to eql("SELECT * FROM articles ORDER BY articles.name DESC LIMIT 1")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'with a parameter' do
|
91
|
+
it 'orders records by primary key and returns up to specified number of records from the end' do
|
92
|
+
expect(Article.last(2).map(&:class)).to eql([ArticleThree, ArticleTwo])
|
93
|
+
expect(Article.see_sql_of.last(2)).to eql("SELECT * FROM articles ORDER BY articles.name DESC LIMIT 2")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context '.limit' do
|
99
|
+
it 'returns up to specified number of records' do
|
100
|
+
expect(Article.limit(2).all.map(&:class)).to eql([ArticleFour, ArticleOne])
|
101
|
+
expect(Article.limit(2).to_sql).to eql("SELECT * FROM articles LIMIT 2")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context '.limit.offset' do
|
106
|
+
it 'returns up to specified number of records with specified offset' do
|
107
|
+
expect(Article.limit(2).offset(1).all.map(&:class)).to eql([ArticleOne, ArticleThree])
|
108
|
+
expect(Article.limit(2).offset(1).to_sql).to eql("SELECT * FROM articles LIMIT 2 OFFSET 1")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
#TODO: implement .last!
|
113
|
+
|
114
|
+
context '.count' do
|
115
|
+
it 'returns results count using SQL SELECT COUNT()' do
|
116
|
+
expect(Article.where(author: 'The author').count).to eql(2)
|
117
|
+
expect(Article.where(author: 'The author').see_sql_of.count).to eql("SELECT COUNT(*) FROM articles WHERE author = 'The author'")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context '.where' do
|
122
|
+
it 'returns an empty array when no result' do
|
123
|
+
expect(Article.where(author: 'Inexisting author')).to be_empty
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'is possible to chain where clauses' do
|
127
|
+
request = Article.where(author: 'The author').where(name: 'Article One')
|
128
|
+
expect(request.last.class.name).to eql(ArticleOne.name)
|
129
|
+
expect(request.to_sql).to eql("SELECT * FROM articles WHERE author = 'The author' AND name = 'Article One'")
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'accepts array of values' do
|
133
|
+
expect(Article.where(name: ['Article One', 'Article Two']).to_a.size).to eql(2)
|
134
|
+
expect(Article.where(name: ['Article One', 'Article Two']).to_sql).to eql("SELECT * FROM articles WHERE name IN (\"Article One\",\"Article Two\")")
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'accepts strings' do
|
138
|
+
expect(Article.where("name = 'Article Two'").first.class).to eql(ArticleTwo)
|
139
|
+
expect(Article.where("name = 'Article Two'").to_sql).to eql("SELECT * FROM articles WHERE name = 'Article Two'")
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'accepts strings followed by an anonymous parameters' do
|
143
|
+
expect(Article.where("name = ?", 'Article Two').first.class).to eql(ArticleTwo)
|
144
|
+
expect(Article.where("name = ?", 'Article Two').to_sql).to eql("SELECT * FROM articles WHERE name = \"Article Two\"")
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'accepts strings followed by several anonymous parameters' do
|
148
|
+
expect(Article.where("name = ? AND author = ?", 'Article Two', 'The author').first.class).to eql(ArticleTwo)
|
149
|
+
expect(Article.where("name = ? AND author = ?", 'Article Two', 'The author').to_sql).to eql("SELECT * FROM articles WHERE name = \"Article Two\" AND author = \"The author\"")
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'accepts strings followed by a hash of named parameters' do
|
153
|
+
expect(Article.where("name = :name AND author = :author", {name: 'Article Two', author: 'The author'}).first.class).to eql(ArticleTwo)
|
154
|
+
expect(Article.where("name = :name AND author = :author", {name: 'Article Two', author: 'The author'}).to_sql).to eql("SELECT * FROM articles WHERE name = \"Article Two\" AND author = \"The author\"")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
context '.where.not' do
|
159
|
+
it 'uses SQL != operator' do
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'is possible to chain where and where.not clauses' do
|
163
|
+
request = Article.where(author: 'The author').where.not(name: 'Article Two')
|
164
|
+
expect(request.last.class.name).to eql(ArticleOne.name)
|
165
|
+
expect(request.to_sql).to eql("SELECT * FROM articles WHERE author = 'The author' AND name != 'Article Two'")
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'is possible to chain where.not and where.not clauses' do
|
169
|
+
request = Article.where.not(author: ['The author', 'Me']).where.not(name: 'Article Two')
|
170
|
+
expect(request.last.class.name).to eql(ArticleThree.name)
|
171
|
+
expect(request.to_sql).to eql("SELECT * FROM articles WHERE author NOT IN (\"The author\",\"Me\") AND name != 'Article Two'")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context '.find_by' do
|
176
|
+
it 'limits result to 1 record' do
|
177
|
+
expect(Article.find_by(author: 'The author').class.name).to eql(ArticleOne.name)
|
178
|
+
expect(Article.see_sql_of.find_by(author: 'The author')).to eql("SELECT * FROM articles WHERE author = 'The author' LIMIT 1")
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'returns nil when no result' do
|
182
|
+
expect(Article.find_by(author: 'Inexisting author')).to be_nil
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
#TODO: implement .find_by!
|
187
|
+
|
188
|
+
context '.find' do
|
189
|
+
it 'raises an error when no primary key has been set' do
|
190
|
+
expect{ Role.find('Role One') }.to raise_error(StaticRecord::NoPrimaryKey)
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'searches by primary key' do
|
194
|
+
expect(Article.find('Article Two').class.name).to eql(ArticleTwo.name)
|
195
|
+
expect(Article.see_sql_of.find('Article Two')).to eql("SELECT * FROM articles WHERE name = 'Article Two' LIMIT 1")
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'accepts array of values' do
|
199
|
+
expect(Article.find(['Article One', 'Article Two']).to_a.size).to eql(2)
|
200
|
+
expect(Article.see_sql_of.find(['Article One', 'Article Two'])).to eql("SELECT * FROM articles WHERE name IN (\"Article One\",\"Article Two\")")
|
201
|
+
end
|
202
|
+
|
203
|
+
context 'one value' do
|
204
|
+
it 'raises an error when no result' do
|
205
|
+
expect{ Article.find('Inexisting Article') }.to raise_error(StaticRecord::RecordNotFound)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
context 'several values' do
|
210
|
+
it 'raises an error when not all results are found' do
|
211
|
+
expect{ Article.find(['Article One', 'Inexisting Article']) }.to raise_error(StaticRecord::RecordNotFound)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
#TODO: implement find_each and find_in_batches
|
217
|
+
|
218
|
+
context '.take' do
|
219
|
+
context 'without parameter' do
|
220
|
+
it 'returns a single record' do
|
221
|
+
expect(Article.take.class).to be < Article
|
222
|
+
expect(Article.see_sql_of.take).to eql("SELECT * FROM articles LIMIT 1")
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
context 'with a parameter' do
|
227
|
+
it 'returns up to the specified number of records' do
|
228
|
+
expect(Article.take(2).size).to eql(2)
|
229
|
+
expect(Article.see_sql_of.take(2)).to eql("SELECT * FROM articles LIMIT 2")
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
#TODO: implement .take!
|
235
|
+
|
236
|
+
context '.or' do
|
237
|
+
it 'allows to use the SQL OR' do
|
238
|
+
expect(Article.where(author: 'Inexisting author').or.where(author: 'The author').size).to eql(2)
|
239
|
+
expect(Article.where(author: 'Inexisting author').or.where(author: 'The author').to_sql).to eql("SELECT * FROM articles WHERE author = 'Inexisting author' OR author = 'The author'")
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|