rotulus 0.2.0

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.
@@ -0,0 +1,171 @@
1
+ module Rotulus
2
+ class Order
3
+ # Creates an Order object that builds the column objects in the "ORDER BY" expression
4
+ #
5
+ # @param ar_model [Class] the ActiveRecord model class name of Page#ar_relation
6
+ # @param raw_hash [Hash<Symbol, Hash>, Hash<Symbol, Symbol>, nil] the order definition of columns
7
+ #
8
+ def initialize(ar_model, raw_hash = {})
9
+ @ar_model = ar_model
10
+ @raw_hash = raw_hash&.with_indifferent_access || {}
11
+ @definition = {}
12
+
13
+ build_column_definitions
14
+ end
15
+
16
+ # Returns an array of the ordered columns
17
+ #
18
+ # @return [Array<Rotulus::Column>] ordered columns
19
+ def columns
20
+ @columns ||= definition.values
21
+ end
22
+
23
+ # Returns an array of the ordered columns' names
24
+ #
25
+ # @return [Array<String>] ordered column names
26
+ def column_names
27
+ @column_names ||= columns.map(&:name)
28
+ end
29
+
30
+ # Returns an array of column names prefixed with table name
31
+ #
32
+ # @return [Array<String>] column names prefixed with the table name
33
+ def prefixed_column_names
34
+ @prefixed_column_names ||= definition.keys.map(&:to_s)
35
+ end
36
+
37
+ # Returns the SELECT expressions to include the ordered columns in the selected columns
38
+ # of a query.
39
+ #
40
+ # @return [String] the SELECT expressions
41
+ def select_sql
42
+ columns.map(&:select_sql).join(', ')
43
+ end
44
+
45
+ # Returns a hash containing the ordered column values of a given ActiveRecord::Base record
46
+ # instance. These values will be used to generate the query to fetch the preceding/succeeding
47
+ # records of a given :record.
48
+ #
49
+ # @param record [ActiveRecord::Base] a record/row returned from Page#records
50
+ # @return [Hash] the hash containing the column values with the column name as key
51
+ def selected_values(record)
52
+ return {} if record.blank?
53
+
54
+ record.slice(*select_aliases)
55
+ .transform_keys do |a|
56
+ Column.select_alias_to_name(a)
57
+ end
58
+ end
59
+
60
+ # Returns the reversed `ORDER BY` expression(s) for the current page when the page was accessed
61
+ # via a 'previous' cursor(i.e. navigating back/ page#paged_back?)
62
+ #
63
+ # @return [String] the ORDER BY clause
64
+ def reversed_sql
65
+ Arel.sql(columns.map(&:reversed_order_sql).join(', '))
66
+ end
67
+
68
+ # Returns the ORDER BY sort expression(s) to sort the records
69
+ #
70
+ # @return [String] the ORDER BY clause
71
+ def sql
72
+ Arel.sql(columns.map(&:order_sql).join(', '))
73
+ end
74
+
75
+ # Generate a 'state' so we can detect whether the order definition has changed.
76
+ #
77
+ # @return [String] the hashed state
78
+ def state
79
+ Digest::MD5.hexdigest(Oj.dump(to_h, mode: :rails))
80
+ end
81
+
82
+ # Returns a hash containing the hash representation of the ordered columns.
83
+ #
84
+ # @return [Hash] the hash representation of the ordered columns.
85
+ def to_h
86
+ definition.each_with_object({}) do |(name, column), h|
87
+ h.merge!(column.to_h)
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :ar_model, :definition, :raw_hash
94
+
95
+ def ar_model_primary_key
96
+ ar_model.primary_key
97
+ end
98
+
99
+ def ar_table
100
+ ar_model.table_name
101
+ end
102
+
103
+ def column_model(model_override, name)
104
+ prefix = name.match(/^.*?(?=\.)/).to_s
105
+ unprefixed_name = name.split('.').last
106
+
107
+ unless model_override.nil?
108
+ return model_override unless model_override.columns_hash[unprefixed_name].nil?
109
+
110
+ raise Rotulus::InvalidColumnError.new(
111
+ "Model '#{model_override}' doesnt have a '#{name}' column. \
112
+ Tip: check the :model option value in the column's order configuration.".squish
113
+ )
114
+ end
115
+
116
+ if (prefix.blank? && !ar_model.columns_hash[name].nil?) ||
117
+ (prefix == ar_table && !ar_model.columns_hash[unprefixed_name].nil?)
118
+ return ar_model
119
+ end
120
+
121
+ raise Rotulus::InvalidColumnError.new(
122
+ "Unable determine which model the column '#{name}' belongs to. \
123
+ Tip: set/check the :model option value in the column's order configuration.".squish
124
+ )
125
+ end
126
+
127
+ def primary_key_ordered?
128
+ !definition["#{ar_table}.#{ar_model_primary_key}"].nil?
129
+ end
130
+
131
+ def build_column_definitions
132
+ raw_hash.each do |column_name, options|
133
+ column_name = column_name.to_s
134
+
135
+ unless options.is_a?(Hash)
136
+ options = if options.to_s.downcase == 'desc'
137
+ { direction: :desc }
138
+ else
139
+ { direction: :asc }
140
+ end
141
+ end
142
+
143
+ model = column_model(options[:model].presence, column_name)
144
+ column = Column.new(model,
145
+ column_name,
146
+ direction: options[:direction],
147
+ nulls: options[:nulls],
148
+ nullable: options[:nullable],
149
+ distinct: options[:distinct])
150
+ next unless definition[column.prefixed_name].nil?
151
+
152
+ definition[column.prefixed_name] = column
153
+ end
154
+
155
+ # Add tie-breaker using the PK
156
+ unless primary_key_ordered?
157
+ pk_column = Column.new(ar_model, ar_model_primary_key, direction: :asc)
158
+ definition[pk_column.prefixed_name] = pk_column
159
+ end
160
+
161
+ columns.first.as_leftmost!
162
+ end
163
+
164
+ # Returns an array of SELECT statement alias of the ordered columns
165
+ #
166
+ # @return [Array<String>] column SELECT aliases
167
+ def select_aliases
168
+ @select_aliases ||= columns.map(&:select_alias)
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,278 @@
1
+ module Rotulus
2
+ class Page
3
+ attr_reader :ar_relation, :order, :limit, :cursor
4
+
5
+ delegate :columns, to: :order, prefix: true
6
+
7
+ # Creates a new Page instance representing a subset of the given ActiveRecord::Relation
8
+ # records sorted using the given 'order' definition param.
9
+ #
10
+ # @param ar_relation [ActiveRecord::Relation] the base relation instance to be paginated
11
+ # @param order [Hash<Symbol, Hash>, Hash<Symbol, Symbol>, nil] the order definition of columns.
12
+ # Same with SQL 'ORDER BY', columns listed first takes precedence in the sorting of records.
13
+ # The order param allows 2 formats: expanded and compact. Expanded format exposes some config
14
+ # which allows more control in generating the optimal SQL queries to filter page records.
15
+ #
16
+ # Available options for each column in expanded order definition:
17
+ # * direction (Symbol) the sort direction, +:asc+ or +:desc+. Default: +:asc+.
18
+ # * nullable (Boolean) whether a null value is expected for this column in the query result.
19
+ # Note that for queries with table JOINs, a column could have a null value
20
+ # even if the column doesn't allow nulls in its table so :nullable might need to be set to
21
+ # +true+ for such cases.
22
+ # Default: +true+ if :nullable option value is nil and the
23
+ # column is defined as nullable in its table otherwise, false.
24
+ # * nulls (Symbol) null values sorting, +:first+ for +NULLS FIRST+ and
25
+ # +:last+ for +NULLS LAST+. Applicable only if column is :nullable.
26
+ # * distinct (Boolean) whether the column value is expected to be unique in the result.
27
+ # Note that for queries with table JOINs, multiple rows could have the same column
28
+ # value even if the column has a unique index defined in its table so :distinct might
29
+ # need to be set to +false+ for such cases.
30
+ # Default: true if :distinct option value is nil and the column is the PK of its
31
+ # table otherwise, false.
32
+ # * model (Class) Model where the column belongs to.
33
+ #
34
+ # @param limit [Integer] the number of records per page. Defaults to the +config.page_default_limit+.
35
+ #
36
+ # @example Using expanded order definition (Recommended)
37
+ # Rotulus::Page.new(User.all, order: { last_name: { direction: :asc },
38
+ # first_name: { direction: :desc, nulls: :last },
39
+ # ssn: { direction: :asc, distinct: true } }, limit: 3)
40
+ #
41
+ # @example Using compact order definition
42
+ # Rotulus::Page.new(User.all, order: { last_name: :asc, first_name: :desc, ssn: :asc }, limit: 3)
43
+ #
44
+ # @raise [InvalidLimit] if the :limit exceeds the configured :page_max_limit or if the
45
+ # :limit is not a positive number.
46
+ def initialize(ar_relation, order: { id: :asc }, limit: nil)
47
+ unless limit_valid?(limit)
48
+ raise InvalidLimit.new("Allowed page limit is 1 up to #{config.page_max_limit}")
49
+ end
50
+
51
+ @ar_relation = ar_relation || model.none
52
+ @order = Order.new(model, order)
53
+ @limit = (limit.presence || config.page_default_limit).to_i
54
+ end
55
+
56
+ # Return a new page pointed to the given cursor(in encoded token format)
57
+ #
58
+ # @param token [String] Base64-encoded representation of cursor.
59
+ #
60
+ # @example
61
+ # page = Rotulus::Page.new(User.where(last_name: 'Doe'), order: { first_name: :desc }, limit: 2)
62
+ # page.at('eyI6ZiI6eyJebyI6IkFjdGl2ZVN1cHBvcnQ6Okhhc2hXaXRoSW5kaWZm...')
63
+ #
64
+ # @return [Page] page instance
65
+ def at(token)
66
+ page_copy = dup
67
+ page_copy.at!(token)
68
+ page_copy
69
+ end
70
+
71
+ # Point the same page instance to the given cursor(in encoded token format)
72
+ #
73
+ # @example
74
+ # page = Rotulus::Page.new(User.where(last_name: 'Doe'), order: { first_name: :desc }, limit: 2)
75
+ # page.at!('eyI6ZiI6eyJebyI6IkFjdGl2ZVN1cHBvcnQ6Okhhc2hXaXRoSW5kaWZm...')
76
+ #
77
+ # @param token [String] Base64-encoded representation of cursor
78
+ # @return [self] page instance
79
+ def at!(token)
80
+ @cursor = token.present? ? cursor_clazz.for_page_and_token!(self, token) : nil
81
+
82
+ reload
83
+ end
84
+
85
+ # Get the records for this page. Note an extra record is fetched(limit + 1)
86
+ # to make it easier to check whether a next or previous page exists.
87
+ #
88
+ # @return [Array<ActiveRecord::Base>] array of records for this page.
89
+ def records
90
+ return loaded_records[1..limit] if paged_back? && extra_row_returned?
91
+
92
+ loaded_records[0...limit]
93
+ end
94
+
95
+ # Clear memoized records to lazily force to initiate the query again.
96
+ #
97
+ # @return [self] page instance
98
+ def reload
99
+ @loaded_records = nil
100
+
101
+ self
102
+ end
103
+
104
+ # Check if a next page exists
105
+ #
106
+ # @return [Boolean] returns true if a next page exists, otherwise returns false.
107
+ def next?
108
+ ((cursor.nil? || paged_forward?) && extra_row_returned?) || paged_back?
109
+ end
110
+
111
+ # Check if a preceding page exists
112
+ #
113
+ # @return [Boolean] returns true if a previous page exists, otherwise returns false.
114
+ def prev?
115
+ (paged_back? && extra_row_returned?) || !cursor.nil? && paged_forward?
116
+ end
117
+
118
+ # Check if the page is the 'root' page; meaning, there are no preceding pages.
119
+ #
120
+ # @return [Boolean] returns true if the page is the root page, otherwise false.
121
+ def root?
122
+ cursor.nil? || !prev?
123
+ end
124
+
125
+ # Generate the cursor token to access the next page if one exists
126
+ #
127
+ # @return [String] Base64-encoded representation of cursor
128
+ def next_token
129
+ return unless next?
130
+
131
+ record = cursor_reference_record(:next)
132
+ return if record.nil?
133
+
134
+ cursor_clazz.new(record, :next).to_token
135
+ end
136
+
137
+ # Generate the cursor token to access the previous page if one exists
138
+ #
139
+ # @return [Cursor] Base64-encoded representation of cursor
140
+ def prev_token
141
+ return unless prev?
142
+
143
+ record = cursor_reference_record(:prev)
144
+ return if record.nil?
145
+
146
+ cursor_clazz.new(record, :prev).to_token
147
+ end
148
+
149
+ # Next page instance
150
+ #
151
+ # @return [Page] the next page with records after the last record of this page.
152
+ def next
153
+ return unless next?
154
+
155
+ at next_token
156
+ end
157
+
158
+ # Previous page instance
159
+ #
160
+ # @return [Page] the previous page with records preceding the first record of this page.
161
+ def prev
162
+ return unless prev?
163
+
164
+ at prev_token
165
+ end
166
+
167
+ # Generate a hash containing the previous and next page cursor tokens
168
+ #
169
+ # @return [Hash] the hash containing the cursor tokens
170
+ def links
171
+ return {} if records.empty?
172
+
173
+ {
174
+ previous: prev_token,
175
+ next: next_token
176
+ }.delete_if { |_, token| token.nil? }
177
+ end
178
+
179
+ # Return Hashed value of this page's state so we can check whether the ar_relation's filter,
180
+ # order definition, and limit are still consistent to the cursor. see Cursor.state_valid?.
181
+ #
182
+ # @return [String] the hashed state
183
+ def state
184
+ Digest::MD5.hexdigest("#{ar_relation.to_sql}~#{order.state}~#{limit}")
185
+ end
186
+
187
+ # Returns a string showing the page's records in table form with the ordered columns
188
+ # as the columns. This method is primarily used to test/debug the pagination behavior.
189
+ #
190
+ # @return [String] table
191
+ def as_table
192
+ Rotulus::PageTableizer.new(self).tableize
193
+ end
194
+
195
+ def inspect
196
+ cursor_info = cursor.nil? ? '' : " cursor='#{cursor}'"
197
+
198
+ "#<#{self.class.name} ar_relation=#{ar_relation} order=#{order} limit=#{limit}#{cursor_info}>"
199
+ end
200
+
201
+ private
202
+
203
+ # If this is the root page or when paginating forward(#paged_forward), limit+1
204
+ # includes the first record of the next page. This lets us know whether there is a page
205
+ # succeeding the current page. When paginating backwards(#paged_back?), the limit+1 includes the
206
+ # record prior to the current page's first record(last record of the previous page, if it exists)
207
+ # -letting us know that the current page has a previous/preceding page.
208
+ def loaded_records
209
+ return @loaded_records unless @loaded_records.nil?
210
+
211
+ @loaded_records = ar_relation.where(cursor&.sql)
212
+ .order(order_by_sql)
213
+ .limit(limit + 1)
214
+ .select(*select_columns)
215
+ return @loaded_records.to_a unless paged_back?
216
+
217
+ # Reverse the returned records in case #paged_back? as the sorting is also reversed.
218
+ @loaded_records = @loaded_records.reverse
219
+ end
220
+
221
+ # Query in #loaded_records uses limit + 1. Returns true if an extra row was retrieved.
222
+ def extra_row_returned?
223
+ loaded_records.size > limit
224
+ end
225
+
226
+ def paged_back?
227
+ !!cursor&.prev?
228
+ end
229
+
230
+ def paged_forward?
231
+ !!cursor&.next?
232
+ end
233
+
234
+ def cursor_reference_record(direction)
235
+ record = direction == :next ? records.last : records.first
236
+
237
+ Record.new(self, order.selected_values(record))
238
+ end
239
+
240
+ # SELECT the ordered columns so we can use the values to generate the 'where' condition
241
+ # to filter next/prev page's records. Alias and normalize those columns so we can access
242
+ # the values using record#slice.
243
+ def select_columns
244
+ base_select_values = ar_relation.select_values.presence || [select_all_sql]
245
+ base_select_values << order.select_sql
246
+ base_select_values
247
+ end
248
+
249
+ def select_all_sql
250
+ Rotulus.db.select_all_sql(model.table_name)
251
+ end
252
+
253
+ def order_by_sql
254
+ return order.reversed_sql if paged_back?
255
+
256
+ order.sql
257
+ end
258
+
259
+ def limit_valid?(limit)
260
+ return true if limit.blank?
261
+
262
+ limit = limit.to_i
263
+ limit >= 1 && limit <= config.page_max_limit
264
+ end
265
+
266
+ def model
267
+ ar_relation.model
268
+ end
269
+
270
+ def config
271
+ @config ||= Rotulus.configuration
272
+ end
273
+
274
+ def cursor_clazz
275
+ @cursor_clazz ||= config.cursor_class
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,143 @@
1
+ module Rotulus
2
+ class PageTableizer
3
+ def initialize(page)
4
+ @page = page
5
+ end
6
+
7
+ # Returns a string showing the page's records in table form with the ordered columns
8
+ # as the columns. Some data types are formatted so output is consistent regardless of the
9
+ # DB engine in use:
10
+ # 1. Datetime - iso8601(3) formatted
11
+ # 2. Float - converted to BigDecimal before converting to string
12
+ # 3. Float/BigDecimal - '.0' fractional portion is dropped
13
+ # 4. Nil - <NULL>
14
+ #
15
+ # @return [String] table
16
+ #
17
+ # example:
18
+ # +-----------------------------------------------------------------------------------------+
19
+ # | users.first_name | users.last_name | users.email | users.id |
20
+ # +-----------------------------------------------------------------------------------------+
21
+ # | George | <NULL> | george@domain.com | 1 |
22
+ # | Jane | Smith | jane.c.smith@email.com | 2 |
23
+ # | Jane | Doe | jane.doe@email.com | 3 |
24
+ # +-----------------------------------------------------------------------------------------+
25
+ #
26
+ def tableize
27
+ return '' if records.blank?
28
+
29
+ s = ''
30
+ s << divider
31
+ s << header
32
+ s << divider
33
+ s << records.map { |record| record_to_string(record) }.join("\n")
34
+ s << "\n"
35
+ s << divider
36
+ s
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :page
42
+
43
+ def order
44
+ @order ||= page.order
45
+ end
46
+
47
+ def records
48
+ @records ||= page.records
49
+ end
50
+
51
+ def columns
52
+ @columns ||= order.prefixed_column_names
53
+ end
54
+
55
+ def col_padding
56
+ 1
57
+ end
58
+
59
+ def col_widths
60
+ return @col_widths if instance_variable_defined?(:@col_widths)
61
+
62
+ @col_widths = columns.each_with_object({}) { |name, h| h[name] = name.size + (col_padding * 2) }
63
+ records.each do |record|
64
+ values = order.selected_values(record)
65
+
66
+ values.each do |col_name, value|
67
+ width = normalize_value(value).size + (col_padding * 2)
68
+
69
+ @col_widths[col_name] = width if @col_widths[col_name] <= width
70
+ end
71
+ end
72
+
73
+ @col_widths
74
+ end
75
+
76
+ def header
77
+ @header ||= columns.reduce('') do |names, name|
78
+ "#{names}|#{name.center(col_widths[name])}"
79
+ end + " |\n"
80
+ end
81
+
82
+ def divider
83
+ @divider ||= '+' + ('-' * (header.size - 3)) + "+\n"
84
+ end
85
+
86
+ def record_to_string(record)
87
+ values = order.selected_values(record)
88
+ s = values.each_with_object('') do |(k, v), s|
89
+ v = normalize_value(v)
90
+ s << "|#{v.center(col_widths[k])}"
91
+ end
92
+
93
+ s << ' |'
94
+ end
95
+
96
+ def normalize_value(value)
97
+ return '<NULL>' if value.nil?
98
+ return "'#{value}'" if value.blank? && value.is_a?(String)
99
+
100
+ value = if Rotulus.db.name == 'sqlite'
101
+ format_sqlite_value(value)
102
+ else
103
+ format_value(value)
104
+ end
105
+
106
+ value = value.to_s if value.is_a?(BigDecimal)
107
+ value = BigDecimal(value.to_s).to_s if value.is_a?(Float)
108
+ value = value.split('.').first if value.is_a?(String) && value =~ /^\-?\d+\.0$/ # drop decimal if it's 0
109
+
110
+ value.to_s
111
+ end
112
+
113
+ def format_sqlite_value(value)
114
+ return value unless value.is_a?(String)
115
+
116
+ date_pattern1 = /^\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}$/
117
+ date_pattern2 = /^\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}$/
118
+
119
+ if value =~ date_pattern1
120
+ return DateTime.strptime(value, '%Y-%m-%d %H:%M:%S.%L')
121
+ .utc
122
+ .iso8601(3)
123
+ .gsub('+00:00', 'Z')
124
+ elsif value =~ date_pattern2
125
+ return DateTime.strptime(value, '%Y-%m-%d %H:%M:%S')
126
+ .utc
127
+ .iso8601(3)
128
+ .gsub('+00:00', 'Z')
129
+ end
130
+
131
+ value
132
+ end
133
+
134
+ def format_value(value)
135
+ value = value.utc if value.respond_to?(:utc)
136
+ if value.respond_to?(:iso8601)
137
+ value = value.method(:iso8601).arity.zero? ? value.iso8601 : value.iso8601(3)
138
+ end
139
+
140
+ value
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,66 @@
1
+ module Rotulus
2
+ class Record
3
+ attr_reader :page, :values
4
+
5
+ # Creates a new Record instance representing the first or last record of a :page
6
+ # wherein the :values include the ordered column values of the AR record. This
7
+ # instance serves as the reference point in generating the SQL query to fetch the the page's
8
+ # previous or next page's records. That is, the first record's values of a page are used
9
+ # in the WHERE condition to fetch the previous page's records and the last record's values
10
+ # are used to fetch next page's records.
11
+ #
12
+ # @param page [Rotulus::Page] the Page instance
13
+ # @param values [Hash] the ordered column values of an AR record
14
+ def initialize(page, values = {})
15
+ @page = page
16
+ @values = normalize_values(values || {})
17
+ end
18
+
19
+ # Get the records preceding or succeeding this record in respect to the sort direction of
20
+ # the ordered columns.
21
+ #
22
+ # @param direction [Symbol] `:next` to fetch succeeding records otherwise, `:prev`.
23
+ #
24
+ # @return [String] SQL 'where' condition
25
+ def sql_seek_condition(direction)
26
+ page.order_columns.reverse.reduce(nil) do |sql, column|
27
+ column_seek_sql = ColumnConditionBuilder.new(
28
+ column,
29
+ values[column.prefixed_name],
30
+ direction,
31
+ sql
32
+ ).build
33
+
34
+ parenthesize = !sql.nil? && !column.leftmost?
35
+ parenthesize ? "(#{column_seek_sql})" : column_seek_sql
36
+ end.squish
37
+ end
38
+
39
+ # Generate a 'state' so we can detect whether the record values changed/
40
+ # for integrity check. e.g. Record values prior to encoding in the cursor
41
+ # vs. the decoded values from an encoded cursor token.
42
+ #
43
+ # @return [String] the hashed state
44
+ def state
45
+ Digest::MD5.hexdigest(values.map { |k, v| "#{k}:#{v}" }.join('~'))
46
+ end
47
+
48
+ private
49
+
50
+ # Normalize values so that serialization-deserialization behaviors(e.g. values from/to encoded
51
+ # cursor data, SQL query generation) are predictable:
52
+ # 1. Date, Time, Datetime: iso8601 formatted
53
+ # 2. Float: converted to BigDecimal
54
+ def normalize_values(values)
55
+ values.transform_values! do |v|
56
+ v = v.utc if v.respond_to?(:utc)
57
+ if v.respond_to?(:iso8601)
58
+ v = v.method(:iso8601).arity.zero? ? v.iso8601 : v.iso8601(6)
59
+ end
60
+
61
+ v = BigDecimal(v.to_s) if v.is_a?(Float)
62
+ v
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ module Rotulus
2
+ VERSION = '0.2.0'
3
+ end
data/lib/rotulus.rb ADDED
@@ -0,0 +1,42 @@
1
+ require 'active_record'
2
+ require 'active_support'
3
+ require 'active_support/core_ext/string/inquiry'
4
+ require 'oj'
5
+ require 'rotulus/version'
6
+ require 'rotulus/configuration'
7
+ require 'rotulus/db/database'
8
+ require 'rotulus/record'
9
+ require 'rotulus/column'
10
+ require 'rotulus/column_condition_builder'
11
+ require 'rotulus/order'
12
+ require 'rotulus/cursor'
13
+ require 'rotulus/page_tableizer'
14
+ require 'rotulus/page'
15
+
16
+ module Rotulus
17
+ class BaseError < StandardError; end
18
+ class CursorError < BaseError; end
19
+ class InvalidCursor < CursorError; end
20
+ class ExpiredCursor < CursorError; end
21
+ class InvalidCursorDirection < CursorError; end
22
+ class InvalidLimit < BaseError; end
23
+ class ConfigurationError < BaseError; end
24
+ class InvalidColumnError < BaseError; end
25
+
26
+ def self.db
27
+ @db ||= case ActiveRecord::Base.connection.adapter_name.downcase
28
+ when /(mysql).*/
29
+ require 'rotulus/db/mysql'
30
+
31
+ Rotulus::DB::MySQL.new
32
+ when /(postgres).*/
33
+ require 'rotulus/db/postgresql'
34
+
35
+ Rotulus::DB::PostgreSQL.new
36
+ else
37
+ require 'rotulus/db/sqlite'
38
+
39
+ Rotulus::DB::SQLite.new
40
+ end
41
+ end
42
+ end