Class: FlexiRecord::Connection

Inherits:
Object
  • Object
show all
Includes:
MonitorMixin
Defined in:
lib/flexirecord.rb

Overview

A Connection object represents a distinct connection to the database.

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Connection

Generates a new Connection object. The passed ‘options’ are a hash, which may contain the following keys:

  • :engine (only :postgresql is supported)

  • :host

  • :port

  • :options

  • :db

  • :user

  • :pass

  • :data_types (used for the PostgreSQL interface to supply a mapping between OID’s and ruby types, will be set automatically in the hash object, if it is nil or missing)

Raises:

  • (ArgumentError)


1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
# File 'lib/flexirecord.rb', line 1219

def initialize(options)
  super()
  options.each do |key, value|
    case key
    when :engine
      @engine = value.to_sym
    when :host
      @host = value.to_s.dup.freeze if value
    when :port
      @port = value.to_i
    when :options
      @options = options.to_s.dup.freeze if value
    when :db
      @dbname = value.to_s.dup.freeze if value
    when :user
      @login = value.to_s.dup.freeze if value
    when :pass
      @passwd = value.to_s.dup.freeze if value
    when :data_types
      @data_types = value.to_hash.dup.freeze if value
    else
      raise ArgumentError, "Unknown option '#{key}'."
    end
  end
  raise ArgumentError, "No engine selected." if @engine.nil?
  raise ArgumentError, "Engine '#{@engine}' not supported." unless @engine == :postgresql
  unless @data_types
    @data_types = {}
    options[:data_types] = {}
    connection = nil
    begin
      connection = FlexiRecord::Connection.new(options)
      connection.query('SELECT "oid", "typname" FROM "pg_type" WHERE typtype=$', 'b').each do |type_record|
      @data_types[type_record.oid.to_i] = type_record.typname.to_s.freeze
      end
    ensure
      connection.close if connection
    end
    options[:data_types] = @data_types.freeze
  end
  begin
    @backend_connection = PGconn.new(@host, @port, @options, nil, @dbname, @login, @passwd)
  rescue PGError
    raise FlexiRecord::DatabaseError, $!.message
  end
  @transaction_stacklevel = 0
  @isolation_level = nil
  @spoiled = false
  nil
end

Instance Method Details

#closeObject

Closes the connection. The connection object must not be used after it is closed.



1493
1494
1495
1496
1497
# File 'lib/flexirecord.rb', line 1493

def close
  @backend_connection.close
  @backend_connection = nil
  nil
end

#execute(command_template, *command_arguments) ⇒ Object

Same as record_query, but having no ‘record_class’, theirfor returning nil.



1398
1399
1400
# File 'lib/flexirecord.rb', line 1398

def execute(command_template, *command_arguments)
  record_query(nil, command_template, *command_arguments)
end

#isolation_levelObject

Returns the isolation_level of a transaction in progress on this connection.



1410
1411
1412
1413
1414
# File 'lib/flexirecord.rb', line 1410

def isolation_level
  synchronize do
    @isolation_level
  end
end

#query(command_template, *command_arguments) ⇒ Object

Same as record_query, but using BaseRecord’s as ‘record_class’.



1393
1394
1395
# File 'lib/flexirecord.rb', line 1393

def query(command_template, *command_arguments)
  record_query(FlexiRecord::BaseRecord, command_template, *command_arguments)
end

#record_query(record_class, command_template, *command_arguments) ⇒ Object

Executes an SQL query and returns an Array of objects of ‘record_class’ (which should be a sub-class of BaseRecord). The ‘command_template’ is an SQL statement with ‘$’ placeholders to be replaced by the following ‘command_arguments’.

Raises:

  • (ArgumentError)


1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
# File 'lib/flexirecord.rb', line 1276

