janko 0.0.2
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 +7 -0
 - data/.gitignore +17 -0
 - data/Gemfile +2 -0
 - data/Guardfile +7 -0
 - data/LICENSE.txt +202 -0
 - data/README.md +186 -0
 - data/Rakefile +14 -0
 - data/assets/insert-performance-graph.png +0 -0
 - data/assets/merge-performance-graph.png +0 -0
 - data/config/environment.rb +7 -0
 - data/janko.gemspec +32 -0
 - data/lib/janko.rb +5 -0
 - data/lib/janko/bulk_merge.rb +57 -0
 - data/lib/janko/column_list.rb +193 -0
 - data/lib/janko/connection.rb +107 -0
 - data/lib/janko/constants.rb +13 -0
 - data/lib/janko/copy_importer.rb +36 -0
 - data/lib/janko/flag.rb +26 -0
 - data/lib/janko/import.rb +80 -0
 - data/lib/janko/insert_importer.rb +32 -0
 - data/lib/janko/merge.rb +167 -0
 - data/lib/janko/merge_result.rb +37 -0
 - data/lib/janko/single_merge.rb +33 -0
 - data/lib/janko/tagged_column.rb +133 -0
 - data/lib/janko/upsert.rb +193 -0
 - data/lib/janko/version.rb +3 -0
 - data/spec/column_list_spec.rb +164 -0
 - data/spec/connection_spec.rb +27 -0
 - data/spec/flag_spec.rb +16 -0
 - data/spec/import_spec.rb +112 -0
 - data/spec/merge_spec.rb +400 -0
 - data/spec/spec_helper.rb +32 -0
 - data/spec/tagged_column_spec.rb +56 -0
 - metadata +261 -0
 
    
        data/lib/janko/upsert.rb
    ADDED
    
    | 
         @@ -0,0 +1,193 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "securerandom"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "agrippa/mutable"
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Janko
         
     | 
| 
      
 5 
     | 
    
         
            +
                # http://dba.stackexchange.com/questions/13468/most-idiomatic-way-to-implement-upsert-in-postgresql-nowadays
         
     | 
| 
      
 6 
     | 
    
         
            +
                # http://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql/8702291#8702291
         
     | 
| 
      
 7 
     | 
    
         
            +
                # http://stackoverflow.com/questions/17575489/postgresql-cte-upsert-returning-modified-rows
         
     | 
| 
      
 8 
     | 
    
         
            +
                class Upsert
         
     | 
| 
      
 9 
     | 
    
         
            +
                    include Agrippa::Mutable
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    state_reader :connection, :table, :columns, :collector, :returning
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                    def default_state
         
     | 
| 
      
 14 
     | 
    
         
            +
                        { collector: MergeResult.new }
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    def result
         
     | 
| 
      
 18 
     | 
    
         
            +
                        collector
         
     | 
| 
      
 19 
     | 
    
         
            +
                    end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                    def prepare
         
     | 
| 
      
 22 
     | 
    
         
            +
                        return(self) if prepared?
         
     | 
| 
      
 23 
     | 
    
         
            +
                        @prepared = "upsert_#{SecureRandom.hex(8)}"
         
     | 
| 
      
 24 
     | 
    
         
            +
                        connection.prepare(@prepared, query)
         
     | 
| 
      
 25 
     | 
    
         
            +
                        self
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                    def push(*values)
         
     | 
| 
      
 29 
     | 
    
         
            +
                        raise(RuntimeError, "Can't #push when reading from a table.") \
         
     | 
| 
      
 30 
     | 
    
         
            +
                            if read_from_table?
         
     | 
| 
      
 31 
     | 
    
         
            +
                        collect_result(exec_query(columns.pack(*values)))
         
     | 
| 
      
 32 
     | 
    
         
            +
                        self
         
     | 
| 
      
 33 
     | 
    
         
            +
                    end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    def process
         
     | 
| 
      
 36 
     | 
    
         
            +
                        raise(RuntimeError, "Can't #process without from_table") \
         
     | 
| 
      
 37 
     | 
    
         
            +
                            unless read_from_table?
         
     | 
| 
      
 38 
     | 
    
         
            +
                        result.clear
         
     | 
| 
      
 39 
     | 
    
         
            +
                        collect_result(exec_query)
         
     | 
| 
      
 40 
     | 
    
         
            +
                        self
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                    def cleanup
         
     | 
| 
      
 44 
     | 
    
         
            +
                        return unless prepared?
         
     | 
| 
      
 45 
     | 
    
         
            +
                        connection.exec("DEALLOCATE \"#{@prepared}\"")
         
     | 
| 
      
 46 
     | 
    
         
            +
                        @prepared = nil
         
     | 
| 
      
 47 
     | 
    
         
            +
                        self
         
     | 
