janko 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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