Class: Volt::DataStore::SqlAdaptorServer

Inherits:
BaseAdaptorServer
  • Object
show all
Includes:
Sql::SqlLogger
Defined in:
app/sql/lib/sql_adaptor_server.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Sql::SqlLogger

#log

Constructor Details

#initialize(volt_app) ⇒ SqlAdaptorServer

Returns a new instance of SqlAdaptorServer.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'app/sql/lib/sql_adaptor_server.rb', line 28

def initialize(volt_app)
  @volt_app = volt_app
  @db_mutex = Mutex.new

  Sequel.default_timezone = :utc
  super

  # Add an invalidation callback when we add a new field.  This ensures
  # any fields added after the first db call (which triggers the
  # reconcile) will reconcile again.
  inv_reconcile = lambda { invalidate_reconcile! }
  Volt::Model.instance_eval do
    alias :__field__ :field

    define_singleton_method(:field) do |*args, &block|
      # Call original field
      result = __field__(*args, &block)
      inv_reconcile.call
      result
    end
  end

  @volt_app.on('boot') do
    # call ```db``` to reconcile
    @app_booted = true
    db
  end
end

Instance Attribute Details

#adaptor_nameObject (readonly)

Returns the value of attribute adaptor_name.



20
21
22
# File 'app/sql/lib/sql_adaptor_server.rb', line 20

def adaptor_name
  @adaptor_name
end

#reconcile_completeObject (readonly)

:reconcile_complete is set to true after the initial load and reconcile. Any models created after this point will attempt to be auto-reconciled. This is mainly used for specs.



25
26
27
# File 'app/sql/lib/sql_adaptor_server.rb', line 25

def reconcile_complete
  @reconcile_complete
end

#sql_dbObject (readonly)

Returns the value of attribute sql_db.



20
21
22
# File 'app/sql/lib/sql_adaptor_server.rb', line 20

def sql_db
  @sql_db
end

Class Method Details

.move_to_db_types(values) ⇒ Object



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'app/sql/lib/sql_adaptor_server.rb', line 349

def self.move_to_db_types(values)
  values = nested_symbolize_keys(values)

  values = Volt::DataTransformer.transform(values, false) do |value|
    if defined?(VoltTime) && value.is_a?(VoltTime)
      value.to_time
    elsif value.is_a?(Symbol)
      # Symbols get turned into strings
      value.to_s
    else
      value
    end
  end

  values
end

Instance Method Details

#connect_to_dbObject



165
166
167
168
169
170
171
172
173
174
175
# File 'app/sql/lib/sql_adaptor_server.rb', line 165

def connect_to_db
  uri_opts, adaptor = connect_uri_or_options

  @db = Sequel.connect(uri_opts)

  if adaptor == 'sqlite'
    @db.set_integer_booleans
  end

  adaptor
end

#connect_uri_or_optionsObject

Parameters:

  • -

    a string URI, or a Hash of options

  • -

    the adaptor name



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'app/sql/lib/sql_adaptor_server.rb', line 140

def connect_uri_or_options
  # check to see if a uri was specified
  conf = Volt.config

  uri = conf.db && conf.db.uri

  if uri
    adaptor = uri[/^([a-z]+)/]

    return uri, adaptor
  else
    adaptor = (conf.db && conf.db.adapter || 'sqlite').to_s
    if adaptor == 'sqlite'
      # Make sure we have a config/db folder
      FileUtils.mkdir_p('config/db')
    end

    data = Volt.config.db.to_h.symbolize_keys
    data[:database] ||= "config/db/#{Volt.env.to_s}.db"
    data[:adapter]  ||= adaptor

    return data, adaptor
  end
end

#connected?Boolean

check if the database can be connected to.

Returns:

  • (Boolean)

    Boolean



66
67
68
69
70
71
72
73
74
75
# File 'app/sql/lib/sql_adaptor_server.rb', line 66

def connected?
  return true
  begin
    db

    true
  rescue ::Sequel::ConnectionFailure => e
    false
  end
end

#create_missing_databaseObject

In order to create the database, we have to connect first witout the database.



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'app/sql/lib/sql_adaptor_server.rb', line 179

def create_missing_database
  @db.disconnect
  uri_opts, adaptor = connect_uri_or_options

  if uri_opts.is_a?(String)
    # A uri
    *uri_opts, db_name = uri_opts.split('/')
    uri_opts = uri_opts.join('/')
  else
    # Options hash
    db_name = uri_opts.delete(:database)
  end

  @db = Sequel.connect(uri_opts)

  # No database, try to create it
  log "Database does not exist, attempting to create database #{db_name}"
  @db.run("CREATE DATABASE #{db_name};")
  @db.disconnect
  @db = nil

  connect_to_db
end

#db(skip_reconcile = false) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'app/sql/lib/sql_adaptor_server.rb', line 77

