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.
@@ -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,3 @@
1
+ module Janko
2
+ VERSION = "0.0.2"
3
+ 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
@@ -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