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



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

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



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

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



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

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



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

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.



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

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



75
76
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
# File 'app/sql/lib/sql_adaptor_server.rb', line 75

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



323
324
325
326
# File 'app/sql/lib/sql_adaptor_server.rb', line 323

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



329
330
331
# File 'app/sql/lib/sql_adaptor_server.rb', line 329

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

#drop_databaseObject



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

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



231
232
233
234
235
# File 'app/sql/lib/sql_adaptor_server.rb', line 231

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.



211
212
213
# File 'app/sql/lib/sql_adaptor_server.rb', line 211

def invalidate_reconcile!
  @reconcile_complete = false
end

#query(collection, query) ⇒ Object



257
258
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
# File 'app/sql/lib/sql_adaptor_server.rb', line 257

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



132
133
134
# File 'app/sql/lib/sql_adaptor_server.rb', line 132

def raw_db
  @db
end

#reconcile!Object



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

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)



227
228
229
# File 'app/sql/lib/sql_adaptor_server.rb', line 227

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

#skip_reconcileObject

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



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

def skip_reconcile
  @skip_reconcile = true

  begin
    yield
  ensure
    @skip_reconcile = false
  end
end

#update(collection, values) ⇒ Object



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

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