def db(skip_reconcile=false)
  if @db && @reconcile_complete
    return @db
  end

  @db_mutex.synchronize do
    unless @db
      begin
        @adaptor_name = connect_to_db

        @db.test_connection
      rescue Sequel::DatabaseConnectionError => e
        if e.message =~ /does not exist/
          create_missing_database
        else
          raise
        end

      rescue Sequel::AdapterNotFound => e
        missing_gem = e.message.match(/LoadError[:] cannot load such file -- ([^ ]+)$/)
        if missing_gem
          helpers = {
            'postgres' => "gem 'pg', '~> 0.18.2'\ngem 'pg_json', '~> 0.1.29'",
            'sqlite3'  => "gem 'sqlite3'",
            'mysql2'   => "gem 'mysql2'"
          }

          adaptor_name = missing_gem[1]
          if (helper = helpers[adaptor_name])
            helper = "\nMake sure you have the following in your gemfile:\n" + helper + "\n\n"
          else
            helper = ''
          end
          raise NameError.new("LoadError: cannot load the #{adaptor_name} gem.#{helper}")
        else
          raise
        end
      end

      if @adaptor_name == 'postgres'
        @db.extension :pg_json
        # @db.extension :pg_json_ops
      end

      if ENV['LOG_SQL']
        @db.loggers << Volt.logger
      end
    end

    # Don't try to reconcile until the app is booted
    reconcile! if !skip_reconcile && @app_booted
  end

  @db
end

#delete(collection, query) ⇒ Object



325
326
327
328
# File 'app/sql/lib/sql_adaptor_server.rb', line 325

def delete(collection, query)
  query = self.class.move_to_db_types(query)
  db.from(collection).where(query).delete
end

#drop_collection(collection) ⇒ Object

remove the collection entirely



331
332
333
# File 'app/sql/lib/sql_adaptor_server.rb', line 331

def drop_collection(collection)
  db.drop_collection(collection)
end

#drop_databaseObject



335
336
337
338
339
340
341
342
343
344
345
346
# File 'app/sql/lib/sql_adaptor_server.rb', line 335

def drop_database
  RootModels.clear_temporary

  # Drop all tables
  raw_db.drop_table(*db.tables)

  RootModels.model_classes.each do |model_klass|
    model_klass.reconciled = false
  end

  invalidate_reconcile!
end

#insert(collection, values) ⇒ Object



233
234
235
236
237
# File 'app/sql/lib/sql_adaptor_server.rb', line 233

def insert(collection, values)
  values = pack_values(collection, values)

  db.from(collection).insert(values)
end

#invalidate_reconcile!Object

Mark that a model changed and we need to rerun reconcile next time the db is accessed.



213
214
215
# File 'app/sql/lib/sql_adaptor_server.rb', line 213

def invalidate_reconcile!
  @reconcile_complete = false
end

#query(collection, query) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'app/sql/lib/sql_adaptor_server.rb', line 259

def query(collection, query)
  allowed_methods = %w(where where_with_block offset order limit count)

  result = db.from(collection.to_sym)

  query.each do |query_part|
    method_name, *args = query_part

    unless allowed_methods.include?(method_name.to_s)
      fail "`#{method_name}` is not part of a valid query"
    end

    # Symbolize Keys
    args = args.map do |arg|
      if arg.is_a?(Hash)
        arg = self.class.nested_symbolize_keys(arg)
      end
      arg
    end

    if method_name == :where_with_block
      # Where calls with block are handled differently.  We have to replay
      # the query that was captured on the client with QueryIdentifier

      # Grab the AST that was generated from the block call on the client.
      block_ast, args = args[-1], args[0..-2]
      args = self.class.move_to_db_types(args)

      result = result.where(*args) do |ident|
        Sql::WhereCall.new(ident).call(block_ast)
      end
    elsif method_name == :order
      op = args[0]
      unless op.is_a?(Hash)
        raise ".order(..) should be passed a hash of field: :desc or field: :asc"
      end

      ops = op.map do |field, asc_desc|
        if asc_desc == :asc
          field
        else
          Sequel.desc(field)
        end
      end

      result = result.order(*ops)
    else
      args = self.class.move_to_db_types(args)
      result = result.send(method_name, *args)
    end
  end

  if result.respond_to?(:all)
    log(result.sql)
    result = result.all.map do |hash|
      # Volt expects symbol keys
      hash.symbolize_keys
    end#.tap {|v| puts "QUERY: " + v.inspect }

    # Unpack extra values
    result = unpack_values(result)
  end

  result
end

#raw_dbObject

Raw access to the sequel connection without auto-migrating.



134
135
136
# File 'app/sql/lib/sql_adaptor_server.rb', line 134

def raw_db
  @db
end

#reconcile!Object



203
204
205
206
207
208
209
# File 'app/sql/lib/sql_adaptor_server.rb', line 203

def reconcile!
  unless @skip_reconcile
    Sql::Reconcile.new(self, @db).reconcile!
  end

  @reconcile_complete = true
end

#reset!Object

Called when the db gets reset (from specs usually)



229
230
231
# File 'app/sql/lib/sql_adaptor_server.rb', line 229

def reset!
  Sql::Reconcile.new(self, @db).reset!
end

#skip_reconcileObject

Used when creating a class that you don’t want to reconcile after



218
219
220
221
222
223
224
225
226
# File 'app/sql/lib/sql_adaptor_server.rb', line 218

def skip_reconcile
  @skip_reconcile = true

  begin
    yield
  ensure
    @skip_reconcile = false
  end
end

#update(collection, values) ⇒ Object



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'app/sql/lib/sql_adaptor_server.rb', line 239

def update(collection, values)
  values = pack_values(collection, values)

  # Find the original so we can update it
  table = db.from(collection)

  # TODO: we should move this to a real upsert
  begin
    table.insert(values)
    log(table.insert_sql(values))
  rescue Sequel::UniqueConstraintViolation => e
    # Already a record, update
    id = values[:id]
    log(table.where(id: id).update_sql(values))
    table.where(id: id).update(values)
  end

  nil
end