Skip to content

Commit

Permalink
Adding adapters for MySQL and Postgres
Browse files Browse the repository at this point in the history
  • Loading branch information
volkanunsal authored and nettofarah committed Apr 7, 2016
1 parent f43f2c0 commit de90964
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 54 deletions.
24 changes: 24 additions & 0 deletions lib/polo/adapters/mysql.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Polo
module Adapters
class MySQL
def on_duplicate_key_update(inserts, records)
insert_and_record = inserts.zip(records)
insert_and_record.map do |insert, record|
values_syntax = record.attributes.keys.map do |key|
"#{key} = VALUES(#{key})"
end

on_dup_syntax = "ON DUPLICATE KEY UPDATE #{values_syntax.join(', ')}"

"#{insert} #{on_dup_syntax}"
end
end

def ignore_transform(inserts, records)
inserts.map do |insert|
insert.gsub("INSERT", "INSERT IGNORE")
end
end
end
end
end
32 changes: 32 additions & 0 deletions lib/polo/adapters/postgres.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module Polo
module Adapters
class Postgres
# TODO: Implement UPSERT. This command became available in 9.1.
#
# See: http://www.the-art-of-web.com/sql/upsert/
def on_duplicate_key_update(inserts, records)
raise 'on_duplicate: :override is not currently supported in the PostgreSQL adapter'
end

# Internal: Transforms an INSERT with PostgreSQL-specific syntax. Ignores
# records that alread exist in the table. To do this, it uses
# a heuristic, i.e. checks if there is a record with the same id
# in the table.
# See: http://stackoverflow.com/a/6527838/32816
#
# inserts - The Array of INSERT statements.
# records - The Array of Arel objects.
#
# Returns the Array of transformed INSERT statements.
def ignore_transform(inserts, records)
insert_and_record = inserts.zip(records)
insert_and_record.map do |insert, record|
table_name = record.class.arel_table.name
id = record[:id]
insert = insert.gsub(/VALUES \((.+)\)$/m, 'SELECT \\1')
insert << " WHERE NOT EXISTS (SELECT 1 FROM #{table_name} WHERE id=#{id});"
end
end
end
end
end
9 changes: 7 additions & 2 deletions lib/polo/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module Polo

class Configuration
attr_reader :on_duplicate_strategy, :blacklist
attr_reader :on_duplicate_strategy, :blacklist, :adapter

def initialize(options={})
options = { on_duplicate: nil, obfuscate: {} }.merge(options)
options = { on_duplicate: nil, obfuscate: {}, adapter: :mysql }.merge(options)
@adapter = options[:adapter]
@on_duplicate_strategy = options[:on_duplicate]
obfuscate(options[:obfuscate])
end
Expand Down Expand Up @@ -33,5 +34,9 @@ def obfuscate(*fields)
def on_duplicate(strategy)
@on_duplicate_strategy = strategy
end

def set_adapter(db)
@adapter = db
end
end
end
56 changes: 25 additions & 31 deletions lib/polo/sql_translator.rb
Original file line number Diff line number Diff line change
@@ -1,53 +1,47 @@
require 'active_record'
require 'polo/configuration'
require 'polo/adapters/mysql'
require 'polo/adapters/postgres'

module Polo
class SqlTranslator

def initialize(object, configuration=Configuration.new)
def initialize(object, configuration = Configuration.new)
@record = object
@configuration = configuration
end

def to_sql
records = Array.wrap(@record)

sqls = records.map do |record|
raw_sql(record)
end

if @configuration.on_duplicate_strategy == :ignore
sqls = ignore_transform(sqls)
case @configuration.adapter
when :mysql
@adapter = Polo::Adapters::MySQL.new
when :postgres
@adapter = Polo::Adapters::Postgres.new
else
raise "Unknown SQL adapter: #{@configuration.adapter}"
end
end

if @configuration.on_duplicate_strategy == :override
sqls = on_duplicate_key_update(sqls, records)
def to_sql
case @configuration.on_duplicate_strategy
when :ignore
@adapter.ignore_transform(inserts, records)
when :override
@adapter.on_duplicate_key_update(inserts, records)
else inserts
end

sqls
end

private

def on_duplicate_key_update(sqls, records)
insert_and_record = sqls.zip(records)
insert_and_record.map do |insert, record|
values_syntax = record.attributes.keys.map do |key|
"#{key} = VALUES(#{key})"
end

on_dup_syntax = "ON DUPLICATE KEY UPDATE #{values_syntax.join(', ')}"

"#{insert} #{on_dup_syntax}"
end
def records
Array.wrap(@record)
end

