Skip to content

Commit de90964

Browse files
volkanunsalnettofarah
authored andcommitted
Adding adapters for MySQL and Postgres
1 parent f43f2c0 commit de90964

File tree

8 files changed

+169
-54
lines changed

8 files changed

+169
-54
lines changed

lib/polo/adapters/mysql.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module Polo
2+
module Adapters
3+
class MySQL
4+
def on_duplicate_key_update(inserts, records)
5+
insert_and_record = inserts.zip(records)
6+
insert_and_record.map do |insert, record|
7+
values_syntax = record.attributes.keys.map do |key|
8+
"#{key} = VALUES(#{key})"
9+
end
10+
11+
on_dup_syntax = "ON DUPLICATE KEY UPDATE #{values_syntax.join(', ')}"
12+
13+
"#{insert} #{on_dup_syntax}"
14+
end
15+
end
16+
17+
def ignore_transform(inserts, records)
18+
inserts.map do |insert|
19+
insert.gsub("INSERT", "INSERT IGNORE")
20+
end
21+
end
22+
end
23+
end
24+
end

lib/polo/adapters/postgres.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module Polo
2+
module Adapters
3+
class Postgres
4+
# TODO: Implement UPSERT. This command became available in 9.1.
5+
#
6+
# See: http://www.the-art-of-web.com/sql/upsert/
7+
def on_duplicate_key_update(inserts, records)
8+
raise 'on_duplicate: :override is not currently supported in the PostgreSQL adapter'
9+
end
10+
11+
# Internal: Transforms an INSERT with PostgreSQL-specific syntax. Ignores
12+
# records that alread exist in the table. To do this, it uses
13+
# a heuristic, i.e. checks if there is a record with the same id
14+
# in the table.
15+
# See: http://stackoverflow.com/a/6527838/32816
16+
#
17+
# inserts - The Array of INSERT statements.
18+
# records - The Array of Arel objects.
19+
#
20+
# Returns the Array of transformed INSERT statements.
21+
def ignore_transform(inserts, records)
22+
insert_and_record = inserts.zip(records)
23+
insert_and_record.map do |insert, record|
24+
table_name = record.class.arel_table.name
25+
id = record[:id]
26+
insert = insert.gsub(/VALUES \((.+)\)$/m, 'SELECT \\1')
27+
insert << " WHERE NOT EXISTS (SELECT 1 FROM #{table_name} WHERE id=#{id});"
28+
end
29+
end
30+
end
31+
end
32+
end

lib/polo/configuration.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
module Polo
22

33
class Configuration
4-
attr_reader :on_duplicate_strategy, :blacklist
4+
attr_reader :on_duplicate_strategy, :blacklist, :adapter
55

66
def initialize(options={})
7-
options = { on_duplicate: nil, obfuscate: {} }.merge(options)
7+
options = { on_duplicate: nil, obfuscate: {}, adapter: :mysql }.merge(options)
8+
@adapter = options[:adapter]
89
@on_duplicate_strategy = options[:on_duplicate]
910
obfuscate(options[:obfuscate])
1011
end
@@ -33,5 +34,9 @@ def obfuscate(*fields)
3334
def on_duplicate(strategy)
3435
@on_duplicate_strategy = strategy
3536
end
37+
38+
def set_adapter(db)
39+
@adapter = db
40+
end
3641
end
3742
end

lib/polo/sql_translator.rb

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,47 @@
11
require 'active_record'
22
require 'polo/configuration'
3+
require 'polo/adapters/mysql'
4+
require 'polo/adapters/postgres'
35

46
module Polo
57
class SqlTranslator
68

7-
def initialize(object, configuration=Configuration.new)
9+
def initialize(object, configuration = Configuration.new)
810
@record = object
911
@configuration = configuration
10-
end
11-
12-
def to_sql
13-
records = Array.wrap(@record)
1412

15-
sqls = records.map do |record|
16-
raw_sql(record)
17-
end
18-
19-
if @configuration.on_duplicate_strategy == :ignore
20-
sqls = ignore_transform(sqls)
13+
case @configuration.adapter
14+
when :mysql
15+
@adapter = Polo::Adapters::MySQL.new
16+
when :postgres
17+
@adapter = Polo::Adapters::Postgres.new
18+
else
19+
raise "Unknown SQL adapter: #{@configuration.adapter}"
2120
end
21+
end
2222

23-
if @configuration.on_duplicate_strategy == :override
24-
sqls = on_duplicate_key_update(sqls, records)
23+
def to_sql
24+
case @configuration.on_duplicate_strategy
25+
when :ignore
26+
@adapter.ignore_transform(inserts, records)
27+
when :override
28+
@adapter.on_duplicate_key_update(inserts, records)
29+
else inserts
2530
end
26-
27-
sqls
2831
end
2932