def record_query(record_class, command_template, *command_arguments)
  command = command_template.to_s.gsub(/\$([^0-9]|$)/) {
    rest = $1
    if command_arguments.empty?
      raise ArgumentError, "Too few arguments supplied for SQL command."
    end
    command_argument = command_arguments.shift
    if command_argument.kind_of? Float
      if command_argument.finite?
        PGconn.quote(command_argument)
      elsif command_argument.nan?
        "'NaN'"
      elsif command_argument < 0
        "'-Infinity'"
      else
        "'Infinity'"
      end
    else
      if command_argument.kind_of? Rational
        command_argument = command_argument.to_f
      elsif command_argument.kind_of? Time
        time_offset_hours = command_argument.utc_offset / 3600
        unless 3600 * time_offset_hours == command_argument.utc_offset
          raise "Time zone offset is not an integer amount of hours."
        end
        command_argument = "%s.%06i%+03i" % [command_argument.strftime("%Y-%m-%d %H:%M:%S"), command_argument.usec, time_offset_hours]
      end
      PGconn.quote(command_argument)
    end << rest
    # argument = command_arguments.shift
    # if argument.kind_of? FlexiRecord::SqlSnippet
    #   argument.to_s + $1
    # else
    #   PGconn.quote(argument) << $1
    # end
  }
  raise ArgumentError, "Too many arguments supplied for SQL command." unless command_arguments.empty?
  if $flexirecord_debug_output
    $flexirecord_debug_output << "#{command}\n"
  end
  begin
    synchronize do
      if record_class
        backend_result = @backend_connection.async_exec(command)
        result = FlexiRecord::RecordArray.new(record_class)
        for row in 0...(backend_result.num_tuples)
          record_data = {}
          for col in 0...(backend_result.num_fields)
            value_string = backend_result.getvalue(row, col)
            record_data[backend_result.fieldname(col)] = if value_string.nil?
              nil
            else
              if @data_types
                data_type = @data_types[backend_result.type(col)]
                if data_type == "bool"
                  value_string[0, 1] == 't'
                elsif ["int2", "int4", "int8"].include? data_type
                  value_string.to_i
                elsif ["text", "varchar"].include? data_type
                  value_string
                elsif data_type == "numeric"
                  unless value_string =~ /^([+-]?[0-9]*)(\.([0-9]+)?)?$/
                    raise "Unexpected format for numeric data from database."
                  end
                  if $3
                    $1.to_i + Rational($3.to_i, 10**($3.length)) * (($1[0, 1] == '-') ? -1 : 1)
                  else
                    $1.to_i
                  end
                elsif ["float4", "float8"].include? data_type
                  if value_string =~ /^NaN$/i
                    0.0 / 0.0
                  elsif value_string =~ /^Infinity$/i
                    1.0 / 0.0
                  elsif value_string =~ /^-Infinity$/i
                    -1.0 / 0.0
                  else
                    value_string.to_f
                  end
                elsif data_type == "date"
                  unless value_string =~ /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/
                    raise "Unexpected format for date from database."
                  end
                  Time.local($1.to_i, $2.to_i, $3.to_i)
                elsif data_type == "timestamp"
                  unless value_string =~ /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]*))?$/
                    raise "Unexpected format for timestamp without time zone from database."
                  end
                  Time.local($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, $8.to_i * 10 ** (6 - $8.to_s.length))
                elsif data_type == "timestamptz"
                  unless value_string =~ /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]*))?( ?[+-][0-9]{2})$/
                    aise "Unexpected format for timestamp with time zone from database."
                  end
                  (Time.utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, $8.to_i * 10 ** (6 - $8.to_s.length)) - (3600 * $9.to_i)).localtime
                else
                  value_string
                end
              else
                value_string
              end
            end
          end
          result << record_class.new(record_data, true)
        end
        return result
      else
        @backend_connection.async_exec(command)
        return nil
      end
    end
  rescue PGError
    @spoiled = true
    raise FlexiRecord::DatabaseError, $!.message
  end
end

#spoiled?Boolean

Returns true, if an error has been occured in the lifetime of this connection.

Returns:

  • (Boolean)


1271
1272
1273
# File 'lib/flexirecord.rb', line 1271

def spoiled?
  @spoiled
end

#transaction(*arguments) ⇒ Object

Starts a transaction or “nested transaction” (implemented by using save points), if already one is in progress. The arguments to this function can be an IsolationLevel constant, a symbol representing an IsolationLevel, the special symbol :unless_open or any number of BaseRecord objects, which are copied with BaseRecord#dup and restored with BaseRecord#replace, in case the transaction fails. If :unless_open is specified, and a transaction is already open, this method does nothing, except calling the given block. As partitial rollbacks on errors won’t happen in this case, it’s not recommended to use the :unless_open parameter, unless you know what you are doing. IsolationLevel’s are only regarded, if there is no transaction open yet.



1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
# File 'lib/flexirecord.rb', line 1417

def transaction(*arguments)
  isolation_level = nil
  records = []
  unless_open_mode = false
  arguments.flatten.each do |argument|
    if argument.kind_of? FlexiRecord::IsolationLevel
      isolation_level_argument = argument
    elsif argument.respond_to? :to_sym
      isolation_level_argument = FlexiRecord::IsolationLevel.by_symbol(argument)
    end
    if argument.nil?
      # nothing
    elsif argument == :unless_open
      unless_open_mode = true
    elsif isolation_level_argument
      unless isolation_level.nil?
        raise ArgumentError, "Multiple isolation levels given."
      end
      isolation_level = isolation_level_argument
    elsif argument.kind_of? Symbol
      raise ArgumentError, "Unknown symbol #{argument.inspect} given as argument to transaction method."
    else
      records << argument
    end
  end
  if unless_open_mode and not records.empty?
    raise ArgumentError, "No records may be specified, if a transaction is started 'unless_open'."
  end
  synchronize do
    if unless_open_mode and transaction?
      return yield
    end
    backup = records.collect { |record| record.dup }
    old_stacklevel = @transaction_stacklevel
    success = true
    begin
      if @transaction_stacklevel == 0
        @isolation_level = isolation_level
        if isolation_level
          execute("BEGIN TRANSACTION ISOLATION LEVEL #{isolation_level}")
        else
          execute('BEGIN TRANSACTION')
        end
      else
        execute('SAVEPOINT "FlexiRecord_' << @transaction_stacklevel.to_s << '"')
      end
      @transaction_stacklevel += 1
      return yield
    rescue StandardError
      success = false
      raise $!
    ensure
      @transaction_stacklevel = old_stacklevel
      if success
        if old_stacklevel == 0
          execute('COMMIT TRANSACTION')
        else
          execute('RELEASE SAVEPOINT "FlexiRecord_' << @transaction_stacklevel.to_s << '"')
        end
      else
        if old_stacklevel == 0
          @isolation_level = 0
          execute('ROLLBACK TRANSACTION')
        else
          execute('ROLLBACK TO SAVEPOINT "FlexiRecord_' << @transaction_stacklevel.to_s << '"')
          execute('RELEASE SAVEPOINT "FlexiRecord_' << @transaction_stacklevel.to_s << '"')
        end
        backup.each_index do |record_index|
          records[record_index].replace(backup[record_index])
        end
      end
    end
  end
end

#transaction?Boolean

Returns true, if a transaction is in progress on this connection.

Returns:

  • (Boolean)


1403
1404
1405
1406
1407
# File 'lib/flexirecord.rb', line 1403

def transaction?
  synchronize do
    @transaction_stacklevel > 0
  end
end