tarsier 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +175 -0
- data/LICENSE.txt +21 -0
- data/README.md +984 -0
- data/exe/tarsier +7 -0
- data/lib/tarsier/application.rb +336 -0
- data/lib/tarsier/cli/commands/console.rb +87 -0
- data/lib/tarsier/cli/commands/generate.rb +85 -0
- data/lib/tarsier/cli/commands/help.rb +50 -0
- data/lib/tarsier/cli/commands/new.rb +59 -0
- data/lib/tarsier/cli/commands/routes.rb +139 -0
- data/lib/tarsier/cli/commands/server.rb +123 -0
- data/lib/tarsier/cli/commands/version.rb +14 -0
- data/lib/tarsier/cli/generators/app.rb +528 -0
- data/lib/tarsier/cli/generators/base.rb +93 -0
- data/lib/tarsier/cli/generators/controller.rb +91 -0
- data/lib/tarsier/cli/generators/middleware.rb +81 -0
- data/lib/tarsier/cli/generators/migration.rb +109 -0
- data/lib/tarsier/cli/generators/model.rb +109 -0
- data/lib/tarsier/cli/generators/resource.rb +27 -0
- data/lib/tarsier/cli/loader.rb +18 -0
- data/lib/tarsier/cli.rb +46 -0
- data/lib/tarsier/controller.rb +282 -0
- data/lib/tarsier/database.rb +588 -0
- data/lib/tarsier/errors.rb +77 -0
- data/lib/tarsier/middleware/base.rb +47 -0
- data/lib/tarsier/middleware/compression.rb +113 -0
- data/lib/tarsier/middleware/cors.rb +101 -0
- data/lib/tarsier/middleware/csrf.rb +88 -0
- data/lib/tarsier/middleware/logger.rb +74 -0
- data/lib/tarsier/middleware/rate_limit.rb +110 -0
- data/lib/tarsier/middleware/stack.rb +143 -0
- data/lib/tarsier/middleware/static.rb +124 -0
- data/lib/tarsier/model.rb +590 -0
- data/lib/tarsier/params.rb +269 -0
- data/lib/tarsier/query.rb +495 -0
- data/lib/tarsier/request.rb +274 -0
- data/lib/tarsier/response.rb +282 -0
- data/lib/tarsier/router/compiler.rb +173 -0
- data/lib/tarsier/router/node.rb +97 -0
- data/lib/tarsier/router/route.rb +119 -0
- data/lib/tarsier/router.rb +272 -0
- data/lib/tarsier/version.rb +5 -0
- data/lib/tarsier/websocket.rb +275 -0
- data/lib/tarsier.rb +167 -0
- data/sig/tarsier.rbs +485 -0
- metadata +230 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarsier
|
|
4
|
+
# Database-agnostic query builder
|
|
5
|
+
#
|
|
6
|
+
# Provides a fluent, chainable interface for building SQL queries.
|
|
7
|
+
# All methods return a new Query instance, making queries immutable.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic queries
|
|
10
|
+
# User.where(active: true)
|
|
11
|
+
# User.where(role: 'admin').order(:name)
|
|
12
|
+
# User.where(age: 18..65).limit(10)
|
|
13
|
+
#
|
|
14
|
+
# @example Chaining
|
|
15
|
+
# User.where(active: true)
|
|
16
|
+
# .where_not(role: 'guest')
|
|
17
|
+
# .order(created_at: :desc)
|
|
18
|
+
# .limit(10)
|
|
19
|
+
# .offset(20)
|
|
20
|
+
#
|
|
21
|
+
# @since 0.1.0
|
|
22
|
+
class Query
|
|
23
|
+
include Enumerable
|
|
24
|
+
|
|
25
|
+
# @return [Class] the model class
|
|
26
|
+
attr_reader :model
|
|
27
|
+
|
|
28
|
+
# @return [Array] query conditions
|
|
29
|
+
attr_reader :conditions
|
|
30
|
+
|
|
31
|
+
# @return [Array] order clauses
|
|
32
|
+
attr_reader :order_clauses
|
|
33
|
+
|
|
34
|
+
# @return [Integer, nil] limit value
|
|
35
|
+
attr_reader :limit_value
|
|
36
|
+
|
|
37
|
+
# @return [Integer, nil] offset value
|
|
38
|
+
attr_reader :offset_value
|
|
39
|
+
|
|
40
|
+
# @return [Array] columns to select
|
|
41
|
+
attr_reader :select_columns
|
|
42
|
+
|
|
43
|
+
# @return [Array] join clauses
|
|
44
|
+
attr_reader :join_clauses
|
|
45
|
+
|
|
46
|
+
# @return [Array] associations to eager load
|
|
47
|
+
attr_reader :include_associations
|
|
48
|
+
|
|
49
|
+
# Create a new query
|
|
50
|
+
#
|
|
51
|
+
# @param model [Class] the model class to query
|
|
52
|
+
def initialize(model)
|
|
53
|
+
@model = model
|
|
54
|
+
@conditions = []
|
|
55
|
+
@order_clauses = []
|
|
56
|
+
@limit_value = nil
|
|
57
|
+
@offset_value = nil
|
|
58
|
+
@select_columns = ["*"]
|
|
59
|
+
@join_clauses = []
|
|
60
|
+
@include_associations = []
|
|
61
|
+
@loaded = false
|
|
62
|
+
@records = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Add WHERE conditions
|
|
66
|
+
#
|
|
67
|
+
# @param conditions [Hash, String] conditions
|
|
68
|
+
# @return [Query] new query with conditions
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# User.where(active: true)
|
|
72
|
+
# User.where(age: 18..30)
|
|
73
|
+
# User.where('created_at > ?', 1.day.ago)
|
|
74
|
+
def where(conditions = nil, *values, **hash_conditions)
|
|
75
|
+
clone_with do |q|
|
|
76
|
+
case conditions
|
|
77
|
+
when Hash
|
|
78
|
+
conditions.each { |k, v| q.add_condition(k, :eq, v) }
|
|
79
|
+
when String
|
|
80
|
+
q.add_raw_condition(conditions, values)
|
|
81
|
+
when nil
|
|
82
|
+
hash_conditions.each { |k, v| q.add_condition(k, :eq, v) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Add WHERE NOT conditions
|
|
88
|
+
#
|
|
89
|
+
# @param conditions [Hash] conditions
|
|
90
|
+
# @return [Query] new query with conditions
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# User.where_not(role: 'guest')
|
|
94
|
+
def where_not(**conditions)
|
|
95
|
+
clone_with do |q|
|
|
96
|
+
conditions.each { |k, v| q.add_condition(k, :neq, v) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Add ORDER BY clause
|
|
101
|
+
#
|
|
102
|
+
# @param columns [Symbol, Hash] columns to order by
|
|
103
|
+
# @return [Query] new query with ordering
|
|
104
|
+
#
|
|
105
|
+
# @example
|
|
106
|
+
# User.order(:name)
|
|
107
|
+
# User.order(created_at: :desc)
|
|
108
|
+
# User.order(:role, created_at: :desc)
|
|
109
|
+
def order(*columns, **hash_order)
|
|
110
|
+
clone_with do |q|
|
|
111
|
+
columns.each { |col| q.add_order(col, :asc) }
|
|
112
|
+
hash_order.each { |col, dir| q.add_order(col, dir) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Set LIMIT
|
|
117
|
+
#
|
|
118
|
+
# @param value [Integer] limit value
|
|
119
|
+
# @return [Query] new query with limit
|
|
120
|
+
def limit(value)
|
|
121
|
+
clone_with { |q| q.instance_variable_set(:@limit_value, value) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Set OFFSET
|
|
125
|
+
#
|
|
126
|
+
# @param value [Integer] offset value
|
|
127
|
+
# @return [Query] new query with offset
|
|
128
|
+
def offset(value)
|
|
129
|
+
clone_with { |q| q.instance_variable_set(:@offset_value, value) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Set SELECT columns
|
|
133
|
+
#
|
|
134
|
+
# @param columns [Array<Symbol>] columns to select
|
|
135
|
+
# @return [Query] new query with select
|
|
136
|
+
def select(*columns)
|
|
137
|
+
clone_with { |q| q.instance_variable_set(:@select_columns, columns.flatten) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Add JOIN clause
|
|
141
|
+
#
|
|
142
|
+
# @param table [Symbol, String] table to join
|
|
143
|
+
# @param on [String] join condition
|
|
144
|
+
# @param type [Symbol] join type (:inner, :left, :right)
|
|
145
|
+
# @return [Query] new query with join
|
|
146
|
+
#
|
|
147
|
+
# @example
|
|
148
|
+
# User.joins(:posts, on: 'users.id = posts.user_id')
|
|
149
|
+
# User.joins(:posts, type: :left)
|
|
150
|
+
def joins(table, on: nil, type: :inner)
|
|
151
|
+
clone_with do |q|
|
|
152
|
+
q.join_clauses << { table: table, on: on, type: type }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Eager load associations
|
|
157
|
+
#
|
|
158
|
+
# @param associations [Array<Symbol>] associations to include
|
|
159
|
+
# @return [Query] new query with includes
|
|
160
|
+
#
|
|
161
|
+
# @example
|
|
162
|
+
# User.includes(:posts, :comments)
|
|
163
|
+
def includes(*associations)
|
|
164
|
+
clone_with do |q|
|
|
165
|
+
q.instance_variable_set(:@include_associations, associations.flatten)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Get first record
|
|
170
|
+
#
|
|
171
|
+
# @return [Model, nil]
|
|
172
|
+
def first
|
|
173
|
+
limit(1).to_a.first
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Get last record
|
|
177
|
+
#
|
|
178
|
+
# @return [Model, nil]
|
|
179
|
+
def last
|
|
180
|
+
order(@model.primary_key => :desc).limit(1).to_a.first
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Get record count
|
|
184
|
+
#
|
|
185
|
+
# @return [Integer]
|
|
186
|
+
def count
|
|
187
|
+
sql, params = build_count_sql
|
|
188
|
+
result = @model.db.get(sql, *params)
|
|
189
|
+
result[:count] || result.values.first
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Check if any records exist
|
|
193
|
+
#
|
|
194
|
+
# @return [Boolean]
|
|
195
|
+
def exists?
|
|
196
|
+
return false unless database_available?
|
|
197
|
+
|
|
198
|
+
limit(1).count > 0
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Check if no records exist
|
|
202
|
+
#
|
|
203
|
+
# @return [Boolean]
|
|
204
|
+
def empty?
|
|
205
|
+
!exists?
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Find or create a record
|
|
209
|
+
#
|
|
210
|
+
# @param attributes [Hash] attributes to find/create by
|
|
211
|
+
# @param create_with [Hash] additional attributes for creation
|
|
212
|
+
# @return [Model]
|
|
213
|
+
def find_or_create_by(attributes, create_with: {})
|
|
214
|
+
where(attributes).first || @model.create(attributes.merge(create_with))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Find or initialize a record
|
|
218
|
+
#
|
|
219
|
+
# @param attributes [Hash] attributes to find/initialize by
|
|
220
|
+
# @return [Model]
|
|
221
|
+
def find_or_initialize_by(attributes)
|
|
222
|
+
where(attributes).first || @model.new(attributes)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Iterate over records
|
|
226
|
+
#
|
|
227
|
+
# @yield [Model] each record
|
|
228
|
+
def each(&block)
|
|
229
|
+
load_records unless @loaded
|
|
230
|
+
@records.each(&block)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Convert to array
|
|
234
|
+
#
|
|
235
|
+
# @return [Array<Model>]
|
|
236
|
+
def to_a
|
|
237
|
+
load_records unless @loaded
|
|
238
|
+
@records
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Build SQL query string (for debugging)
|
|
242
|
+
#
|
|
243
|
+
# @return [String]
|
|
244
|
+
def to_sql
|
|
245
|
+
parts = []
|
|
246
|
+
|
|
247
|
+
# SELECT
|
|
248
|
+
columns = @select_columns.map { |c| c == "*" ? "*" : c.to_s }.join(", ")
|
|
249
|
+
parts << "SELECT #{columns}"
|
|
250
|
+
|
|
251
|
+
# FROM
|
|
252
|
+
parts << "FROM #{@model.table}"
|
|
253
|
+
|
|
254
|
+
# JOINS
|
|
255
|
+
@join_clauses.each do |join|
|
|
256
|
+
join_type = join[:type].to_s.upcase
|
|
257
|
+
parts << "#{join_type} JOIN #{join[:table]}"
|
|
258
|
+
parts << "ON #{join[:on]}" if join[:on]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# WHERE
|
|
262
|
+
unless @conditions.empty?
|
|
263
|
+
where_parts = @conditions.map { |c| condition_to_display_sql(c) }
|
|
264
|
+
parts << "WHERE #{where_parts.join(' AND ')}"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# ORDER BY
|
|
268
|
+
unless @order_clauses.empty?
|
|
269
|
+
order_parts = @order_clauses.map { |o| "#{o[:column]} #{o[:direction].to_s.upcase}" }
|
|
270
|
+
parts << "ORDER BY #{order_parts.join(', ')}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# LIMIT
|
|
274
|
+
parts << "LIMIT #{@limit_value}" if @limit_value
|
|
275
|
+
|
|
276
|
+
# OFFSET
|
|
277
|
+
parts << "OFFSET #{@offset_value}" if @offset_value
|
|
278
|
+
|
|
279
|
+
parts.join(" ")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Pluck specific columns
|
|
283
|
+
#
|
|
284
|
+
# @param columns [Array<Symbol>] columns to pluck
|
|
285
|
+
# @return [Array]
|
|
286
|
+
#
|
|
287
|
+
# @example
|
|
288
|
+
# User.pluck(:id)
|
|
289
|
+
# User.pluck(:id, :name)
|
|
290
|
+
def pluck(*columns)
|
|
291
|
+
select(*columns).to_a.map do |record|
|
|
292
|
+
if columns.size == 1
|
|
293
|
+
record.to_h[columns.first]
|
|
294
|
+
else
|
|
295
|
+
columns.map { |c| record.to_h[c] }
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Get IDs only
|
|
301
|
+
#
|
|
302
|
+
# @return [Array]
|
|
303
|
+
def ids
|
|
304
|
+
pluck(@model.primary_key)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Update all matching records
|
|
308
|
+
#
|
|
309
|
+
# @param attributes [Hash] attributes to update
|
|
310
|
+
# @return [Integer] number of updated records
|
|
311
|
+
def update_all(attributes)
|
|
312
|
+
where_clause, where_params = build_where
|
|
313
|
+
set_clause = attributes.keys.map { |k| "#{k} = ?" }.join(", ")
|
|
314
|
+
|
|
315
|
+
sql = "UPDATE #{@model.table} SET #{set_clause}"
|
|
316
|
+
sql += " WHERE #{where_clause}" unless where_clause.empty?
|
|
317
|
+
|
|
318
|
+
@model.db.execute(sql, *attributes.values, *where_params)
|
|
319
|
+
@model.db.send(:affected_rows)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Delete all matching records
|
|
323
|
+
#
|
|
324
|
+
# @return [Integer] number of deleted records
|
|
325
|
+
def delete_all
|
|
326
|
+
where_clause, where_params = build_where
|
|
327
|
+
|
|
328
|
+
sql = "DELETE FROM #{@model.table}"
|
|
329
|
+
sql += " WHERE #{where_clause}" unless where_clause.empty?
|
|
330
|
+
|
|
331
|
+
@model.db.execute(sql, *where_params)
|
|
332
|
+
@model.db.send(:affected_rows)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
protected
|
|
336
|
+
|
|
337
|
+
def add_condition(column, operator, value)
|
|
338
|
+
@conditions << { column: column, operator: operator, value: value }
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def add_raw_condition(sql, values)
|
|
342
|
+
@conditions << { raw: sql, values: values }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def add_order(column, direction)
|
|
346
|
+
@order_clauses << { column: column, direction: direction }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
private
|
|
350
|
+
|
|
351
|
+
def clone_with
|
|
352
|
+
clone = self.class.new(@model)
|
|
353
|
+
clone.instance_variable_set(:@conditions, @conditions.dup)
|
|
354
|
+
clone.instance_variable_set(:@order_clauses, @order_clauses.dup)
|
|
355
|
+
clone.instance_variable_set(:@limit_value, @limit_value)
|
|
356
|
+
clone.instance_variable_set(:@offset_value, @offset_value)
|
|
357
|
+
clone.instance_variable_set(:@select_columns, @select_columns.dup)
|
|
358
|
+
clone.instance_variable_set(:@join_clauses, @join_clauses.dup)
|
|
359
|
+
clone.instance_variable_set(:@include_associations, @include_associations.dup)
|
|
360
|
+
yield clone
|
|
361
|
+
clone
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def build_sql
|
|
365
|
+
parts = []
|
|
366
|
+
params = []
|
|
367
|
+
|
|
368
|
+
# SELECT
|
|
369
|
+
columns = @select_columns.map { |c| c == "*" ? "*" : c.to_s }.join(", ")
|
|
370
|
+
parts << "SELECT #{columns}"
|
|
371
|
+
|
|
372
|
+
# FROM
|
|
373
|
+
parts << "FROM #{@model.table}"
|
|
374
|
+
|
|
375
|
+
# WHERE
|
|
376
|
+
where_clause, where_params = build_where
|
|
377
|
+
unless where_clause.empty?
|
|
378
|
+
parts << "WHERE #{where_clause}"
|
|
379
|
+
params.concat(where_params)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# ORDER BY
|
|
383
|
+
unless @order_clauses.empty?
|
|
384
|
+
order_parts = @order_clauses.map { |o| "#{o[:column]} #{o[:direction].to_s.upcase}" }
|
|
385
|
+
parts << "ORDER BY #{order_parts.join(', ')}"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# LIMIT
|
|
389
|
+
parts << "LIMIT #{@limit_value}" if @limit_value
|
|
390
|
+
|
|
391
|
+
# OFFSET
|
|
392
|
+
parts << "OFFSET #{@offset_value}" if @offset_value
|
|
393
|
+
|
|
394
|
+
[parts.join(" "), params]
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def build_count_sql
|
|
398
|
+
parts = ["SELECT COUNT(*) as count", "FROM #{@model.table}"]
|
|
399
|
+
params = []
|
|
400
|
+
|
|
401
|
+
where_clause, where_params = build_where
|
|
402
|
+
unless where_clause.empty?
|
|
403
|
+
parts << "WHERE #{where_clause}"
|
|
404
|
+
params.concat(where_params)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
[parts.join(" "), params]
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def build_where
|
|
411
|
+
return ["", []] if @conditions.empty?
|
|
412
|
+
|
|
413
|
+
clauses = []
|
|
414
|
+
params = []
|
|
415
|
+
|
|
416
|
+
@conditions.each do |condition|
|
|
417
|
+
if condition[:raw]
|
|
418
|
+
clauses << condition[:raw]
|
|
419
|
+
params.concat(condition[:values])
|
|
420
|
+
else
|
|
421
|
+
clause, value = condition_to_sql(condition)
|
|
422
|
+
clauses << clause
|
|
423
|
+
params.concat(Array(value)) unless value.nil?
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
[clauses.join(" AND "), params]
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def condition_to_sql(condition)
|
|
431
|
+
column = condition[:column]
|
|
432
|
+
value = condition[:value]
|
|
433
|
+
operator = condition[:operator]
|
|
434
|
+
|
|
435
|
+
case value
|
|
436
|
+
when nil
|
|
437
|
+
[operator == :eq ? "#{column} IS NULL" : "#{column} IS NOT NULL", nil]
|
|
438
|
+
when Range
|
|
439
|
+
["#{column} BETWEEN ? AND ?", [value.begin, value.end]]
|
|
440
|
+
when Array
|
|
441
|
+
placeholders = (["?"] * value.size).join(", ")
|
|
442
|
+
op = operator == :eq ? "IN" : "NOT IN"
|
|
443
|
+
["#{column} #{op} (#{placeholders})", value]
|
|
444
|
+
else
|
|
445
|
+
op = operator == :eq ? "=" : "!="
|
|
446
|
+
["#{column} #{op} ?", [value]]
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def condition_to_display_sql(condition)
|
|
451
|
+
return condition[:raw] if condition[:raw]
|
|
452
|
+
|
|
453
|
+
column = condition[:column]
|
|
454
|
+
value = condition[:value]
|
|
455
|
+
operator = condition[:operator]
|
|
456
|
+
|
|
457
|
+
case value
|
|
458
|
+
when nil
|
|
459
|
+
operator == :eq ? "#{column} IS NULL" : "#{column} IS NOT NULL"
|
|
460
|
+
when Range
|
|
461
|
+
"#{column} BETWEEN #{quote_value(value.begin)} AND #{quote_value(value.end)}"
|
|
462
|
+
when Array
|
|
463
|
+
values = value.map { |v| quote_value(v) }.join(", ")
|
|
464
|
+
op = operator == :eq ? "IN" : "NOT IN"
|
|
465
|
+
"#{column} #{op} (#{values})"
|
|
466
|
+
else
|
|
467
|
+
op = operator == :eq ? "=" : "!="
|
|
468
|
+
"#{column} #{op} #{quote_value(value)}"
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def quote_value(value)
|
|
473
|
+
case value
|
|
474
|
+
when String then "'#{value.gsub("'", "''")}'"
|
|
475
|
+
when nil then "NULL"
|
|
476
|
+
when true then "TRUE"
|
|
477
|
+
when false then "FALSE"
|
|
478
|
+
else value.to_s
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def load_records
|
|
483
|
+
sql, params = build_sql
|
|
484
|
+
rows = @model.db.execute(sql, *params)
|
|
485
|
+
@records = rows.map { |row| @model.from_row(row) }
|
|
486
|
+
@loaded = true
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def database_available?
|
|
490
|
+
Tarsier::Database.connected?
|
|
491
|
+
rescue StandardError
|
|
492
|
+
false
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|