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.
data/lib/janko/flag.rb ADDED
@@ -0,0 +1,26 @@
1
+ module Janko
2
+ class Flag
3
+ attr_reader :value
4
+
5
+ def initialize(value)
6
+ @value = value
7
+ end
8
+
9
+ def |(other)
10
+ self.class.new(value | other.value)
11
+ end
12
+
13
+ def eql?(other)
14
+ return unless other.is_a?(self.class)
15
+ (value & other.value) != 0
16
+ end
17
+
18
+ def ==(other)
19
+ eql?(other)
20
+ end
21
+
22
+ def ===(other)
23
+ eql?(other)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,80 @@
1
+ require "agrippa/mutable_hash"
2
+ require "janko/connection"
3
+ require "janko/column_list"
4
+ require "janko/copy_importer"
5
+ require "janko/insert_importer"
6
+ require "janko/constants"
7
+
8
+ # http://starfighter.ngrok.io/
9
+
10
+ # delegate :start, :stop, :push, to: :delegate
11
+ # set(table:, columns:)
12
+ #
13
+ # connect(connection)
14
+ # builder => self
15
+
16
+ module Janko
17
+ class Import
18
+ include Agrippa::MutableHash
19
+
20
+ state_reader :connection
21
+
22
+ state_writer :table, prefix: false
23
+
24
+ def default_state
25
+ { columns: Janko::ALL, importer: Janko::CopyImporter }
26
+ end
27
+
28
+ def connect(connection)
29
+ @state[:connection] = Connection.build(connection)
30
+ self
31
+ end
32
+
33
+ def use(importer)
34
+ @state[:importer] = importer
35
+ self
36
+ end
37
+
38
+ def columns(*columns)
39
+ @state[:columns] = columns.flatten
40
+ self
41
+ end
42
+
43
+ def start
44
+ @state[:started] = true
45
+ delegate.start
46
+ self
47
+ end
48
+
49
+ def push(values)
50
+ raise("Call #start before #push") unless @state[:started]
51
+ delegate.push(values)
52
+ self
53
+ end
54
+
55
+ def stop
56
+ raise("Call #start before #stop") unless @state[:started]
57
+ delegate.stop
58
+ @state[:started] = false
59
+ self
60
+ end
61
+
62
+ private
63
+
64
+ def preserve_state_if_started
65
+ return(self) unless @state[:started]
66
+ raise("Call #stop before changing import options.")
67
+ end
68
+
69
+ def delegate
70
+ @delegate ||= @state[:importer].new(delegate_options)
71
+ end
72
+
73
+ def delegate_options
74
+ raise("No table specified.") unless @state[:table]
75
+ column_list = ColumnList.build(@state[:columns])
76
+ raise("No columns specified.") if column_list.empty?
77
+ @state.merge(columns: column_list)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,32 @@
1
+ require "agrippa/mutable"
2
+
3
+ module Janko
4
+ class InsertImporter
5
+ include Agrippa::Mutable
6
+
7
+ state_reader :connection, :table, :columns
8
+
9
+ def start
10
+ query = sprintf("INSERT INTO %s(%s) VALUES(%s)", table,
11
+ columns.to_list, columns.to_binds)
12
+ connection.prepare(statement_name, query)
13
+ self
14
+ end
15
+
16
+ def push(values)
17
+ connection.exec_prepared(statement_name, columns.pack(values))
18
+ self
19
+ end
20
+
21
+ def stop
22
+ connection.exec("DEALLOCATE \"#{statement_name}\"")
23
+ self
24
+ end
25
+
26
+ private
27
+
28
+ def statement_name
29
+ @statement_name ||= "import-#{SecureRandom.hex(8)}"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,167 @@
1
+ require "agrippa/mutable_hash"
2
+ require "agrippa/delegation"
3
+ require "janko/connection"
4
+ require "janko/column_list"
5
+ require "janko/single_merge"
6
+ require "janko/bulk_merge"
7
+
8
+ module Janko
9
+ class Merge
10
+ include Agrippa::MutableHash
11
+
12
+ include Agrippa::Delegation
13
+
14
+ state_writer :table, :locking, :transaction, :collector
15
+
16
+ state_reader :table, :connection
17
+
18
+ delegate :exec, to: "connection"
19
+
20
+ delegate :result, to: "delegate"
21
+
22
+ def default_state
23
+ { strategy: Janko::BulkMerge, connection: Connection.default }
24
+ end
25
+
26
+ def connect(connection)
27
+ @state[:connection] = Connection.build(connection)
28
+ self
29
+ end
30
+
31
+ def use(strategy)
32
+ @state[:strategy] = strategy
33
+ self
34
+ end
35
+
36
+ def returning(returning)
37
+ returning = returning.to_s
38
+ raise("Merge can return inserted, updated, all, or none.") \
39
+ unless %w(inserted updated all none).include?(returning)
40
+ chain(returning: returning)
41
+ end
42
+
43
+ def key(*list)
44
+ preserve_state_if_started
45
+ columns.tag("key", *list)
46
+ self
47
+ end
48
+
49
+ def update(*list)
50
+ preserve_state_if_started
51
+ columns.tag("update", *list)
52
+ self
53
+ end
54
+
55
+ def insert(*list)
56
+ preserve_state_if_started
57
+ columns.tag("insert", *list)
58
+ self
59
+ end
60
+
61
+ def select(*list)
62
+ preserve_state_if_started
63
+ columns.tag("select", *list)
64
+ self
65
+ end
66
+
67
+ def alter(*list, &block)
68
+ preserve_state_if_started
69
+ columns.alter(*list, &block)
70
+ self
71
+ end
72
+
73
+ def start
74
+ @state[:started] = true
75
+ reset_delegate
76
+ begin_transaction and lock_table
77
+ rollback_on_error { delegate.start }
78
+ self
79
+ end
80
+
81
+ def push(*args)
82
+ raise("Call #start before #push") unless @state[:started]
83
+ rollback_on_error { delegate.push(*args) }
84
+ self
85
+ end
86
+
87
+ def stop
88
+ raise("Call #start before #stop") unless @state[:started]
89
+ rollback_on_error { delegate.stop }
90
+ @state[:started] = false
91
+ self
92
+ end
93
+
94
+ def columns
95
+ @state[:columns] ||= begin
96
+ raise("Connect before setting merge parameters.") \
97
+ unless connected?
98
+ default_column_list
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def connected?
105
+ @state.has_key?(:connection)
106
+ end
107
+
108
+ def default_column_list
109
+ ColumnList.new(parent: self)
110
+ .tag(:key, :id)
111
+ .tag(:select, Janko::ALL)
112
+ .tag(:insert, Janko::ALL).untag(:insert, :id)
113
+ .tag(:update, Janko::ALL).untag(:update, :id)
114
+ end
115
+
116
+ def rollback_on_error
117
+ begin
118
+ yield
119
+ rescue
120
+ raise
121
+ ensure
122
+ commit_or_rollback_transaction
123
+ end
124
+ end
125
+
126
+ def begin_transaction
127
+ return(self) if connection.in_transaction?
128
+ return(self) if (@state[:transaction] == false)
129
+ @state[:our_transaction] == true
130
+ exec("BEGIN")
131
+ self
132
+ end
133
+
134
+ def commit_or_rollback_transaction
135
+ return(self) unless connection.in_transaction?
136
+ return(self) unless @state.delete(:our_transaction)
137
+ exec(connection.failed? ? "ROLLBACK" : "COMMIT")
138
+ self
139
+ end
140
+
141
+ def lock_table
142
+ return(self) unless connection.in_transaction?
143
+ return(self) if (@state[:locking] == false)
144
+ exec("LOCK TABLE #{table} IN SHARE ROW EXCLUSIVE MODE;")
145
+ self
146
+ end
147
+
148
+ def preserve_state_if_started
149
+ return(self) unless @state[:started]
150
+ raise("Call #stop before changing import options.")
151
+ end
152
+
153
+ def delegate
154
+ @delegate ||= begin
155
+ strategy_class = @state[:strategy]
156
+ strategy_class || raise("Set strategy before merging.")
157
+ strategy_class.new(@state.slice(:connection, :table,
158
+ :returning, :collector).merge(columns: columns))
159
+ end
160
+ end
161
+
162
+ def reset_delegate
163
+ @delegate = nil
164
+ self
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,37 @@
1
+ module Janko
2
+ class MergeResult
3
+ include Enumerable
4
+
5
+ attr_reader :count
6
+
7
+ def initialize
8
+ @tuples = Hash.new { |h, k| h[k] = [] }
9
+ @count = 0
10
+ end
11
+
12
+ def push(tag, tuple)
13
+ @tuples[tag.to_s].push(tuple)
14
+ @count += 1
15
+ self
16
+ end
17
+
18
+ def inserted
19
+ @tuples["inserted"]
20
+ end
21
+
22
+ def updated
23
+ @tuples["updated"]
24
+ end
25
+
26
+ def clear
27
+ @tuples.clear
28
+ self
29
+ end
30
+
31
+ def each(&block)
32
+ inserted.each(&block)
33
+ updated.each(&block)
34
+ self
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ require "janko/merge_result"
2
+ require "janko/upsert"
3
+
4
+ module Janko
5
+ class SingleMerge
6
+ attr_reader :upsert
7
+
8
+ def initialize(options = {})
9
+ @upsert = Upsert.new(options)
10
+ @options = options
11
+ end
12
+
13
+ def start
14
+ upsert.result.clear
15
+ upsert.prepare if @options[:use_prepared_query]
16
+ self
17
+ end
18
+
19
+ def push(*values)
20
+ upsert.push(*values)
21
+ self
22
+ end
23
+
24
+ def stop
25
+ upsert.cleanup
26
+ self
27
+ end
28
+
29
+ def result
30
+ upsert.result
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,133 @@
1
+ require "janko/constants"
2
+ require "agrippa/state"
3
+ require "agrippa/mutable"
4
+ require "agrippa/delegation"
5
+
6
+ module Janko
7
+ include Constants
8
+
9
+ class TaggedColumn
10
+ include Agrippa::Delegation
11
+
12
+ include Agrippa::Mutable
13
+
14
+ state_reader :tags, :name, :parent
15
+
16
+ state_writer :wrap, :default, :on_update, prefix: false
17
+
18
+ delegate :table, :connection, to: :parent
19
+
20
+ def default_state
21
+ { tags: {} }
22
+ end
23
+
24
+ def set(updates)
25
+ chain(updates)
26
+ end
27
+
28
+ def tag(tag)
29
+ tag = tag.to_s
30
+ tags.merge!(tag => true) unless (tag == "")
31
+ self
32
+ end
33
+
34
+ def untag(tag)
35
+ tag = tag.to_s
36
+ tags.reject! { |k| k == tag } unless (tag == "")
37
+ self
38
+ end
39
+
40
+ def has_tag?(tag)
41
+ tags.has_key?(tag.to_s)
42
+ end
43
+
44
+ def tagged?
45
+ not tags.empty?
46
+ end
47
+
48
+ def to_s
49
+ tags.keys.join(" ")
50
+ end
51
+
52
+ def type
53
+ connection.column_type(table, name)
54
+ end
55
+
56
+ # FIXME: Quoting
57
+ def quoted(prefix = nil)
58
+ prefix.nil? ? "\"#{name}\"" : "\"#{prefix}\".\"#{name}\""
59
+ end
60
+
61
+ def to_condition(left, right)
62
+ "#{quoted(left)} = #{quoted(right)}"
63
+ end
64
+
65
+ def to_setter(left, right)
66
+ "#{quoted} = #{maybe_on_update(left, right)}"
67
+ end
68
+
69
+ def to_value(prefix = nil)
70
+ maybe_wrap(nil, prefix)
71
+ end
72
+
73
+ def to_bind(position)
74
+ "$#{position}"
75
+ end
76
+
77
+ def to_typecast_bind(position)
78
+ "$#{position}::#{type}"
79
+ end
80
+
81
+ def inspect
82
+ children = "(#{tags.keys.join(" ")})"
83
+ "#<#{self.class}:0x#{self.__id__.to_s(16)} #{name}#{children}>"
84
+ end
85
+
86
+ private
87
+
88
+ def maybe_wrap(left, right)
89
+ inner = maybe_default(left, right)
90
+ return(inner) unless @wrap
91
+ @wrap.gsub(/\$NEW/i) { inner }
92
+ end
93
+
94
+ def maybe_on_update(left, right)
95
+ inner = maybe_wrap(left, right)
96
+ return(inner) unless @on_update
97
+ output = Agrippa::State.new(@on_update, :gsub)
98
+ output.gsub(/\$NEW/i) { inner }
99
+ output.gsub(/\$OLD/i) { quoted(left) }
100
+ output._value
101
+ end
102
+
103
+ def maybe_default(left, right)
104
+ values = [ quoted(right) ]
105
+ values.push(keep_existing_value(left))
106
+ values.push(column_default_value)
107
+ values.compact!
108
+ return(values.first) if (values.length == 1)
109
+ "COALESCE(#{values.join(", ")})"
110
+ end
111
+
112
+ def keep_existing_value(prefix)
113
+ return if prefix.nil?
114
+ return unless (value = @default)
115
+ return unless (value == Janko::KEEP)
116
+ quoted(prefix)
117
+ end
118
+
119
+ def column_default_value
120
+ @column_default_value ||= begin
121
+ return unless (value = @default)
122
+ return(value) unless value.is_a?(Flag)
123
+ return unless (value == Janko::DEFAULT)
124
+ connection.column_default(table, name)
125
+ end
126
+ end
127
+
128
+ def flagged(value, flag)
129
+ return(false) unless value.is_a?(Fixnum)
130
+ (value & flag) == value
131
+ end
132
+ end
133
+ end