| 
      
 48 
     | 
    
         
            +
                    end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                    private
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    def result_type_column
         
     | 
| 
      
 53 
     | 
    
         
            +
                        "__type"
         
     | 
| 
      
 54 
     | 
    
         
            +
                    end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                    def collect_result(tuples)
         
     | 
| 
      
 57 
     | 
    
         
            +
                        return(self) if (tuples.count == 0)
         
     | 
| 
      
 58 
     | 
    
         
            +
                        tuples.each { |t| result.push(t.delete(result_type_column), t) }
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                    def read_from_table?
         
     | 
| 
      
 62 
     | 
    
         
            +
                        not @from_table.nil?
         
     | 
| 
      
 63 
     | 
    
         
            +
                    end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                    def prepared?
         
     | 
| 
      
 66 
     | 
    
         
            +
                        not @prepared.nil?
         
     | 
| 
      
 67 
     | 
    
         
            +
                    end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                    def exec_query(*binds)
         
     | 
| 
      
 70 
     | 
    
         
            +
                        return(connection.exec(query, *binds)) unless prepared?
         
     | 
| 
      
 71 
     | 
    
         
            +
                        connection.exec_prepared(@prepared, *binds)
         
     | 
| 
      
 72 
     | 
    
         
            +
                    end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    def key_columns
         
     | 
| 
      
 75 
     | 
    
         
            +
                        @key_columns ||= columns.tagged(:key)
         
     | 
| 
      
 76 
     | 
    
         
            +
                    end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                    def update_columns
         
     | 
| 
      
 79 
     | 
    
         
            +
                        @update_columns ||= columns.tagged(:update).not_tagged(:key)
         
     | 
| 
      
 80 
     | 
    
         
            +
                    end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                    def insert_columns
         
     | 
| 
      
 83 
     | 
    
         
            +
                        @insert_columns ||= columns.tagged(:insert)
         
     | 
| 
      
 84 
     | 
    
         
            +
                    end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                    def select_columns
         
     | 
| 
      
 87 
     | 
    
         
            +
                        @select_columns ||= columns.tagged(:select)
         
     | 
| 
      
 88 
     | 
    
         
            +
                    end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                    def insert_columns_with_defaults
         
     | 
| 
      
 91 
     | 
    
         
            +
                        insert_columns.to_list_with_defaults
         
     | 
| 
      
 92 
     | 
    
         
            +
                    end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                    def columns_to_binds_with_types
         
     | 
| 
      
 95 
     | 
    
         
            +
                        columns.to_typecast_binds
         
     | 
| 
      
 96 
     | 
    
         
            +
                    end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                    def update_clause
         
     | 
| 
      
 99 
     | 
    
         
            +
                        update_columns.to_setters("upsert_updates", source_table)
         
     | 
| 
      
 100 
     | 
    
         
            +
                    end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                    def update_key_clause
         
     | 
| 
      
 103 
     | 
    
         
            +
                        key_columns.to_conditions("upsert_updates", source_table)
         
     | 
| 
      
 104 
     | 
    
         
            +
                    end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                    def insert_key_clause
         
     | 
| 
      
 107 
     | 
    
         
            +
                        key_columns.to_conditions("upsert", source_table)
         
     | 
| 
      
 108 
     | 
    
         
            +
                    end
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                    def query
         
     | 
| 
      
 111 
     | 
    
         
            +
                        return(query_returning_nothing) if select_columns.empty?
         
     | 
| 
      
 112 
     | 
    
         
            +
                        return(query_returning_all) if (returning == "all")
         
     | 
| 
      
 113 
     | 
    
         
            +
                        return(query_returning_inserts) if (returning == "inserted")
         
     | 
| 
      
 114 
     | 
    
         
            +
                        return(query_returning_updates) if (returning == "updated")
         
     | 
| 
      
 115 
     | 
    
         
            +
                        query_returning_nothing
         
     | 
| 
      
 116 
     | 
    
         
            +
                    end
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                    def source_table
         
     | 
| 
      
 119 
     | 
    
         
            +
                        @from_table || "upsert_data"
         
     | 
| 
      
 120 
     | 
    
         
            +
                    end
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
                    def source_fragment
         
     | 
| 
      
 123 
     | 
    
         
            +
                        return if read_from_table?
         
     | 
| 
      
 124 
     | 
    
         
            +
                        return(<<-END)
         
     | 
| 
      
 125 
     | 
    
         
            +
                            #{source_table} (#{columns.to_list}) AS (VALUES
         
     | 
