static-record 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +112 -0
  4. data/Rakefile +22 -0
  5. data/app/assets/javascripts/static_record/application.js +13 -0
  6. data/app/assets/stylesheets/static_record/application.css +15 -0
  7. data/app/controllers/static_record/application_controller.rb +4 -0
  8. data/app/helpers/static_record/application_helper.rb +4 -0
  9. data/app/models/concerns/query_building_concern.rb +103 -0
  10. data/app/models/concerns/sqlite_storing_concern.rb +61 -0
  11. data/app/models/static_record/base.rb +61 -0
  12. data/app/models/static_record/predicates.rb +100 -0
  13. data/app/models/static_record/querying.rb +25 -0
  14. data/app/models/static_record/relation.rb +110 -0
  15. data/app/views/layouts/static_record/application.html.erb +14 -0
  16. data/config/routes.rb +2 -0
  17. data/lib/static_record.rb +5 -0
  18. data/lib/static_record/engine.rb +5 -0
  19. data/lib/static_record/exceptions.rb +5 -0
  20. data/lib/static_record/version.rb +3 -0
  21. data/lib/tasks/static_record_tasks.rake +4 -0
  22. data/spec/models/static_record/base_spec.rb +10 -0
  23. data/spec/models/static_record/querying_spec.rb +8 -0
  24. data/spec/models/static_record/relation_spec.rb +242 -0
  25. data/spec/rails_helper.rb +15 -0
  26. data/spec/spec_helper.rb +18 -0
  27. data/spec/test_app/app/controllers/application_controller.rb +5 -0
  28. data/spec/test_app/app/helpers/application_helper.rb +2 -0
  29. data/spec/test_app/app/models/article.rb +6 -0
  30. data/spec/test_app/app/models/articles/article_four.rb +5 -0
  31. data/spec/test_app/app/models/articles/article_one.rb +5 -0
  32. data/spec/test_app/app/models/articles/article_three.rb +5 -0
  33. data/spec/test_app/app/models/articles/article_two.rb +5 -0
  34. data/spec/test_app/app/models/role.rb +5 -0
  35. data/spec/test_app/app/models/roles/role_one.rb +4 -0
  36. data/spec/test_app/config/application.rb +32 -0
  37. data/spec/test_app/config/boot.rb +5 -0
  38. data/spec/test_app/config/environment.rb +5 -0
  39. data/spec/test_app/config/environments/development.rb +41 -0
  40. data/spec/test_app/config/environments/production.rb +79 -0
  41. data/spec/test_app/config/environments/test.rb +42 -0
  42. data/spec/test_app/config/initializers/assets.rb +11 -0
  43. data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
  44. data/spec/test_app/config/initializers/cookies_serializer.rb +3 -0
  45. data/spec/test_app/config/initializers/filter_parameter_logging.rb +4 -0
  46. data/spec/test_app/config/initializers/inflections.rb +16 -0
  47. data/spec/test_app/config/initializers/mime_types.rb +4 -0
  48. data/spec/test_app/config/initializers/session_store.rb +3 -0
  49. data/spec/test_app/config/initializers/wrap_parameters.rb +14 -0
  50. data/spec/test_app/config/routes.rb +4 -0
  51. data/spec/test_app/db/schema.rb +16 -0
  52. 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>
@@ -0,0 +1,2 @@
1
+ StaticRecord::Engine.routes.draw do
2
+ end
@@ -0,0 +1,5 @@
1
+ require 'static_record/engine'
2
+ require 'static_record/exceptions'
3
+
4
+ module StaticRecord
5
+ end
@@ -0,0 +1,5 @@
1
+ module StaticRecord
2
+ class Engine < ::Rails::Engine # :nodoc:
3
+ isolate_namespace StaticRecord
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module StaticRecord
2
+ class RecordNotFound < RuntimeError; end
3
+ class ReservedAttributeName < RuntimeError; end
4
+ class NoPrimaryKey < RuntimeError; end
5
+ end
@@ -0,0 +1,3 @@
1
+ module StaticRecord
2
+ VERSION = '1.0.0.pre'.freeze
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :static_record do
3
+ # # Task goes here
4
+ # end
@@ -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,8 @@
1
+ require 'spec_helper'
2
+ require 'rails_helper'
3
+
4
+ RSpec.describe StaticRecord::Querying, :type => :model do
5
+ it 'delegates requests to StaticRecord::Relation' do
6
+ expect(Article.where(author: 'The author')).to be_a(StaticRecord::Relation)
7
+ end
8
+ 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