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.
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