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.
Files changed (58) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +57 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +20 -0
  7. data/Gemfile +1 -2
  8. data/Gemfile.lock +175 -85
  9. data/README.md +117 -41
  10. data/Rakefile +4 -16
  11. data/app/assets/config/manifest.js +0 -0
  12. data/bin/bundle +3 -0
  13. data/bin/rails +4 -0
  14. data/bin/rake +4 -0
  15. data/bin/setup +56 -0
  16. data/config.ru +4 -0
  17. data/config/application.rb +21 -0
  18. data/config/boot.rb +3 -0
  19. data/config/database.yml +24 -0
  20. data/config/database.yml.travis +23 -0
  21. data/config/environment.rb +5 -0
  22. data/config/environments/development.rb +41 -0
  23. data/config/environments/production.rb +79 -0
  24. data/config/environments/test.rb +42 -0
  25. data/config/locales/en.yml +23 -0
  26. data/config/routes.rb +56 -0
  27. data/config/secrets.yml +22 -0
  28. data/db/migrate/20150214192135_create_test_tables.rb +24 -0
  29. data/db/migrate/20150710162236_create_composite_models_table.rb +9 -0
  30. data/db/schema.rb +48 -0
  31. data/db/seeds.rb +7 -0
  32. data/lib/postgres_upsert.rb +38 -6
  33. data/lib/postgres_upsert/model_to_model_adapter.rb +37 -0
  34. data/lib/postgres_upsert/read_adapters/active_record_adapter.rb +37 -0
  35. data/lib/postgres_upsert/read_adapters/file_adapter.rb +42 -0
  36. data/lib/postgres_upsert/read_adapters/io_adapter.rb +42 -0
  37. data/lib/postgres_upsert/result.rb +23 -0
  38. data/lib/postgres_upsert/table_writer.rb +48 -0
  39. data/lib/postgres_upsert/write_adapters/active_record_adapter.rb +36 -0
  40. data/lib/postgres_upsert/write_adapters/table_adapter.rb +56 -0
  41. data/lib/postgres_upsert/writer.rb +130 -92
  42. data/postgres_upsert.gemspec +7 -4
  43. data/spec/composite_key_spec.rb +50 -0
  44. data/spec/fixtures/comma_with_header_duplicate.csv +3 -0
  45. data/spec/fixtures/composite_key_model.rb +4 -0
  46. data/spec/fixtures/composite_key_with_header.csv +3 -0
  47. data/spec/fixtures/composite_nonkey_with_header.csv +3 -0
  48. data/spec/fixtures/test_model_copy.rb +4 -0
  49. data/spec/from_table_spec.rb +40 -0
  50. data/spec/pg_upsert_csv_spec.rb +93 -35
  51. data/spec/rails_helper.rb +1 -0
  52. data/spec/spec_helper.rb +9 -37
  53. metadata +106 -37
  54. data/VERSION +0 -1
  55. data/lib/postgres_upsert/active_record.rb +0 -13
  56. data/spec/fixtures/2_col_binary_data.dat +0 -0
  57. data/spec/pg_upsert_binary_spec.rb +0 -35
  58. 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
@@ -0,0 +1,9 @@
1
+ class CreateCompositeModelsTable < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :composite_key_models do |t|
4
+ t.integer :comp_key_1
5
+ t.integer :comp_key_2
6
+ t.string :data
7
+ end
8
+ end
9
+ 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)
@@ -1,14 +1,46 @@
1
1
  require 'rubygems'
2
2
  require 'active_record'
3
- require 'postgres_upsert/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'
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
- class PostgresCopy < Rails::Railtie
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
- initializer 'postgres_upsert' do
10
- ActiveSupport.on_load :active_record do
11
- require "postgres_upsert/active_record"
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