def ignore_transform(inserts)
inserts.map do |insert|
insert.gsub("INSERT", "INSERT IGNORE")
def inserts
records.map do |record|
raw_sql(record)
end
end

private

# Internal: Generates an insert SQL statement for a given record
#
# It will make use of the InsertManager class from the Arel gem to generate
Expand Down
41 changes: 41 additions & 0 deletions spec/adapters/mysql_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'spec_helper'

describe Polo::Adapters::MySQL do

let(:adapter) { Polo::Adapters::MySQL.new }

let(:netto) do
AR::Chef.where(name: 'Netto').first
end

before(:all) do
TestData.create_netto
end

let(:translator) { Polo::SqlTranslator.new(netto, Polo::Configuration.new(adapter: :mysql)) }

describe '#ignore_transform' do
it 'appends the IGNORE command after INSERTs' do
insert_netto = [%q{INSERT IGNORE INTO "chefs" ("id", "name", "email") VALUES (1, 'Netto', 'nettofarah@gmail.com')}]

records = translator.records
inserts = translator.inserts
translated_sql = adapter.ignore_transform(inserts, records)
expect(translated_sql).to eq(insert_netto)
end
end


describe '#on_duplicate_key_update' do
it 'appends ON DUPLICATE KEY UPDATE with all values to the current INSERT statement' do
insert_netto = [
%q{INSERT INTO "chefs" ("id", "name", "email") VALUES (1, 'Netto', 'nettofarah@gmail.com') ON DUPLICATE KEY UPDATE id = VALUES(id), name = VALUES(name), email = VALUES(email)}
]

inserts = translator.inserts
records = translator.records
translated_sql = adapter.on_duplicate_key_update(inserts, records)
expect(translated_sql).to eq(insert_netto)
end
end
end
34 changes: 34 additions & 0 deletions spec/adapters/postgres_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'spec_helper'

describe Polo::Adapters::Postgres do

let(:adapter) { Polo::Adapters::Postgres.new }

let(:netto) do
AR::Chef.where(name: 'Netto').first
end

before(:all) do
TestData.create_netto
end

let(:translator) { Polo::SqlTranslator.new(netto, Polo::Configuration.new(adapter: :postgres)) }

describe '#on_duplicate_key_update' do
it 'should raise an error' do
expect { adapter.on_duplicate_key_update(double(), double()) }.to raise_error('on_duplicate: :override is not currently supported in the PostgreSQL adapter')
end
end

describe '#ignore_transform' do
it 'transforms INSERT by appending WHERE NOT EXISTS clause' do

insert_netto = [%q{INSERT INTO "chefs" ("id", "name", "email") SELECT 1, 'Netto', 'nettofarah@gmail.com' WHERE NOT EXISTS (SELECT 1 FROM chefs WHERE id=1);}]

records = translator.records
inserts = translator.inserts
translated_sql = adapter.ignore_transform(inserts, records)
expect(translated_sql).to eq(insert_netto)
end
end
end
6 changes: 6 additions & 0 deletions spec/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

describe Polo::Configuration do

describe 'adapter' do
it 'defaults to mysql' do
expect(Polo.defaults.adapter).to be :mysql
end
end

describe 'on_duplicate' do
it 'defaults to nothing' do
expect(Polo.defaults.on_duplicate_strategy).to be nil
Expand Down
21 changes: 0 additions & 21 deletions spec/sql_translator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,4 @@
recipe_to_sql = Polo::SqlTranslator.new(recipe).to_sql.first
expect(recipe_to_sql).to include(%q{'{"quality":"ok"}'}) # JSON, not YAML
end

describe "options" do
describe "on_duplicate: :ignore" do
it 'uses INSERT IGNORE as opposed to regular inserts' do
insert_netto = [%q{INSERT IGNORE INTO "chefs" ("id", "name", "email") VALUES (1, 'Netto', 'nettofarah@gmail.com')}]
netto_to_sql = Polo::SqlTranslator.new(netto, Polo::Configuration.new(on_duplicate: :ignore)).to_sql
expect(netto_to_sql).to eq(insert_netto)
end
end

describe "on_duplicate: :override" do
it 'appends ON DUPLICATE KEY UPDATE to the statement' do
insert_netto = [
%q{INSERT INTO "chefs" ("id", "name", "email") VALUES (1, 'Netto', 'nettofarah@gmail.com') ON DUPLICATE KEY UPDATE id = VALUES(id), name = VALUES(name), email = VALUES(email)}
]

netto_to_sql = Polo::SqlTranslator.new(netto, Polo::Configuration.new(on_duplicate: :override)).to_sql
expect(netto_to_sql).to eq(insert_netto)
end
end
end
end

0 comments on commit de90964

Please sign in to comment.