static-record 1.0.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|