| 
      
 126 
     | 
    
         
            +
                                (#{columns_to_binds_with_types})),
         
     | 
| 
      
 127 
     | 
    
         
            +
                        END
         
     | 
| 
      
 128 
     | 
    
         
            +
                    end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                    def upsert_fragment
         
     | 
| 
      
 131 
     | 
    
         
            +
                        return(<<-END)
         
     | 
| 
      
 132 
     | 
    
         
            +
                            UPDATE #{table} upsert_updates
         
     | 
| 
      
 133 
     | 
    
         
            +
                            SET #{update_clause} FROM #{source_table}
         
     | 
| 
      
 134 
     | 
    
         
            +
                            WHERE (#{update_key_clause})
         
     | 
| 
      
 135 
     | 
    
         
            +
                            RETURNING upsert_updates.*
         
     | 
| 
      
 136 
     | 
    
         
            +
                        END
         
     | 
| 
      
 137 
     | 
    
         
            +
                    end
         
     | 
| 
      
 138 
     | 
    
         
            +
             
     | 
| 
      
 139 
     | 
    
         
            +
                    def insert_fragment
         
     | 
| 
      
 140 
     | 
    
         
            +
                        return(<<-END)
         
     | 
| 
      
 141 
     | 
    
         
            +
                            INSERT INTO #{table} (#{insert_columns.to_list})
         
     | 
| 
      
 142 
     | 
    
         
            +
                            SELECT #{insert_columns_with_defaults} FROM #{source_table}
         
     | 
| 
      
 143 
     | 
    
         
            +
                            WHERE NOT EXISTS (SELECT 1 FROM upsert
         
     | 
| 
      
 144 
     | 
    
         
            +
                                WHERE #{insert_key_clause})
         
     | 
| 
      
 145 
     | 
    
         
            +
                        END
         
     | 
| 
      
 146 
     | 
    
         
            +
                    end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                    def query_returning_nothing
         
     | 
| 
      
 149 
     | 
    
         
            +
                        return(<<-END)
         
     | 
| 
      
 150 
     | 
    
         
            +
                            WITH #{source_fragment} upsert AS (#{upsert_fragment})
         
     | 
| 
      
 151 
     | 
    
         
            +
                            #{insert_fragment}
         
     | 
| 
      
 152 
     | 
    
         
            +
                        END
         
     | 
| 
      
 153 
     | 
    
         
            +
                    end
         
     | 
| 
      
 154 
     | 
    
         
            +
             
     | 
| 
      
 155 
     | 
    
         
            +
                    def query_with_returnable_tuples
         
     | 
| 
      
 156 
     | 
    
         
            +
                        return(<<-END)
         
     | 
| 
      
 157 
     | 
    
         
            +
                            WITH #{source_fragment} upsert AS (#{upsert_fragment}), 
         
     | 
| 
      
 158 
     | 
    
         
            +
                            inserted AS (#{insert_fragment} RETURNING #{table}.*)
         
     | 
| 
      
 159 
     | 
    
         
            +
                        END
         
     | 
| 
      
 160 
     | 
    
         
            +
                    end
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                    def query_returning_inserts
         
     | 
| 
      
 163 
     | 
    
         
            +
                        return(<<-END)
         
     | 
| 
      
 164 
     | 
    
         
            +
                            #{query_with_returnable_tuples}
         
     | 
| 
      
 165 
     | 
    
         
            +
                            SELECT 'inserted'
         
     | 
| 
      
 166 
     | 
    
         
            +
                                AS #{result_type_column}, #{select_columns.to_list}
         
     | 
| 
      
 167 
     | 
    
         
            +
                                FROM inserted
         
     | 
| 
      
 168 
     | 
    
         
            +
                        END
         
     | 
| 
      
 169 
     | 
    
         
            +
                    end
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                    def query_returning_updates
         
     | 
| 
      
 172 
     | 
    
         
            +
                        return(<<-END)
         
     | 
| 
      
 173 
     | 
    
         
            +
                            #{query_with_returnable_tuples}
         
     | 
| 
      
 174 
     | 
    
         
            +
                            SELECT 'updated'
         
     | 
| 
      
 175 
     | 
    
         
            +
                                AS #{result_type_column}, #{select_columns.to_list}
         
     | 
| 
      
 176 
     | 
    
         
            +
                                FROM upsert
         
     | 
| 
      
 177 
     | 
    
         
            +
                        END
         
     | 
| 
      
 178 
     | 
    
         
            +
                    end
         
     | 
| 
      
 179 
     | 
    
         
            +
             
     | 
| 
      
 180 
     | 
    
         
            +
                    def query_returning_all
         
     | 
| 
      
 181 
     | 
    
         
            +
                        return(<<-END)
         
     | 
| 
      
 182 
     | 
    
         
            +
                            #{query_with_returnable_tuples}
         
     | 
| 
      
 183 
     | 
    
         
            +
                            SELECT 'inserted'
         
     | 
| 
      
 184 
     | 
    
         
            +
                                AS #{result_type_column}, #{select_columns.to_list}
         
     | 
| 
      
 185 
     | 
    
         
            +
                                FROM inserted
         
     | 
| 
      
 186 
     | 
    
         
            +
                            UNION ALL
         
     | 
| 
      
 187 
     | 
    
         
            +
                            SELECT 'updated'
         
     | 
| 
      
 188 
     | 
    
         
            +
                                AS #{result_type_column}, #{select_columns.to_list}
         
     | 
| 
      
 189 
     | 
    
         
            +
                                FROM upsert
         
     | 
| 
      
 190 
     | 
    
         
            +
                        END
         
     | 
| 
      
 191 
     | 
    
         
            +
                    end
         
     | 
| 
      
 192 
     | 
    
         
            +
                end
         
     | 
| 
      
 193 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,164 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "spec_helper"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "janko/column_list"
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            RSpec.describe Janko::ColumnList do
         
     | 
| 
      
 5 
     | 
    
         
            +
                let(:subject) { Janko::ColumnList.new.builder }
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                it "#empty?" do
         
     | 
| 
      
 8 
     | 
    
         
            +
                    expect(subject.empty?).to eq(true)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    expect(subject.add(:a).empty?).to eq(false)
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                it "#columns" do
         
     | 
| 
      
 13 
     | 
    
         
            +
                    expect(subject.add(:a, "b", :c).columns).to eq(%w(a b c))
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
                
         
     | 
| 
      
 16 
     | 
    
         
            +
                describe "#tagged" do
         
     | 
| 
      
 17 
     | 
    
         
            +
                    it "filters" do
         
     | 
| 
      
 18 
     | 
    
         
            +
                        subject.tag(:red, :a).tag(:blue, :b)
         
     | 
| 
      
 19 
     | 
    
         
            +
                        expect(subject.tagged(:red).columns).to eq(%w(a))
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    it "symbols or strings" do
         
     | 
| 
      
 23 
     | 
    
         
            +
                        subject.tag(:red, :a)
         
     | 
| 
      
 24 
     | 
    
         
            +
                        expect(subject.tagged("red").columns).to eq(%w(a))
         
     | 
| 
      
 25 
     | 
    
         
            +
                        subject.tag("red", :b)
         
     | 
| 
      
 26 
     | 
    
         
            +
                        expect(subject.tagged("red").columns).to eq(%w(b))
         
     | 
| 
      
 27 
     | 
    
         
            +
                    end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    it "preserves column order" do
         
     | 
| 
      
 30 
     | 
    
         
            +
                        subject.add(:a, :b, :c)
         
     | 
| 
      
 31 
     | 
    
         
            +
                        subject.tag(:red, :c, :b)
         
     | 
| 
      
 32 
     | 
    
         
            +
                        expect(subject.tagged(:red).columns).to eq(%w(b c))
         
     | 
| 
      
 33 
     | 
    
         
            +
                    end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    it "all tagged columns" do
         
     | 
| 
      
 36 
     | 
    
         
            +
                        subject.add(:a, :b, :c)
         
     | 
| 
      
 37 
     | 
    
         
            +
                        subject.tag(:red, :a).tag(:green, :c)
         
     | 
| 
      
 38 
     | 
    
         
            +
                        expect(subject.tagged.columns).to eq(%w(a c))
         
     | 
| 
      
 39 
     | 
    
         
            +
                    end
         
     | 
| 
      
 40 
     | 
    
         
            +
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                describe "#pack" do
         
     | 
| 
      
 43 
     | 
    
         
            +
                    it "maintains order" do
         
     | 
| 
      
 44 
     | 
    
         
            +
                        subject.add(:a, :b, :c)
         
     | 
| 
      
 45 
     | 
    
         
            +
                        expect(subject.pack(b: "2", a: "1", c: "3")).to eq(%w(1 2 3))
         
     | 
| 
      
 46 
     | 
    
         
            +
                    end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                    it "null-fills fewer columns" do
         
     | 
| 
      
 49 
     | 
    
         
            +
                        subject.add(:a, :b, :c)
         
     | 
| 
      
 50 
     | 
    
         
            +
                        expect(subject.pack(a: 1, c: 3)).to eq([1, nil, 3])
         
     | 
| 
      
 51 
     | 
    
         
            +
                    end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                    it "unknown columns" do
         
     | 
| 
      
 54 
     | 
    
         
            +
                        subject.add(:a, :b)
         
     | 
| 
      
 55 
     | 
    
         
            +
                        expect(lambda { subject.pack(b: 2, a: 1, c: 3) }).to \
         
     | 
| 
      
 56 
     | 
    
         
            +
                            raise_error(ArgumentError)
         
     | 
| 
      
 57 
     | 
    
         
            +
                    end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                    it "indifferent access" do
         
     | 
| 
      
 60 
     | 
    
         
            +
                        subject.add(:a, "b")
         
     | 
| 
      
 61 
     | 
    
         
            +
                        expect(subject.pack("a" => "1", b: "2")).to eq(%w(1 2))
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end
         
     | 
| 
      
 63 
     | 
    
         
            +
                end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                describe "#add" do
         
     | 
| 
      
 66 
     | 
    
         
            +
                    it "multiple times" do
         
     | 
| 
      
 67 
     | 
    
         
            +
                        subject.add(:a).add(:b, :c)
         
     | 
| 
      
 68 
     | 
    
         
            +
                        expect(subject.pack(b: "2", a: "1", c: "3")).to eq(%w(1 2 3))
         
     | 
| 
      
 69 
     | 
    
         
            +
                    end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                    it "duplicate columns" do
         
     | 
| 
      
 72 
     | 
    
         
            +
                        subject.add(:a).add(:a, :b, :c)
         
     | 
| 
      
 73 
     | 
    
         
            +
                        expect(subject.pack(b: "2", a: "1", c: "3")).to eq(%w(1 2 3))
         
     | 
| 
      
 74 
     | 
    
         
            +
                    end
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                describe "#alter" do
         
     | 
| 
      
 78 
     | 
    
         
            +
                    before(:each) { subject.add(:a) }
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                    it "modifies a column" do
         
     | 
| 
      
 81 
     | 
    
         
            +
                        subject.alter(:a) { |f| f.tag(:altered) }
         
     | 
| 
      
 82 
     | 
    
         
            +
                        expect(subject.tagged(:altered).columns).to eq(%w(a))
         
     | 
| 
      
 83 
     | 
    
         
            +
                    end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                    it "modifies multiple columns" do
         
     | 
| 
      
 86 
     | 
    
         
            +
                        subject.add(:b)
         
     | 
| 
      
 87 
     | 
    
         
            +
                        subject.alter(:a, :b) { |f| f.tag(:altered) }
         
     | 
| 
      
 88 
     | 
    
         
            +
                        expect(subject.tagged(:altered).columns).to eq(%w(a b))
         
     | 
| 
      
 89 
     | 
    
         
            +
                    end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                    it "preserves name and parent" do
         
     | 
| 
      
 92 
     | 
    
         
            +
                        parent = double
         
     | 
| 
      
 93 
     | 
    
         
            +
                        connection = double
         
     | 
| 
      
 94 
     | 
    
         
            +
                        expect(parent).to receive(:connection).and_return(connection)
         
     | 
| 
      
 95 
     | 
    
         
            +
                        subject.set(parent: parent)
         
     | 
| 
      
 96 
     | 
    
         
            +
                        subject.alter(:a) { |f| Janko::TaggedColumn.new }
         
     | 
| 
      
 97 
     | 
    
         
            +
                        expect(subject.columns).to eq(%w(a))
         
     | 
| 
      
 98 
     | 
    
         
            +
                        expect(subject).to be_all { |_, c| c.connection == connection }
         
     | 
| 
      
 99 
     | 
    
         
            +
                    end
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                    it "only works on existing columns" do
         
     | 
| 
      
 102 
     | 
    
         
            +
                        expect(lambda { subject.alter(:b) { |f| f.tag(:b) } }) \
         
     | 
| 
      
 103 
     | 
    
         
            +
                            .to raise_error(RuntimeError)
         
     | 
| 
      
 104 
     | 
    
         
            +
                    end
         
     | 
| 
      
 105 
     | 
    
         
            +
                end
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                it "Janko::ALL" do
         
     | 
| 
      
 108 
     | 
    
         
            +
                    parent = double
         
     | 
| 
      
 109 
     | 
    
         
            +
                    connection = double
         
     | 
| 
      
 110 
     | 
    
         
            +
                    expect(parent).to receive(:table)
         
     | 
| 
      
 111 
     | 
    
         
            +
                    expect(parent).to receive(:connection).and_return(connection)
         
     | 
| 
      
 112 
     | 
    
         
            +
                    expect(connection).to receive(:column_list) \
         
     | 
| 
      
 113 
     | 
    
         
            +
                        .and_return(%w(id a b c))
         
     | 
| 
      
 114 
     | 
    
         
            +
                    subject.set(parent: parent)
         
     | 
| 
      
 115 
     | 
    
         
            +
                    subject.add(Janko::ALL)
         
     | 
| 
      
 116 
     | 
    
         
            +
                    expect(subject.columns).to eq(%w(id a b c))
         
     | 
| 
      
 117 
     | 
    
         
            +
                end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                it "Janko::DEFAULT" do
         
     | 
| 
      
 120 
     | 
    
         
            +
                    parent = double
         
     | 
| 
      
 121 
     | 
    
         
            +
                    connection = double
         
     | 
| 
      
 122 
     | 
    
         
            +
                    expect(parent).to receive(:table)
         
     | 
| 
      
 123 
     | 
    
         
            +
                    expect(parent).to receive(:connection).and_return(connection)
         
     | 
| 
      
 124 
     | 
    
         
            +
                    expect(connection).to receive(:column_list) \
         
     | 
| 
      
 125 
     | 
    
         
            +
                        .and_return(%w(id a b c))
         
     | 
| 
      
 126 
     | 
    
         
            +
                    subject.set(parent: parent)
         
     | 
| 
      
 127 
     | 
    
         
            +
                    subject.add(Janko::DEFAULT)
         
     | 
| 
      
 128 
     | 
    
         
            +
                    expect(subject.columns).to eq(%w(a b c))
         
     | 
| 
      
 129 
     | 
    
         
            +
                end
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
                it "all columns except" do
         
     | 
| 
      
 132 
     | 
    
         
            +
                    parent = double
         
     | 
| 
      
 133 
     | 
    
         
            +
                    connection = double
         
     | 
| 
      
 134 
     | 
    
         
            +
                    expect(parent).to receive(:table)
         
     | 
| 
      
 135 
     | 
    
         
            +
                    expect(parent).to receive(:connection).and_return(connection)
         
     | 
| 
      
 136 
     | 
    
         
            +
                    expect(connection).to receive(:column_list) \
         
     | 
| 
      
 137 
     | 
    
         
            +
                        .and_return(%w(id a b c))
         
     | 
| 
      
 138 
     | 
    
         
            +
                    subject.set(parent: parent)
         
     | 
| 
      
 139 
     | 
    
         
            +
                    subject.add(except: "b")
         
     | 
| 
      
 140 
     | 
    
         
            +
                    expect(subject.columns).to eq(%w(id a c))
         
     | 
| 
      
 141 
     | 
    
         
            +
                end
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                it "#remove" do
         
     | 
| 
      
 144 
     | 
    
         
            +
                    subject.add(:a, :b, :c).remove("b")
         
     | 
| 
      
 145 
     | 
    
         
            +
                    expect(subject.columns).to eq(%w(a c))
         
     | 
| 
      
 146 
     | 
    
         
            +
                end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                it "#to_list" do
         
     | 
| 
      
 149 
     | 
    
         
            +
                    subject.add(:a, :b, :c)
         
     | 
| 
      
 150 
     | 
    
         
            +
                    expect(subject.to_list).to eq("\"a\",\"b\",\"c\"")
         
     | 
| 
      
 151 
     | 
    
         
            +
                end
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                it "#to_binds" do
         
     | 
| 
      
 154 
     | 
    
         
            +
                    subject.add(:a, :b, :c)
         
     | 
| 
      
 155 
     | 
    
         
            +
                    expect(subject.to_binds).to eq("$1,$2,$3")
         
     | 
| 
      
 156 
     | 
    
         
            +
                end
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                it "#inspect includes children" do
         
     | 
| 
      
 159 
     | 
    
         
            +
                    subject.add(:foo, :bar).tag("blue", :bar)
         
     | 
| 
      
 160 
     | 
    
         
            +
                    expect(subject.inspect).to match(/foo/)
         
     | 
| 
      
 161 
     | 
    
         
            +
                    expect(subject.inspect).to match(/bar/)
         
     | 
| 
      
 162 
     | 
    
         
            +
                    expect(subject.inspect).to match(/blue/)
         
     | 
| 
      
 163 
     | 
    
         
            +
                end
         
     | 
| 
      
 164 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,27 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "spec_helper"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "janko/connection"
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            RSpec.describe Janko::Connection do
         
     | 
| 
      
 5 
     | 
    
         
            +
                describe ".build" do
         
     | 
| 
      
 6 
     | 
    
         
            +
                    it "PG::Connection" do
         
     | 
| 
      
 7 
     | 
    
         
            +
                        backend = ActiveRecord::Base.connection.raw_connection
         
     | 
| 
      
 8 
     | 
    
         
            +
                        connection = Janko::Connection.build(backend)
         
     | 
| 
      
 9 
     | 
    
         
            +
                        expect(connection.backend).to be(backend)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    it "ActiveRecord::Base.connection" do
         
     | 
| 
      
 13 
     | 
    
         
            +
                        backend = ActiveRecord::Base.connection
         
     | 
| 
      
 14 
     | 
    
         
            +
                        connection = Janko::Connection.build(backend)
         
     | 
| 
      
 15 
     | 
    
         
            +
                        raw_connection = backend.raw_connection
         
     | 
| 
      
 16 
     | 
    
         
            +
                        expect(connection.backend).to be(raw_connection)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                    it "ActiveRecord::Base" do
         
     | 
| 
      
 20 
     | 
    
         
            +
                        backend = ActiveRecord::Base
         
     | 
| 
      
 21 
     | 
    
         
            +
                        connection = Janko::Connection.build(backend)
         
     | 
| 
      
 22 
     | 
    
         
            +
                        raw_connection = backend.connection.raw_connection
         
     | 
| 
      
 23 
     | 
    
         
            +
                        expect(connection.backend).to be(raw_connection)
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
            end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
    
        data/spec/flag_spec.rb
    ADDED
    
    | 
         @@ -0,0 +1,16 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "spec_helper"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "janko/flag"
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            RSpec.describe Janko::Flag do
         
     | 
| 
      
 5 
     | 
    
         
            +
                def flag(value)
         
     | 
| 
      
 6 
     | 
    
         
            +
                    Janko::Flag.new(value)
         
     | 
| 
      
 7 
     | 
    
         
            +
                end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                it { expect(flag(2 ** 0)).to_not eq(2 ** 0) }
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                it { expect(flag(2 ** 0)).to eq(flag(2 ** 0)) }
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                it { expect(flag(2 ** 0)).to eq(flag(2 ** 0) | flag(2 ** 1)) }
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                it { expect(flag(2 ** 0)).to_not eq(flag(2 ** 2) | flag(2 ** 1)) }
         
     | 
| 
      
 16 
     | 
    
         
            +
            end
         
     | 
    
        data/spec/import_spec.rb
    ADDED
    
    | 
         @@ -0,0 +1,112 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "spec_helper"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "janko/import"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require "pg"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            RSpec.shared_examples_for "an importer" do
         
     | 
| 
      
 6 
     | 
    
         
            +
                describe "insert" do
         
     | 
| 
      
 7 
     | 
    
         
            +
                    let(:subject) { importer.connect(connection) }
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    around :each do |example|
         
     | 
| 
      
 10 
     | 
    
         
            +
                        begin
         
     | 
| 
      
 11 
     | 
    
         
            +
                            subject.table("import_test").columns(:id, :value)
         
     | 
| 
      
 12 
     | 
    
         
            +
                            connection.exec(<<-END)
         
     | 
| 
      
 13 
     | 
    
         
            +
                                BEGIN; CREATE TEMP TABLE import_test(
         
     | 
| 
      
 14 
     | 
    
         
            +
                                    id integer not null, value text)
         
     | 
| 
      
 15 
     | 
    
         
            +
                                    WITHOUT OIDS ON COMMIT DROP;
         
     | 
| 
      
 16 
     | 
    
         
            +
                                CREATE UNIQUE INDEX index_import_test_id
         
     | 
| 
      
 17 
     | 
    
         
            +
                                    ON import_test(id);
         
     | 
| 
      
 18 
     | 
    
         
            +
                            END
         
     | 
| 
      
 19 
     | 
    
         
            +
                            example.run
         
     | 
| 
      
 20 
     | 
    
         
            +
                        rescue
         
     | 
| 
      
 21 
     | 
    
         
            +
                            raise
         
     | 
| 
      
 22 
     | 
    
         
            +
                        ensure
         
     | 
| 
      
 23 
     | 
    
         
            +
                            connection.exec("ROLLBACK")
         
     | 
| 
      
 24 
     | 
    
         
            +
                        end
         
     | 
| 
      
 25 
     | 
    
         
            +
                    end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                    def result
         
     | 
| 
      
 28 
     | 
    
         
            +
                        output = connection.exec(<<-END)
         
     | 
| 
      
 29 
     | 
    
         
            +
                            SELECT id, value FROM import_test ORDER BY id asc
         
     | 
| 
      
 30 
     | 
    
         
            +
                        END
         
     | 
| 
      
 31 
     | 
    
         
            +
                        output.values.map { |v| v.join(",") }.join(";")
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
         
     | 
| 
      
 34 
     | 
    
         
            +
                    it "single row" do
         
     | 
| 
      
 35 
     | 
    
         
            +
                        subject.start.push(id: 1, value: "fish").stop
         
     | 
| 
      
 36 
     | 
    
         
            +
                        expect(result).to eq("1,fish")
         
     | 
| 
      
 37 
     | 
    
         
            +
                    end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                    it "multiple rows" do
         
     | 
| 
      
 40 
     | 
    
         
            +
                        subject.start
         
     | 
| 
      
 41 
     | 
    
         
            +
                        subject.push(id: 1, value: "fish")
         
     | 
| 
      
 42 
     | 
    
         
            +
                        subject.push(id: 2, value: "fish")
         
     | 
| 
      
 43 
     | 
    
         
            +
                        subject.stop
         
     | 
| 
      
 44 
     | 
    
         
            +
                        expect(result).to eq("1,fish;2,fish")
         
     | 
| 
      
 45 
     | 
    
         
            +
                    end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                    it "nullable column" do
         
     | 
| 
      
 48 
     | 
    
         
            +
                        subject.start.push(id: "1").stop
         
     | 
| 
      
 49 
     | 
    
         
            +
                        expect(result).to eq("1,")
         
     | 
| 
      
 50 
     | 
    
         
            +
                    end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    it "violates not-null constraint" do
         
     | 
| 
      
 53 
     | 
    
         
            +
                        subject.start
         
     | 
| 
      
 54 
     | 
    
         
            +
                        row = { value: "fish" }
         
     | 
| 
      
 55 
     | 
    
         
            +
                        expect(lambda { subject.push(row).stop })
         
     | 
| 
      
 56 
     | 
    
         
            +
                            .to raise_error(PG::NotNullViolation)
         
     | 
| 
      
 57 
     | 
    
         
            +
                        connection.exec("ROLLBACK")
         
     | 
| 
      
 58 
     | 
    
         
            +
                    end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                    it "integer overflow" do
         
     | 
| 
      
 61 
     | 
    
         
            +
                        subject.start
         
     | 
| 
      
 62 
     | 
    
         
            +
                        row = { id: 2**32, value: "fish" }
         
     | 
| 
      
 63 
     | 
    
         
            +
                        expect(lambda { subject.push(row).stop })
         
     | 
| 
      
 64 
     | 
    
         
            +
                            .to raise_error(PG::NumericValueOutOfRange)
         
     | 
| 
      
 65 
     | 
    
         
            +
                        connection.exec("ROLLBACK")
         
     | 
| 
      
 66 
     | 
    
         
            +
                    end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                    it "violate uniqueness constraint" do
         
     | 
| 
      
 69 
     | 
    
         
            +
                        subject.start
         
     | 
| 
      
 70 
     | 
    
         
            +
                        row = { id: 1, value: "fish" }
         
     | 
| 
      
 71 
     | 
    
         
            +
                        subject.push(row)
         
     | 
| 
      
 72 
     | 
    
         
            +
                        expect(lambda { subject.push(row).stop })
         
     | 
| 
      
 73 
     | 
    
         
            +
                            .to raise_error(PG::UniqueViolation)
         
     | 
| 
      
 74 
     | 
    
         
            +
                        connection.exec("ROLLBACK")
         
     | 
| 
      
 75 
     | 
    
         
            +
                    end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                    it "violate uniqueness constraint (async errors)" do
         
     | 
| 
      
 78 
     | 
    
         
            +
                        subject.start
         
     | 
| 
      
 79 
     | 
    
         
            +
                        a = { id: 1, value: "fish" }
         
     | 
| 
      
 80 
     | 
    
         
            +
                        b = { id: 2, value: "fish" }
         
     | 
| 
      
 81 
     | 
    
         
            +
                        c = { id: 3, value: "fish" }
         
     | 
| 
      
 82 
     | 
    
         
            +
                        subject.push(b)
         
     | 
| 
      
 83 
     | 
    
         
            +
                        copy = lambda { subject.push(a).push(b).push(c).stop }
         
     | 
| 
      
 84 
     | 
    
         
            +
                        expect(copy).to raise_error(PG::UniqueViolation)
         
     | 
| 
      
 85 
     | 
    
         
            +
                        connection.exec("ROLLBACK")
         
     | 
| 
      
 86 
     | 
    
         
            +
                    end
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                    it "too many columns" do
         
     | 
| 
      
 89 
     | 
    
         
            +
                        subject.start
         
     | 
| 
      
 90 
     | 
    
         
            +
                        row = [ 1, "fish", "woah" ]
         
     | 
| 
      
 91 
     | 
    
         
            +
                        expect(lambda { subject.push(*row).stop })
         
     | 
| 
      
 92 
     | 
    
         
            +
                            .to raise_error(ArgumentError)
         
     | 
| 
      
 93 
     | 
    
         
            +
                        connection.exec("ROLLBACK")
         
     | 
| 
      
 94 
     | 
    
         
            +
                    end
         
     | 
| 
      
 95 
     | 
    
         
            +
                end
         
     | 
| 
      
 96 
     | 
    
         
            +
            end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
            RSpec.describe Janko::Import do
         
     | 
| 
      
 99 
     | 
    
         
            +
                let(:connection) { ActiveRecord::Base.connection.raw_connection }
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                describe "#strategy insert" do
         
     | 
| 
      
 102 
     | 
    
         
            +
                    let(:importer) { Janko::Import.new.use(Janko::InsertImporter) }
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                    it_behaves_like "an importer"
         
     | 
| 
      
 105 
     | 
    
         
            +
                end
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                describe "#strategy copy" do
         
     | 
| 
      
 108 
     | 
    
         
            +
                    let(:importer) { Janko::Import.new.use(Janko::CopyImporter) }
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                    it_behaves_like "an importer"
         
     | 
| 
      
 111 
     | 
    
         
            +
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
            end
         
     |