volt-sql 0.0.1
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/.gitignore +17 -0
- data/Gemfile +10 -0
- data/README.md +31 -0
- data/Rakefile +1 -0
- data/app/sql/config/dependencies.rb +1 -0
- data/app/sql/config/initializers/boot.rb +5 -0
- data/app/sql/config/routes.rb +1 -0
- data/app/sql/controllers/main_controller.rb +5 -0
- data/app/sql/lib/field_updater.rb +103 -0
- data/app/sql/lib/helper.rb +121 -0
- data/app/sql/lib/index_updater.rb +82 -0
- data/app/sql/lib/migration.rb +52 -0
- data/app/sql/lib/migration_generator.rb +44 -0
- data/app/sql/lib/reconcile.rb +51 -0
- data/app/sql/lib/sql_adaptor_client.rb +110 -0
- data/app/sql/lib/sql_adaptor_server.rb +438 -0
- data/app/sql/lib/sql_logger.rb +13 -0
- data/app/sql/lib/store_persistor.rb +10 -0
- data/app/sql/lib/table_reconcile.rb +123 -0
- data/app/sql/lib/where_call.rb +64 -0
- data/app/sql/views/main/index.html +2 -0
- data/config/db/development.db +0 -0
- data/config/db/test.db +0 -0
- data/lib/volt/sql/version.rb +5 -0
- data/lib/volt/sql.rb +27 -0
- data/spec/postgres/lib/helpers.rb +22 -0
- data/spec/postgres/lib/index_updater_spec.rb +88 -0
- data/spec/postgres/lib/postgres_where_call_spec.rb +59 -0
- data/spec/postgres/lib/table_reconcile_spec.rb +178 -0
- data/spec/spec_helper.rb +36 -0
- data/volt-sql.gemspec +23 -0
- metadata +122 -0
@@ -0,0 +1,438 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'sequel/extensions/pg_json'
|
3
|
+
require 'sql/lib/where_call'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'thread'
|
6
|
+
require 'sql/lib/migration'
|
7
|
+
require 'sql/lib/migration_generator'
|
8
|
+
require 'sql/lib/reconcile'
|
9
|
+
require 'sql/lib/sql_logger'
|
10
|
+
require 'sql/lib/helper'
|
11
|
+
require 'volt/utils/data_transformer'
|
12
|
+
|
13
|
+
|
14
|
+
module Volt
|
15
|
+
class DataStore
|
16
|
+
class SqlAdaptorServer < BaseAdaptorServer
|
17
|
+
include Volt::Sql::SqlLogger
|
18
|
+
|
19
|
+
attr_reader :db, :sql_db, :adaptor_name
|
20
|
+
|
21
|
+
# :reconcile_complete is set to true after the initial load and reconcile.
|
22
|
+
# Any models created after this point will attempt to be auto-reconciled.
|
23
|
+
# This is mainly used for specs.
|
24
|
+
attr_reader :reconcile_complete
|
25
|
+
|
26
|
+
|
27
|
+
def initialize(volt_app)
|
28
|
+
@volt_app = volt_app
|
29
|
+
@db_mutex = Mutex.new
|
30
|
+
|
31
|
+
Sequel.default_timezone = :utc
|
32
|
+
super
|
33
|
+
|
34
|
+
# Add an invalidation callback when we add a new field. This ensures
|
35
|
+
# any fields added after the first db call (which triggers the
|
36
|
+
# reconcile) will reconcile again.
|
37
|
+
inv_reconcile = lambda { invalidate_reconcile! }
|
38
|
+
Volt::Model.instance_eval do
|
39
|
+
alias :__field__ :field
|
40
|
+
|
41
|
+
define_singleton_method(:field) do |*args, &block|
|
42
|
+
# Call original field
|
43
|
+
result = __field__(*args, &block)
|
44
|
+
inv_reconcile.call
|
45
|
+
result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
@volt_app.on('boot') do
|
50
|
+
# call ```db``` to reconcile
|
51
|
+
@app_booted = true
|
52
|
+
db
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Set db_driver on public
|
57
|
+
Volt.configure do |config|
|
58
|
+
config.db_driver = 'sqlite'
|
59
|
+
end
|
60
|
+
|
61
|
+
# check if the database can be connected to.
|
62
|
+
# @return Boolean
|
63
|
+
def connected?
|
64
|
+
return true
|
65
|
+
begin
|
66
|
+
db
|
67
|
+
|
68
|
+
true
|
69
|
+
rescue ::Sequel::ConnectionFailure => e
|
70
|
+
false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def db(skip_reconcile=false)
|
75
|
+
if @db && @reconcile_complete
|
76
|
+
return @db
|
77
|
+
end
|
78
|
+
|
79
|
+
@db_mutex.synchronize do
|
80
|
+
unless @db
|
81
|
+
begin
|
82
|
+
@adaptor_name = connect_to_db
|
83
|
+
|
84
|
+
@db.test_connection
|
85
|
+
rescue Sequel::DatabaseConnectionError => e
|
86
|
+
if e.message =~ /does not exist/
|
87
|
+
create_missing_database
|
88
|
+
else
|
89
|
+
raise
|
90
|
+
end
|
91
|
+
|
92
|
+
rescue Sequel::AdapterNotFound => e
|
93
|
+
missing_gem = e.message.match(/LoadError[:] cannot load such file -- ([^ ]+)$/)
|
94
|
+
if missing_gem
|
95
|
+
helpers = {
|
96
|
+
'postgres' => "gem 'pg', '~> 0.18.2'\ngem 'pg_json', '~> 0.1.29'",
|
97
|
+
'sqlite3' => "gem 'sqlite3'",
|
98
|
+
'mysql2' => "gem 'mysql2'"
|
99
|
+
}
|
100
|
+
|
101
|
+
adaptor_name = missing_gem[1]
|
102
|
+
if (helper = helpers[adaptor_name])
|
103
|
+
helper = "\nMake sure you have the following in your gemfile:\n" + helper + "\n\n"
|
104
|
+
else
|
105
|
+
helper = ''
|
106
|
+
end
|
107
|
+
raise NameError.new("LoadError: cannot load the #{adaptor_name} gem.#{helper}")
|
108
|
+
else
|
109
|
+
raise
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
if @adaptor_name == 'postgres'
|
114
|
+
@db.extension :pg_json
|
115
|
+
# @db.extension :pg_json_ops
|
116
|
+
end
|
117
|
+
|
118
|
+
if ENV['LOG_SQL']
|
119
|
+
@db.loggers << Volt.logger
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Don't try to reconcile until the app is booted
|
124
|
+
reconcile! if !skip_reconcile && @app_booted
|
125
|
+
end
|
126
|
+
|
127
|
+
@db
|
128
|
+
end
|
129
|
+
|
130
|
+
# @param - a string URI, or a Hash of options
|
131
|
+
# @param - the adaptor name
|
132
|
+
def connect_uri_or_options
|
133
|
+
# check to see if a uri was specified
|
134
|
+
conf = Volt.config
|
135
|
+
|
136
|
+
uri = conf.db && conf.db.uri
|
137
|
+
|
138
|
+
if uri
|
139
|
+
adaptor = uri[/^([a-z]+)/]
|
140
|
+
|
141
|
+
return uri, adaptor
|
142
|
+
else
|
143
|
+
adaptor = (conf.db && conf.db.adapter || 'sqlite').to_s
|
144
|
+
if adaptor == 'sqlite'
|
145
|
+
# Make sure we have a config/db folder
|
146
|
+
FileUtils.mkdir_p('config/db')
|
147
|
+
end
|
148
|
+
|
149
|
+
data = Volt.config.db.to_h.symbolize_keys
|
150
|
+
data[:database] ||= "config/db/#{Volt.env.to_s}.db"
|
151
|
+
data[:adapter] ||= adaptor
|
152
|
+
|
153
|
+
return data, adaptor
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def connect_to_db
|
158
|
+
uri_opts, adaptor = connect_uri_or_options
|
159
|
+
|
160
|
+
@db = Sequel.connect(uri_opts)
|
161
|
+
|
162
|
+
if adaptor == 'sqlite'
|
163
|
+
@db.set_integer_booleans
|
164
|
+
end
|
165
|
+
|
166
|
+
adaptor
|
167
|
+
end
|
168
|
+
|
169
|
+
# In order to create the database, we have to connect first witout the
|
170
|
+
# database.
|
171
|
+
def create_missing_database
|
172
|
+
@db.disconnect
|
173
|
+
uri_opts, adaptor = connect_uri_or_options
|
174
|
+
|
175
|
+
if uri_opts.is_a?(String)
|
176
|
+
# A uri
|
177
|
+
*uri_opts, db_name = uri_opts.split('/')
|
178
|
+
uri_opts = uri_opts.join('/')
|
179
|
+
else
|
180
|
+
# Options hash
|
181
|
+
db_name = uri_opts.delete(:database)
|
182
|
+
end
|
183
|
+
|
184
|
+
@db = Sequel.connect(uri_opts)
|
185
|
+
|
186
|
+
# No database, try to create it
|
187
|
+
log "Database does not exist, attempting to create database #{db_name}"
|
188
|
+
@db.run("CREATE DATABASE #{db_name};")
|
189
|
+
@db.disconnect
|
190
|
+
@db = nil
|
191
|
+
|
192
|
+
connect_to_db
|
193
|
+
end
|
194
|
+
|
195
|
+
def reconcile!
|
196
|
+
unless @skip_reconcile
|
197
|
+
Sql::Reconcile.new(self, @db).reconcile!
|
198
|
+
end
|
199
|
+
|
200
|
+
@reconcile_complete = true
|
201
|
+
end
|
202
|
+
|
203
|
+
# Mark that a model changed and we need to rerun reconcile next time the
|
204
|
+
# db is accessed.
|
205
|
+
def invalidate_reconcile!
|
206
|
+
@reconcile_complete = false
|
207
|
+
end
|
208
|
+
|
209
|
+
# Used when creating a class that you don't want to reconcile after
|
210
|
+
def skip_reconcile
|
211
|
+
@skip_reconcile = true
|
212
|
+
|
213
|
+
begin
|
214
|
+
yield
|
215
|
+
ensure
|
216
|
+
@skip_reconcile = false
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Called when the db gets reset (from specs usually)
|
221
|
+
def reset!
|
222
|
+
Sql::Reconcile.new(self, @db).reset!
|
223
|
+
end
|
224
|
+
|
225
|
+
def insert(collection, values)
|
226
|
+
values = pack_values(collection, values)
|
227
|
+
|
228
|
+
db.from(collection).insert(values)
|
229
|
+
end
|
230
|
+
|
231
|
+
def update(collection, values)
|
232
|
+
values = pack_values(collection, values)
|
233
|
+
|
234
|
+
# Find the original so we can update it
|
235
|
+
table = db.from(collection)
|
236
|
+
|
237
|
+
# TODO: we should move this to a real upsert
|
238
|
+
begin
|
239
|
+
table.insert(values)
|
240
|
+
log(table.insert_sql(values))
|
241
|
+
rescue Sequel::UniqueConstraintViolation => e
|
242
|
+
# Already a record, update
|
243
|
+
id = values[:id]
|
244
|
+
log(table.where(id: id).update_sql(values))
|
245
|
+
table.where(id: id).update(values)
|
246
|
+
end
|
247
|
+
|
248
|
+
nil
|
249
|
+
end
|
250
|
+
|
251
|
+
def query(collection, query)
|
252
|
+
allowed_methods = %w(where where_with_block offset limit count)
|
253
|
+
|
254
|
+
result = db.from(collection.to_sym)
|
255
|
+
|
256
|
+
query.each do |query_part|
|
257
|
+
method_name, *args = query_part
|
258
|
+
|
259
|
+
unless allowed_methods.include?(method_name.to_s)
|
260
|
+
fail "`#{method_name}` is not part of a valid query"
|
261
|
+
end
|
262
|
+
|
263
|
+
# Symbolize Keys
|
264
|
+
args = args.map do |arg|
|
265
|
+
if arg.is_a?(Hash)
|
266
|
+
arg = self.class.nested_symbolize_keys(arg)
|
267
|
+
end
|
268
|
+
arg
|
269
|
+
end
|
270
|
+
|
271
|
+
if method_name == :where_with_block
|
272
|
+
# Where calls with block are handled differently. We have to replay
|
273
|
+
# the query that was captured on the client with QueryIdentifier
|
274
|
+
|
275
|
+
# Grab the AST that was generated from the block call on the client.
|
276
|
+
block_ast = args.pop
|
277
|
+
args = self.class.move_to_db_types(args)
|
278
|
+
|
279
|
+
result = result.where(*args) do |ident|
|
280
|
+
Sql::WhereCall.new(ident).call(block_ast)
|
281
|
+
end
|
282
|
+
else
|
283
|
+
args = self.class.move_to_db_types(args)
|
284
|
+
result = result.send(method_name, *args)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
if result.respond_to?(:all)
|
289
|
+
log(result.sql)
|
290
|
+
result = result.all.map do |hash|
|
291
|
+
# Volt expects symbol keys
|
292
|
+
hash.symbolize_keys
|
293
|
+
end#.tap {|v| puts "QUERY: " + v.inspect }
|
294
|
+
|
295
|
+
# Unpack extra values
|
296
|
+
result = unpack_values!(result)
|
297
|
+
end
|
298
|
+
|
299
|
+
result
|
300
|
+
end
|
301
|
+
|
302
|
+
def delete(collection, query)
|
303
|
+
query = self.class.move_to_db_types(query)
|
304
|
+
db.from(collection).where(query).delete
|
305
|
+
end
|
306
|
+
|
307
|
+
# remove the collection entirely
|
308
|
+
def drop_collection(collection)
|
309
|
+
db.drop_collection(collection)
|
310
|
+
end
|
311
|
+
|
312
|
+
def drop_database
|
313
|
+
RootModels.clear_temporary
|
314
|
+
|
315
|
+
# Drop all tables
|
316
|
+
db(true).drop_table(*db.tables)
|
317
|
+
|
318
|
+
RootModels.model_classes.each do |model_klass|
|
319
|
+
model_klass.reconciled = false
|
320
|
+
end
|
321
|
+
|
322
|
+
invalidate_reconcile!
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
def self.move_to_db_types(values)
|
327
|
+
values = nested_symbolize_keys(values)
|
328
|
+
|
329
|
+
values = Volt::DataTransformer.transform(values, false) do |value|
|
330
|
+
if defined?(VoltTime) && value.is_a?(VoltTime)
|
331
|
+
value.to_time
|
332
|
+
elsif value.is_a?(Symbol)
|
333
|
+
# Symbols get turned into strings
|
334
|
+
value.to_s
|
335
|
+
else
|
336
|
+
value
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
values
|
341
|
+
end
|
342
|
+
|
343
|
+
private
|
344
|
+
|
345
|
+
def self.nested_symbolize_keys(values)
|
346
|
+
DataTransformer.transform_keys(values) do |key|
|
347
|
+
key.to_sym
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# Take the values and symbolize them, and also remove any values that
|
352
|
+
# aren't going in fields and move them into extra.
|
353
|
+
#
|
354
|
+
# Then change VoltTime's to Time for Sequel
|
355
|
+
def pack_values(collection, values)
|
356
|
+
values = self.class.move_to_db_types(values)
|
357
|
+
|
358
|
+
klass = Volt::Model.class_at_path([collection])
|
359
|
+
# Find any fields in values that aren't defined with a ```field```,
|
360
|
+
# and put them into extra.
|
361
|
+
extra = {}
|
362
|
+
values = values.select do |key, value|
|
363
|
+
if klass.fields[key] || key == :id
|
364
|
+
# field is defined, keep in values
|
365
|
+
true
|
366
|
+
else
|
367
|
+
# field does not exist, move to extra
|
368
|
+
extra[key] = value
|
369
|
+
|
370
|
+
false
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Copy the extras into the values
|
375
|
+
values[:extra] = serialize(extra) if extra.present?
|
376
|
+
|
377
|
+
# Volt.logger.info("Insert into #{collection}: #{values.inspect}")
|
378
|
+
|
379
|
+
values
|
380
|
+
end
|
381
|
+
|
382
|
+
# Loop through the inputs array and change values in place to be unpacked.
|
383
|
+
# Unpacking means moving the extra field out and into the main fields.
|
384
|
+
#
|
385
|
+
# Then transform Time to VoltTime
|
386
|
+
def unpack_values!(inputs)
|
387
|
+
values = inputs.each do |values|
|
388
|
+
extra = values.delete(:extra)
|
389
|
+
|
390
|
+
if extra
|
391
|
+
extra = deserialize(extra)
|
392
|
+
self.class.nested_symbolize_keys(extra.to_h).each_pair do |key, new_value|
|
393
|
+
unless values[key]
|
394
|
+
values[key] = new_value
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
values = Volt::DataTransformer.transform(values) do |value|
|
401
|
+
if defined?(VoltTime) && value.is_a?(Time)
|
402
|
+
value = VoltTime.from_time(value)
|
403
|
+
else
|
404
|
+
value
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
values
|
409
|
+
end
|
410
|
+
|
411
|
+
end
|
412
|
+
|
413
|
+
|
414
|
+
# Specific adaptors for each database
|
415
|
+
class PostgresAdaptorServer < SqlAdaptorServer
|
416
|
+
def serialize(extra)
|
417
|
+
Sequel.pg_json(extra)
|
418
|
+
end
|
419
|
+
|
420
|
+
def deserialize(extra)
|
421
|
+
extra
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
class SqliteAdaptorServer < SqlAdaptorServer
|
426
|
+
def serialize(extra)
|
427
|
+
JSON.dump(extra)
|
428
|
+
end
|
429
|
+
|
430
|
+
def deserialize(extra)
|
431
|
+
JSON.parse(extra)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
class MysqlAdaptorServer < SqlAdaptorServer
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# Table Reconcile is responsible for migrating a table from the database state
|
2
|
+
# to the model state.
|
3
|
+
require 'sql/lib/sql_logger'
|
4
|
+
require 'sql/lib/field_updater'
|
5
|
+
require 'sql/lib/index_updater'
|
6
|
+
|
7
|
+
module Volt
|
8
|
+
module Sql
|
9
|
+
class TableReconcile
|
10
|
+
include SqlLogger
|
11
|
+
|
12
|
+
attr_reader :field_updater
|
13
|
+
|
14
|
+
def initialize(adaptor, db, model_class)
|
15
|
+
@model_class = model_class
|
16
|
+
@adaptor = adaptor
|
17
|
+
@db = db
|
18
|
+
@field_updater = FieldUpdater.new(@db, self)
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
table_name = @model_class.collection_name
|
23
|
+
|
24
|
+
ensure_table(table_name)
|
25
|
+
|
26
|
+
update_fields(@model_class, table_name)
|
27
|
+
|
28
|
+
IndexUpdater.new(@db, @model_class, table_name)
|
29
|
+
|
30
|
+
@model_class.reconciled = true
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create an empty table if one does not exist
|
34
|
+
def ensure_table(table_name)
|
35
|
+
# Check if table exists
|
36
|
+
if !@db.tables || !@db.tables.include?(table_name)
|
37
|
+
log("Creating Table #{table_name}")
|
38
|
+
adaptor_name = @adaptor.adaptor_name
|
39
|
+
@db.create_table(table_name) do
|
40
|
+
# guid id
|
41
|
+
column :id, String, :unique => true, :null => false, :primary_key => true
|
42
|
+
|
43
|
+
# When using underscore notation on a field that does not exist, the
|
44
|
+
# data will be stored in extra.
|
45
|
+
if adaptor_name == 'postgres'
|
46
|
+
# Use jsonb
|
47
|
+
column :extra, 'json'
|
48
|
+
else
|
49
|
+
column :extra, String
|
50
|
+
end
|
51
|
+
end
|
52
|
+
# TODO: there's some issue with @db.schema and no clue why, but I
|
53
|
+
# have to run this again to get .schema to work later.
|
54
|
+
@db.tables
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def sequel_class_and_opts_from_db(db_field)
|
60
|
+
vclasses, vopts = Helper.klasses_and_options_from_db(db_field)
|
61
|
+
return Helper.column_type_and_options_for_sequel(vclasses, vopts)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Pulls the db_fields out of sequel
|
65
|
+
def db_fields_for_table(table_name)
|
66
|
+
db_fields = {}
|
67
|
+
result = @db.schema(table_name).each do |col|
|
68
|
+
db_fields[col[0].to_sym] = col[1]
|
69
|
+
end
|
70
|
+
|
71
|
+
db_fields
|
72
|
+
end
|
73
|
+
|
74
|
+
def update_fields(model_class, table_name)
|
75
|
+
if (fields = model_class.fields)
|
76
|
+
db_fields = db_fields_for_table(table_name)
|
77
|
+
|
78
|
+
db_fields.delete(:id)
|
79
|
+
db_fields.delete(:extra)
|
80
|
+
|
81
|
+
orphan_fields = db_fields.keys - fields.keys
|
82
|
+
new_fields = fields.keys - db_fields.keys
|
83
|
+
|
84
|
+
# If a single field was renamed, we can auto-migrate
|
85
|
+
if (orphan_fields.size == 1 && new_fields.size == 1)
|
86
|
+
from_name = orphan_fields[0]
|
87
|
+
to_name = new_fields[0]
|
88
|
+
auto_migrate_field_rename(table_name, from_name, to_name)
|
89
|
+
|
90
|
+
# Move in start fields
|
91
|
+
db_fields[to_name] = db_fields.delete(from_name)
|
92
|
+
end
|
93
|
+
|
94
|
+
if new_fields.size == 0 && orphan_fields.size > 0
|
95
|
+
# one or more fields were removed
|
96
|
+
orphan_fields.each do |field_name|
|
97
|
+
@field_updater.auto_migrate_remove_field(table_name, field_name, db_fields[field_name])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
if orphan_fields.size == 0
|
103
|
+
|
104
|
+
fields.each do |name, klass_and_opts|
|
105
|
+
name = name.to_sym
|
106
|
+
klasses, opts = klass_and_opts
|
107
|
+
|
108
|
+
# Get the original state for the field
|
109
|
+
db_field = db_fields.delete(name)
|
110
|
+
|
111
|
+
@field_updater.update_field(model_class, table_name, db_field, name, klasses, opts)
|
112
|
+
end
|
113
|
+
|
114
|
+
# remove any fields we didn't see in the models
|
115
|
+
end
|
116
|
+
else
|
117
|
+
# Either >1 orphaned fields, or more than one new fields
|
118
|
+
raise "Could not auto migrate #{table_name}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Queries on the client side can be captured using block syntax. On the client
|
2
|
+
# a Volt::QueryIdentifier is passed to the block. It will create a AST for
|
3
|
+
# queries which is sent.
|
4
|
+
#
|
5
|
+
# Sql::WhereCall can reply the query to the underlying database engine (in
|
6
|
+
# this case, sequel)
|
7
|
+
|
8
|
+
module Volt
|
9
|
+
module Sql
|
10
|
+
class WhereCall
|
11
|
+
VALID_METHODS = ['&', '|', '~', '>', '<', '>=', '<=' , '=~', '!~']
|
12
|
+
def initialize(ident)
|
13
|
+
@ident = ident
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(ast)
|
17
|
+
walk(ast)
|
18
|
+
end
|
19
|
+
|
20
|
+
def walk(ast)
|
21
|
+
if ast.is_a?(Array) && !ast.is_a?(Sequel::SQL::Identifier)
|
22
|
+
op = ast.shift
|
23
|
+
|
24
|
+
case op
|
25
|
+
when 'c'
|
26
|
+
return op_call(*ast)
|
27
|
+
when 'a'
|
28
|
+
# We popped off the 'a', so we just return the array
|
29
|
+
return ast
|
30
|
+
else
|
31
|
+
raise "invalid op: #{op.inspect} - #{ast.inspect} - #{ast.is_a?(Array).inspect}"
|
32
|
+
end
|
33
|
+
else
|
34
|
+
# Not an operation, return
|
35
|
+
return ast
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def op_call(self_obj, method_name, *args)
|
40
|
+
if self_obj == 'ident'
|
41
|
+
self_obj = @ident
|
42
|
+
end
|
43
|
+
|
44
|
+
# walk on the self obj
|
45
|
+
self_obj = walk(self_obj)
|
46
|
+
|
47
|
+
# Method name security checks
|
48
|
+
case method_name
|
49
|
+
when 'send'
|
50
|
+
raise "Send is not supported in queries"
|
51
|
+
end
|
52
|
+
|
53
|
+
if method_name !~ /^[a-zA-Z0-9_]+$/ && !VALID_METHODS.include?(method_name)
|
54
|
+
raise "Only method names matching /[a-zA-Z0-9_]/ are allowed from client side queries (called `#{method_name}`)"
|
55
|
+
end
|
56
|
+
|
57
|
+
walked_args = args.map {|arg| walk(arg) }
|
58
|
+
|
59
|
+
# We have to use __send__ because send is handled differently
|
60
|
+
self_obj.__send__(method_name, *walked_args)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
Binary file
|
data/config/db/test.db
ADDED
Binary file
|
data/lib/volt/sql.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# If you need to require in code in the gem's app folder, keep in mind that
|
2
|
+
# the app is not on the load path when the gem is required. Use
|
3
|
+
# app/{gemname}/config/initializers/boot.rb to require in client or server
|
4
|
+
# code.
|
5
|
+
#
|
6
|
+
# Also, in volt apps, you typically use the lib folder in the
|
7
|
+
# app/{componentname} folder instead of this lib folder. This lib folder is
|
8
|
+
# for setting up gem code when Bundler.require is called. (or the gem is
|
9
|
+
# required.)
|
10
|
+
#
|
11
|
+
# If you need to configure volt in some way, you can add a Volt.configure block
|
12
|
+
# in this file.
|
13
|
+
|
14
|
+
|
15
|
+
Volt.configure do |config|
|
16
|
+
# Set the datastore to sql
|
17
|
+
config.public.datastore_name = 'sql'
|
18
|
+
|
19
|
+
# Include the sql component on the client
|
20
|
+
config.default_components << 'sql'
|
21
|
+
end
|
22
|
+
|
23
|
+
module Volt
|
24
|
+
module Sql
|
25
|
+
# Your code goes here...
|
26
|
+
end
|
27
|
+
end
|