postgres_upsert 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -13
- data/.rubocop.yml +57 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +18 -3
- data/Gemfile +1 -2
- data/Gemfile.lock +171 -109
- data/README.md +8 -5
- data/app/assets/config/manifest.js +0 -0
- data/bin/setup +27 -0
- data/config/application.rb +0 -3
- data/config/database.yml +2 -0
- data/config/database.yml.travis +23 -0
- data/db/migrate/20150214192135_create_test_tables.rb +6 -1
- data/db/migrate/20150710162236_create_composite_models_table.rb +1 -1
- data/db/schema.rb +22 -17
- data/lib/postgres_upsert.rb +35 -6
- data/lib/postgres_upsert/model_to_model_adapter.rb +37 -0
- data/lib/postgres_upsert/read_adapters/active_record_adapter.rb +37 -0
- data/lib/postgres_upsert/read_adapters/file_adapter.rb +42 -0
- data/lib/postgres_upsert/read_adapters/io_adapter.rb +42 -0
- data/lib/postgres_upsert/result.rb +3 -4
- data/lib/postgres_upsert/table_writer.rb +7 -9
- data/lib/postgres_upsert/write_adapters/active_record_adapter.rb +36 -0
- data/lib/postgres_upsert/write_adapters/table_adapter.rb +56 -0
- data/lib/postgres_upsert/writer.rb +87 -69
- data/postgres_upsert.gemspec +6 -3
- data/spec/composite_key_spec.rb +0 -6
- data/spec/fixtures/comma_with_header_duplicate.csv +3 -0
- data/spec/fixtures/test_model_copy.rb +4 -0
- data/spec/from_table_spec.rb +40 -0
- data/spec/pg_upsert_csv_spec.rb +11 -10
- data/spec/rails_helper.rb +1 -0
- data/spec/spec_helper.rb +5 -2
- metadata +77 -31
- data/VERSION +0 -1
|
File without changes
|
data/bin/setup
CHANGED
|
@@ -4,6 +4,19 @@ require 'pathname'
|
|
|
4
4
|
# path to your application root.
|
|
5
5
|
APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
def silently command
|
|
9
|
+
system command + " > /dev/null 2>&1"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def successfully command
|
|
13
|
+
silently command or fail "Error in script: " + command
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def announce message
|
|
17
|
+
puts "\n== " + message + " =="
|
|
18
|
+
end
|
|
19
|
+
|
|
7
20
|
Dir.chdir APP_ROOT do
|
|
8
21
|
# This script is a starting point to setup your application.
|
|
9
22
|
# Add necessary setup steps to this file:
|
|
@@ -17,6 +30,20 @@ Dir.chdir APP_ROOT do
|
|
|
17
30
|
# system "cp config/database.yml.sample config/database.yml"
|
|
18
31
|
# end
|
|
19
32
|
|
|
33
|
+
unless silently "ls -A /usr/local/var/postgres"
|
|
34
|
+
announce "initializing postgres db in /usr/local/var/postgres"
|
|
35
|
+
silently "initdb /usr/local/var/postgres"
|
|
36
|
+
|
|
37
|
+
announce "creating superuser 'postgres'. Hope that's cool?"
|
|
38
|
+
silently "createuser -s postgres"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
unless silently "ls -A ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist"
|
|
42
|
+
announce "setting up Postgres to start on launch"
|
|
43
|
+
silently "ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents"
|
|
44
|
+
silently "launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist"
|
|
45
|
+
end
|
|
46
|
+
|
|
20
47
|
puts "\n== Preparing database =="
|
|
21
48
|
system "bin/rake db:setup"
|
|
22
49
|
|
data/config/application.rb
CHANGED
|
@@ -18,7 +18,4 @@ class Application < Rails::Application
|
|
|
18
18
|
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
|
19
19
|
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
|
20
20
|
# config.i18n.default_locale = :de
|
|
21
|
-
|
|
22
|
-
# Do not swallow errors in after_commit/after_rollback callbacks.
|
|
23
|
-
config.active_record.raise_in_transactional_callbacks = true
|
|
24
21
|
end
|
data/config/database.yml
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# SQLite version 3.x
|
|
2
|
+
# gem 'activerecord-jdbcsqlite3-adapter'
|
|
3
|
+
#
|
|
4
|
+
# Configure Using Gemfile
|
|
5
|
+
# gem 'activerecord-jdbcsqlite3-adapter'
|
|
6
|
+
#
|
|
7
|
+
default: &default
|
|
8
|
+
adapter: postgresql
|
|
9
|
+
host: localhost
|
|
10
|
+
port: 5432
|
|
11
|
+
pool: 5
|
|
12
|
+
username: postgres
|
|
13
|
+
|
|
14
|
+
development:
|
|
15
|
+
<<: *default
|
|
16
|
+
database: ar_pg_copy_dev
|
|
17
|
+
|
|
18
|
+
# Warning: The database defined as "test" will be erased and
|
|
19
|
+
# re-generated from your development database when you run "rake".
|
|
20
|
+
# Do not set this db to the same as development or production.
|
|
21
|
+
test:
|
|
22
|
+
<<: *default
|
|
23
|
+
database: ar_pg_copy_test
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
class CreateTestTables < ActiveRecord::Migration
|
|
1
|
+
class CreateTestTables < ActiveRecord::Migration[6.0]
|
|
2
2
|
def change
|
|
3
3
|
create_table :test_models do |t|
|
|
4
4
|
t.string :data
|
|
5
5
|
t.timestamps
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
create_table :test_model_copies do |t|
|
|
9
|
+
t.string :data
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
8
13
|
create_table :three_columns do |t|
|
|
9
14
|
t.string :data
|
|
10
15
|
t.string :extra
|
data/db/schema.rb
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
# encoding: UTF-8
|
|
2
1
|
# This file is auto-generated from the current state of the database. Instead
|
|
3
2
|
# of editing this file, please use the migrations feature of Active Record to
|
|
4
3
|
# incrementally modify your database, and then regenerate this schema definition.
|
|
5
4
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# from scratch.
|
|
10
|
-
#
|
|
5
|
+
# This file is the source Rails uses to define your schema when running `rails
|
|
6
|
+
# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
|
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
|
9
|
+
# migrations use external dependencies or application code.
|
|
11
10
|
#
|
|
12
11
|
# It's strongly recommended that you check this file into your version control system.
|
|
13
12
|
|
|
14
|
-
ActiveRecord::Schema.define(version:
|
|
13
|
+
ActiveRecord::Schema.define(version: 2015_07_10_162236) do
|
|
15
14
|
|
|
16
15
|
# These are extensions that must be enabled in order to support this database
|
|
17
16
|
enable_extension "plpgsql"
|
|
@@ -19,25 +18,31 @@ ActiveRecord::Schema.define(version: 20150710162236) do
|
|
|
19
18
|
create_table "composite_key_models", force: :cascade do |t|
|
|
20
19
|
t.integer "comp_key_1"
|
|
21
20
|
t.integer "comp_key_2"
|
|
22
|
-
t.string
|
|
21
|
+
t.string "data"
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
create_table "reserved_word_models", force: :cascade do |t|
|
|
26
|
-
t.string "select"
|
|
27
|
-
t.string "group"
|
|
25
|
+
t.string "select"
|
|
26
|
+
t.string "group"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
create_table "test_model_copies", force: :cascade do |t|
|
|
30
|
+
t.string "data"
|
|
31
|
+
t.datetime "created_at", precision: 6, null: false
|
|
32
|
+
t.datetime "updated_at", precision: 6, null: false
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
create_table "test_models", force: :cascade do |t|
|
|
31
|
-
t.string
|
|
32
|
-
t.datetime "created_at"
|
|
33
|
-
t.datetime "updated_at"
|
|
36
|
+
t.string "data"
|
|
37
|
+
t.datetime "created_at", precision: 6, null: false
|
|
38
|
+
t.datetime "updated_at", precision: 6, null: false
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
create_table "three_columns", force: :cascade do |t|
|
|
37
|
-
t.string
|
|
38
|
-
t.string
|
|
39
|
-
t.datetime "created_at"
|
|
40
|
-
t.datetime "updated_at"
|
|
42
|
+
t.string "data"
|
|
43
|
+
t.string "extra"
|
|
44
|
+
t.datetime "created_at", precision: 6, null: false
|
|
45
|
+
t.datetime "updated_at", precision: 6, null: false
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
end
|
data/lib/postgres_upsert.rb
CHANGED
|
@@ -1,17 +1,46 @@
|
|
|
1
1
|
require 'rubygems'
|
|
2
2
|
require 'active_record'
|
|
3
|
+
require 'postgres_upsert/read_adapters/active_record_adapter'
|
|
4
|
+
require 'postgres_upsert/read_adapters/file_adapter'
|
|
5
|
+
require 'postgres_upsert/read_adapters/io_adapter'
|
|
6
|
+
require 'postgres_upsert/write_adapters/active_record_adapter'
|
|
7
|
+
require 'postgres_upsert/write_adapters/table_adapter'
|
|
3
8
|
require 'postgres_upsert/writer'
|
|
4
9
|
require 'postgres_upsert/table_writer'
|
|
10
|
+
require 'postgres_upsert/model_to_model_adapter'
|
|
5
11
|
require 'postgres_upsert/result'
|
|
6
12
|
require 'rails'
|
|
7
13
|
|
|
8
14
|
module PostgresUpsert
|
|
9
|
-
|
|
10
15
|
class << self
|
|
11
|
-
def write
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
def write(destination, source, options = {})
|
|
17
|
+
read_adapter = read_adapter(source).new(source, options)
|
|
18
|
+
write_adapter = write_adapter(destination).new(destination, options)
|
|
19
|
+
Writer.new(destination, write_adapter, read_adapter, options).write
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read_adapter(source)
|
|
23
|
+
if [StringIO, File].include?(source.class)
|
|
24
|
+
ReadAdapters::IOAdapter
|
|
25
|
+
elsif [String].include?(source.class)
|
|
26
|
+
ReadAdapters::FileAdapter
|
|
27
|
+
elsif source < ActiveRecord::Base
|
|
28
|
+
ReadAdapters::ActiveRecordAdapter
|
|
29
|
+
else
|
|
30
|
+
raise "Source must be a Filename string, StringIO of data, or a ActiveRecord Class."
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def write_adapter(destination)
|
|
35
|
+
if [String].include?(destination.class)
|
|
36
|
+
WriteAdapters::TableAdapter
|
|
37
|
+
elsif destination <= ActiveRecord::Base
|
|
38
|
+
WriteAdapters::ActiveRecordAdapter
|
|
39
|
+
# elsif source < ActiveRecord::Base && destination < ActiveRecord::Base
|
|
40
|
+
#ModelToModelAdapter
|
|
41
|
+
else
|
|
42
|
+
raise "Destination must be an ActiveRecord class or a table name string"
|
|
43
|
+
end
|
|
15
44
|
end
|
|
16
45
|
end
|
|
17
|
-
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
|
|
3
|
+
module PostgresUpsert
|
|
4
|
+
class ModelToModelAdapter
|
|
5
|
+
def initialize(destination_model, source_model, options = {})
|
|
6
|
+
@destination_model = destination_model
|
|
7
|
+
@source_model = source_model
|
|
8
|
+
@options = options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def write
|
|
12
|
+
source_table = @source_model.table_name
|
|
13
|
+
source_conn = @source_model.connection.raw_connection
|
|
14
|
+
|
|
15
|
+
to_stdout_sql = "COPY #{source_table} TO STDOUT"
|
|
16
|
+
|
|
17
|
+
csv_string = CSV.generate do |csv|
|
|
18
|
+
csv << @source_model.column_names # CSV header row
|
|
19
|
+
source_conn.copy_data(to_stdout_sql) do
|
|
20
|
+
while (line = source_conn.get_copy_data) do
|
|
21
|
+
csv << line.split("\t")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
io = StringIO.new(csv_string)
|
|
26
|
+
Writer.new(@destination_model, io, @options).write
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def get_columns
|
|
32
|
+
# columns_list = @options[:columns]
|
|
33
|
+
# columns_list ||=
|
|
34
|
+
@source.column_names
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module PostgresUpsert
|
|
2
|
+
module ReadAdapters
|
|
3
|
+
class ActiveRecordAdapter
|
|
4
|
+
def initialize(source, options)
|
|
5
|
+
@options = sanitize_options(options)
|
|
6
|
+
@source = source
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def sanitize_options(options)
|
|
10
|
+
options.slice(
|
|
11
|
+
:columns, :map, :unique_key
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def continuous_write_enabled
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def gets(&block)
|
|
20
|
+
batch_size = 1_000
|
|
21
|
+
line = ""
|
|
22
|
+
conn = @source.connection.raw_connection
|
|
23
|
+
|
|
24
|
+
conn.copy_data("COPY #{@source.table_name} TO STDOUT") do
|
|
25
|
+
while (line_read = conn.get_copy_data) do
|
|
26
|
+
line << line_read
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
yield line
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def columns
|
|
33
|
+
@source.column_names
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module PostgresUpsert
|
|
2
|
+
module ReadAdapters
|
|
3
|
+
class FileAdapter
|
|
4
|
+
def initialize(source, options)
|
|
5
|
+
@options = sanitize_options(options)
|
|
6
|
+
@source = File.open(source, 'r')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def sanitize_options(options)
|
|
10
|
+
options.slice(
|
|
11
|
+
:delimiter, :header, :columns, :map, :unique_key
|
|
12
|
+
).reverse_merge(
|
|
13
|
+
header: true,
|
|
14
|
+
delimiter: ',',
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def continuous_write_enabled
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def gets
|
|
23
|
+
@source.gets
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def columns
|
|
27
|
+
@columns ||= begin
|
|
28
|
+
columns_list = @options[:columns] ? @options[:columns].map(&:to_s) : []
|
|
29
|
+
if @options[:header]
|
|
30
|
+
# if header is present, we need to strip it from io, whether we use it for the columns list or not.
|
|
31
|
+
line = gets
|
|
32
|
+
if columns_list.empty?
|
|
33
|
+
columns_list = line.strip.split(@options[:delimiter])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
columns_list = columns_list.map { |c| @options[:map][c.to_s] } if @options[:map]
|
|
37
|
+
columns_list
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module PostgresUpsert
|
|
2
|
+
module ReadAdapters
|
|
3
|
+
class IOAdapter
|
|
4
|
+
def initialize(source, options)
|
|
5
|
+
@options = sanitize_options(options)
|
|
6
|
+
@source = source
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def sanitize_options(options)
|
|
10
|
+
options.slice(
|
|
11
|
+
:delimiter, :header, :columns, :map, :unique_key
|
|
12
|
+
).reverse_merge(
|
|
13
|
+
header: true,
|
|
14
|
+
delimiter: ',',
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def continuous_write_enabled
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def gets
|
|
23
|
+
@source.gets
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def columns
|
|
27
|
+
@columns ||= begin
|
|
28
|
+
columns_list = @options[:columns] ? @options[:columns].map(&:to_s) : []
|
|
29
|
+
if @options[:header]
|
|
30
|
+
# if header is present, we need to strip it from io, whether we use it for the columns list or not.
|
|
31
|
+
line = gets
|
|
32
|
+
if columns_list.empty?
|
|
33
|
+
columns_list = line.strip.split(@options[:delimiter])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
columns_list = columns_list.map { |c| @options[:map][c.to_s] } if @options[:map]
|
|
37
|
+
columns_list
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -4,8 +4,8 @@ module PostgresUpsert
|
|
|
4
4
|
|
|
5
5
|
def initialize(insert_result, update_result, copy_result)
|
|
6
6
|
@inserted = insert_result ? insert_result.cmd_tuples : 0
|
|
7
|
-
@updated = update_result ? update_result.cmd_tuples : 0
|
|
8
|
-
@copied = copy_result ? copy_result.cmd_tuples : 0
|
|
7
|
+
@updated = update_result ? update_result.cmd_tuples : 0
|
|
8
|
+
@copied = copy_result ? copy_result.cmd_tuples : 0
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def changed_rows
|
|
@@ -20,5 +20,4 @@ module PostgresUpsert
|
|
|
20
20
|
@updated
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
|
-
end
|
|
24
|
-
|
|
23
|
+
end
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
module PostgresUpsert
|
|
2
|
-
# alternate version of PostgresUpsert::Writer which does not rely on AR table information.
|
|
3
|
-
#
|
|
2
|
+
# alternate version of PostgresUpsert::Writer which does not rely on AR table information.
|
|
3
|
+
# We can use this model to upsert data into views, or tables not associated to rails models
|
|
4
4
|
class TableWriter < Writer
|
|
5
|
-
|
|
6
5
|
def initialize(table_name, source, options = {})
|
|
7
6
|
@table_name = table_name
|
|
8
7
|
super(nil, source, options)
|
|
@@ -16,7 +15,7 @@ module PostgresUpsert
|
|
|
16
15
|
|
|
17
16
|
def primary_key
|
|
18
17
|
@primary_key ||= begin
|
|
19
|
-
query = <<-
|
|
18
|
+
query = <<-SELECT_KEY
|
|
20
19
|
SELECT
|
|
21
20
|
pg_attribute.attname,
|
|
22
21
|
format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
|
|
@@ -27,24 +26,23 @@ module PostgresUpsert
|
|
|
27
26
|
pg_attribute.attrelid = pg_class.oid AND
|
|
28
27
|
pg_attribute.attnum = any(pg_index.indkey)
|
|
29
28
|
AND indisprimary
|
|
30
|
-
|
|
29
|
+
SELECT_KEY
|
|
31
30
|
|
|
32
31
|
pg_result = ActiveRecord::Base.connection.execute query
|
|
33
|
-
pg_result.each{ |row| return row['attname'] }
|
|
32
|
+
pg_result.each { |row| return row['attname'] }
|
|
34
33
|
end
|
|
35
34
|
end
|
|
36
35
|
|
|
37
|
-
def
|
|
36
|
+
def destination_columns
|
|
38
37
|
@column_names ||= begin
|
|
39
38
|
query = "SELECT * FROM information_schema.columns WHERE TABLE_NAME = '#{@table_name}'"
|
|
40
39
|
pg_result = ActiveRecord::Base.connection.execute query
|
|
41
|
-
pg_result.map{ |row| row['column_name'] }
|
|
40
|
+
pg_result.map { |row| row['column_name'] }
|
|
42
41
|
end
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
def quoted_table_name
|
|
46
45
|
@quoted_table_name ||= ActiveRecord::Base.connection.quote_table_name(@table_name)
|
|
47
46
|
end
|
|
48
|
-
|
|
49
47
|
end
|
|
50
48
|
end
|