30-
private
31-
32-
def on_duplicate_key_update(sqls, records)
33-
insert_and_record = sqls.zip(records)
34-
insert_and_record.map do |insert, record|
35-
values_syntax = record.attributes.keys.map do |key|
36-
"#{key} = VALUES(#{key})"
37-
end
38-
39-
on_dup_syntax = "ON DUPLICATE KEY UPDATE #{values_syntax.join(', ')}"
40-
41-
"#{insert} #{on_dup_syntax}"
42-
end
33+
def records
34+
Array.wrap(@record)
4335
end
4436

45-
def ignore_transform(inserts)
46-
inserts.map do |insert|
47-
insert.gsub("INSERT", "INSERT IGNORE")
37+
def inserts
38+
records.map do |record|
39+
raw_sql(record)
4840
end
4941
end
5042

43+
private
44+
5145
# Internal: Generates an insert SQL statement for a given record
5246
#
5347
# It will make use of the InsertManager class from the Arel gem to generate

spec/adapters/mysql_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
require 'spec_helper'
2+
3+
describe Polo::Adapters::MySQL do
4+
5+
let(:adapter) { Polo::Adapters::MySQL.new }
6+
7+
let(:netto) do
8+
AR::Chef.where(name: 'Netto').first
9+
end
10+
11+
before(:all) do
12+
TestData.create_netto
13+
end
14+
15+
let(:translator) { Polo::SqlTranslator.new(netto, Polo::Configuration.new(adapter: :mysql)) }
16+
17+
describe '#ignore_transform' do
18+
it 'appends the IGNORE command after INSERTs' do
19+
insert_netto = [%q{INSERT IGNORE INTO "chefs" ("id", "name", "email") VALUES (1, 'Netto', 'nettofarah@gmail.com')}]
20+
21+
records = translator.records
22+
inserts = translator.inserts
23+
translated_sql = adapter.ignore_transform(inserts, records)
24+
expect(translated_sql).to eq(insert_netto)
25+
end
26+
end
27+
28+
29+
describe '#on_duplicate_key_update' do
30+
it 'appends ON DUPLICATE KEY UPDATE with all values to the current INSERT statement' do
31+
insert_netto = [
32+
%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)}
33+
]
34+
35+
inserts = translator.inserts
36+
records = translator.records
37+
translated_sql = adapter.on_duplicate_key_update(inserts, records)
38+
expect(translated_sql).to eq(insert_netto)
39+
end
40+
end
41+
end

spec/adapters/postgres_spec.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
require 'spec_helper'
2+
3+
describe Polo::Adapters::Postgres do
4+
5+
let(:adapter) { Polo::Adapters::Postgres.new }
6+
7+
let(:netto) do
8+
AR::Chef.where(name: 'Netto').first
9+
end
10+
11+
before(:all) do
12+
TestData.create_netto
13+
end
14+
15+
let(:translator) { Polo::SqlTranslator.new(netto, Polo::Configuration.new(adapter: :postgres)) }
16+
17+
describe '#on_duplicate_key_update' do
18+
it 'should raise an error' do
19+
expect { adapter.on_duplicate_key_update(double(), double()) }.to raise_error('on_duplicate: :override is not currently supported in the PostgreSQL adapter')
20+
end
21+
end
22+
23+
describe '#ignore_transform' do
24+
it 'transforms INSERT by appending WHERE NOT EXISTS clause' do
25+
26+
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);}]
27+
28+
records = translator.records
29+
inserts = translator.inserts
30+
translated_sql = adapter.ignore_transform(inserts, records)
31+
expect(translated_sql).to eq(insert_netto)
32+
end
33+
end
34+
end

spec/configuration_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
describe Polo::Configuration do
44

5+
describe 'adapter' do
6+
it 'defaults to mysql' do
7+
expect(Polo.defaults.adapter).to be :mysql
8+
end
9+
end
10+
511
describe 'on_duplicate' do
612
it 'defaults to nothing' do
713
expect(Polo.defaults.on_duplicate_strategy).to be nil

spec/sql_translator_spec.rb

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,4 @@
2121
recipe_to_sql = Polo::SqlTranslator.new(recipe).to_sql.first
2222
expect(recipe_to_sql).to include(%q{'{"quality":"ok"}'}) # JSON, not YAML
2323
end
24-
25-
describe "options" do
26-
describe "on_duplicate: :ignore" do
27-
it 'uses INSERT IGNORE as opposed to regular inserts' do
28-
insert_netto = [%q{INSERT IGNORE INTO "chefs" ("id", "name", "email") VALUES (1, 'Netto', 'nettofarah@gmail.com')}]
29-
netto_to_sql = Polo::SqlTranslator.new(netto, Polo::Configuration.new(on_duplicate: :ignore)).to_sql
30-
expect(netto_to_sql).to eq(insert_netto)
31-
end
32-
end
33-
34-
describe "on_duplicate: :override" do
35-
it 'appends ON DUPLICATE KEY UPDATE to the statement' do
36-
insert_netto = [
37-
%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)}
38-
]
39-
40-
netto_to_sql = Polo::SqlTranslator.new(netto, Polo::Configuration.new(on_duplicate: :override)).to_sql
41-
expect(netto_to_sql).to eq(insert_netto)
42-
end
43-
end
44-
end
4524
end

0 commit comments

Comments
 (0)