diff --git a/lib/polo/adapters/mysql.rb b/lib/polo/adapters/mysql.rb new file mode 100644 index 0000000..7b6fe40 --- /dev/null +++ b/lib/polo/adapters/mysql.rb @@ -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 \ No newline at end of file diff --git a/lib/polo/adapters/postgres.rb b/lib/polo/adapters/postgres.rb new file mode 100644 index 0000000..437a6f1 --- /dev/null +++ b/lib/polo/adapters/postgres.rb @@ -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 \ No newline at end of file diff --git a/lib/polo/configuration.rb b/lib/polo/configuration.rb index edf61c2..932dec5 100644 --- a/lib/polo/configuration.rb +++ b/lib/polo/configuration.rb @@ -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 @@ -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 diff --git a/lib/polo/sql_translator.rb b/lib/polo/sql_translator.rb index 26e2718..f8d3fa7 100644 --- a/lib/polo/sql_translator.rb +++ b/lib/polo/sql_translator.rb @@ -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 diff --git a/spec/adapters/mysql_spec.rb b/spec/adapters/mysql_spec.rb new file mode 100644 index 0000000..4df5044 --- /dev/null +++ b/spec/adapters/mysql_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/adapters/postgres_spec.rb b/spec/adapters/postgres_spec.rb new file mode 100644 index 0000000..98c7ef6 --- /dev/null +++ b/spec/adapters/postgres_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 46bd722..e730d0f 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -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 diff --git a/spec/sql_translator_spec.rb b/spec/sql_translator_spec.rb index f71a0e3..27287a8 100644 --- a/spec/sql_translator_spec.rb +++ b/spec/sql_translator_spec.rb @@ -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