postgres_upsert 2.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 -5
- data/.gitignore +3 -0
- data/.rubocop.yml +57 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/Gemfile +1 -2
- data/Gemfile.lock +175 -85
- data/README.md +117 -41
- data/Rakefile +4 -16
- data/app/assets/config/manifest.js +0 -0
- data/bin/bundle +3 -0
- data/bin/rails +4 -0
- data/bin/rake +4 -0
- data/bin/setup +56 -0
- data/config.ru +4 -0
- data/config/application.rb +21 -0
- data/config/boot.rb +3 -0
- data/config/database.yml +24 -0
- data/config/database.yml.travis +23 -0
- data/config/environment.rb +5 -0
- data/config/environments/development.rb +41 -0
- data/config/environments/production.rb +79 -0
- data/config/environments/test.rb +42 -0
- data/config/locales/en.yml +23 -0
- data/config/routes.rb +56 -0
- data/config/secrets.yml +22 -0
- data/db/migrate/20150214192135_create_test_tables.rb +24 -0
- data/db/migrate/20150710162236_create_composite_models_table.rb +9 -0
- data/db/schema.rb +48 -0
- data/db/seeds.rb +7 -0
- data/lib/postgres_upsert.rb +38 -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 +23 -0
- data/lib/postgres_upsert/table_writer.rb +48 -0
- 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 +130 -92
- data/postgres_upsert.gemspec +7 -4
- data/spec/composite_key_spec.rb +50 -0
- data/spec/fixtures/comma_with_header_duplicate.csv +3 -0
- data/spec/fixtures/composite_key_model.rb +4 -0
- data/spec/fixtures/composite_key_with_header.csv +3 -0
- data/spec/fixtures/composite_nonkey_with_header.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 +93 -35
- data/spec/rails_helper.rb +1 -0
- data/spec/spec_helper.rb +9 -37
- metadata +106 -37
- data/VERSION +0 -1
- data/lib/postgres_upsert/active_record.rb +0 -13
- data/spec/fixtures/2_col_binary_data.dat +0 -0
- data/spec/pg_upsert_binary_spec.rb +0 -35
- data/spec/spec.opts +0 -1
@@ -0,0 +1,24 @@
|
|
1
|
+
class CreateTestTables < ActiveRecord::Migration[6.0]
|
2
|
+
def change
|
3
|
+
create_table :test_models do |t|
|
4
|
+
t.string :data
|
5
|
+
t.timestamps
|
6
|
+
end
|
7
|
+
|
8
|
+
create_table :test_model_copies do |t|
|
9
|
+
t.string :data
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :three_columns do |t|
|
14
|
+
t.string :data
|
15
|
+
t.string :extra
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table :reserved_word_models do |t|
|
20
|
+
t.string :select
|
21
|
+
t.string :group
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/db/schema.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
4
|
+
#
|
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.
|
10
|
+
#
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
12
|
+
|
13
|
+
ActiveRecord::Schema.define(version: 2015_07_10_162236) do
|
14
|
+
|
15
|
+
# These are extensions that must be enabled in order to support this database
|
16
|
+
enable_extension "plpgsql"
|
17
|
+
|
18
|
+
create_table "composite_key_models", force: :cascade do |t|
|
19
|
+
t.integer "comp_key_1"
|
20
|
+
t.integer "comp_key_2"
|
21
|
+
t.string "data"
|
22
|
+
end
|
23
|
+
|
24
|
+
create_table "reserved_word_models", force: :cascade do |t|
|
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
|
33
|
+
end
|
34
|
+
|
35
|
+
create_table "test_models", force: :cascade do |t|
|
36
|
+
t.string "data"
|
37
|
+
t.datetime "created_at", precision: 6, null: false
|
38
|
+
t.datetime "updated_at", precision: 6, null: false
|
39
|
+
end
|
40
|
+
|
41
|
+
create_table "three_columns", force: :cascade do |t|
|
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
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/db/seeds.rb
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# This file should contain all the record creation needed to seed the database with its default values.
|
2
|
+
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
|
3
|
+
#
|
4
|
+
# Examples:
|
5
|
+
#
|
6
|
+
# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
|
7
|
+
# Mayor.create(name: 'Emanuel', city: cities.first)
|
data/lib/postgres_upsert.rb
CHANGED
@@ -1,14 +1,46 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'active_record'
|
3
|
-
require 'postgres_upsert/
|
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'
|
4
8
|
require 'postgres_upsert/writer'
|
9
|
+
require 'postgres_upsert/table_writer'
|
10
|
+
require 'postgres_upsert/model_to_model_adapter'
|
11
|
+
require 'postgres_upsert/result'
|
5
12
|
require 'rails'
|
6
13
|
|
7
|
-
|
14
|
+
module PostgresUpsert
|
15
|
+
class << self
|
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
|
8
33
|
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
12
44
|
end
|
13
45
|
end
|
14
|
-
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
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module PostgresUpsert
|
2
|
+
class Result
|
3
|
+
attr_reader :inserted, :updated
|
4
|
+
|
5
|
+
def initialize(insert_result, update_result, copy_result)
|
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
|
9
|
+
end
|
10
|
+
|
11
|
+
def changed_rows
|
12
|
+
@inserted + @updated
|
13
|
+
end
|
14
|
+
|
15
|
+
def copied_rows
|
16
|
+
@copied
|
17
|
+
end
|
18
|
+
|
19
|
+
def updated_rows
|
20
|
+
@updated
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module PostgresUpsert
|
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
|
+
class TableWriter < Writer
|
5
|
+
def initialize(table_name, source, options = {})
|
6
|
+
@table_name = table_name
|
7
|
+
super(nil, source, options)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def database_connection
|
13
|
+
ActiveRecord::Base.connection
|
14
|
+
end
|
15
|
+
|
16
|
+
def primary_key
|
17
|
+
@primary_key ||= begin
|
18
|
+
query = <<-SELECT_KEY
|
19
|
+
SELECT
|
20
|
+
pg_attribute.attname,
|
21
|
+
format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
|
22
|
+
FROM pg_index, pg_class, pg_attribute
|
23
|
+
WHERE
|
24
|
+
pg_class.oid = '#{@table_name}'::regclass AND
|
25
|
+
indrelid = pg_class.oid AND
|
26
|
+
pg_attribute.attrelid = pg_class.oid AND
|
27
|
+
pg_attribute.attnum = any(pg_index.indkey)
|
28
|
+
AND indisprimary
|
29
|
+
SELECT_KEY
|
30
|
+
|
31
|
+
pg_result = ActiveRecord::Base.connection.execute query
|
32
|
+
pg_result.each { |row| return row['attname'] }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def destination_columns
|
37
|
+
@column_names ||= begin
|
38
|
+
query = "SELECT * FROM information_schema.columns WHERE TABLE_NAME = '#{@table_name}'"
|
39
|
+
pg_result = ActiveRecord::Base.connection.execute query
|
40
|
+
pg_result.map { |row| row['column_name'] }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def quoted_table_name
|
45
|
+
@quoted_table_name ||= ActiveRecord::Base.connection.quote_table_name(@table_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module PostgresUpsert
|
2
|
+
module WriteAdapters
|
3
|
+
class ActiveRecordAdapter
|
4
|
+
def initialize(destination, options)
|
5
|
+
@destination = destination
|
6
|
+
@options = sanitize_options(options)
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
def sanitize_options(options)
|
11
|
+
options.slice(
|
12
|
+
:delimiter, :unique_key
|
13
|
+
).reverse_merge(
|
14
|
+
delimiter: ',',
|
15
|
+
unique_key: [primary_key],
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def database_connection
|
20
|
+
@destination.connection
|
21
|
+
end
|
22
|
+
|
23
|
+
def primary_key
|
24
|
+
@destination.primary_key
|
25
|
+
end
|
26
|
+
|
27
|
+
def column_names
|
28
|
+
@destination.column_names
|
29
|
+
end
|
30
|
+
|
31
|
+
def quoted_table_name
|
32
|
+
@destination.quoted_table_name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module PostgresUpsert
|
2
|
+
module WriteAdapters
|
3
|
+
class TableAdapter
|
4
|
+
def initialize(destination, options)
|
5
|
+
@destination = destination
|
6
|
+
@options = sanitize_options(options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def sanitize_options(options)
|
10
|
+
options.slice(
|
11
|
+
:delimiter, :unique_key
|
12
|
+
).reverse_merge(
|
13
|
+
delimiter: ',',
|
14
|
+
unique_key: [primary_key],
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def database_connection
|
19
|
+
ActiveRecord::Base.connection
|
20
|
+
end
|
21
|
+
|
22
|
+
def primary_key
|
23
|
+
@primary_key ||= begin
|
24
|
+
query = <<-SELECT_KEY
|
25
|
+
SELECT
|
26
|
+
pg_attribute.attname,
|
27
|
+
format_type(pg_attribute.atttypid, pg_attribute.atttypmod)
|
28
|
+
FROM pg_index, pg_class, pg_attribute
|
29
|
+
WHERE
|
30
|
+
pg_class.oid = '#{@destination}'::regclass AND
|
31
|
+
indrelid = pg_class.oid AND
|
32
|
+
pg_attribute.attrelid = pg_class.oid AND
|
33
|
+
pg_attribute.attnum = any(pg_index.indkey)
|
34
|
+
AND indisprimary
|
35
|
+
SELECT_KEY
|
36
|
+
|
37
|
+
pg_result = ActiveRecord::Base.connection.execute query
|
38
|
+
pg_result.each { |row| return row['attname'] }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def column_names
|
43
|
+
@column_names ||= begin
|
44
|
+
query = "SELECT * FROM information_schema.columns WHERE TABLE_NAME = '#{@destination}'"
|
45
|
+
pg_result = ActiveRecord::Base.connection.execute query
|
46
|
+
pg_result.map { |row| row['column_name'] }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def quoted_table_name
|
51
|
+
@quoted_table_name ||= database_connection.quote_table_name(@destination)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|