pgsync 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pgsync might be problematic. Click here for more details.

@@ -0,0 +1,235 @@
1
+ module PgSync
2
+ class TaskResolver
3
+ include Utils
4
+
5
+ attr_reader :args, :opts, :source, :destination, :config, :first_schema, :notes
6
+
7
+ def initialize(args:, opts:, source:, destination:, config:, first_schema:)
8
+ @args = args
9
+ @opts = opts
10
+ @source = source
11
+ @destination = destination
12
+ @config = config
13
+ @groups = config["groups"] || {}
14
+ @first_schema = first_schema
15
+ @notes = []
16
+ end
17
+
18
+ def tasks
19
+ tasks = []
20
+
21
+ # get lists from args
22
+ groups, tables = process_args
23
+
24
+ # expand groups into tasks
25
+ groups.each do |group|
26
+ tasks.concat(group_to_tasks(group))
27
+ end
28
+
29
+ # expand tables into tasks
30
+ tables.each do |table|
31
+ tasks.concat(table_to_tasks(table))
32
+ end
33
+
34
+ # get default if none given
35
+ if !opts[:groups] && !opts[:tables] && args.size == 0
36
+ tasks.concat(default_tasks)
37
+ end
38
+
39
+ # resolve any tables that need it
40
+ tasks.each do |task|
41
+ task[:table] = fully_resolve(task[:table])
42
+ end
43
+
44
+ tasks
45
+ end
46
+
47
+ def group?(group)
48
+ @groups.key?(group)
49
+ end
50
+
51
+ private
52
+
53
+ def group_to_tasks(value)
54
+ group, param = value.split(":", 2)
55
+ raise Error, "Group not found: #{group}" unless group?(group)
56
+
57
+ @groups[group].map do |table|
58
+ table_sql = nil
59
+ if table.is_a?(Array)
60
+ table, table_sql = table
61
+ end
62
+
63
+ {
64
+ table: to_table(table),
65
+ sql: expand_sql(table_sql, param)
66
+ }
67
+ end
68
+ end
69
+
70
+ def table_to_tasks(value)
71
+ raise Error, "Cannot use parameters with tables" if value.include?(":")
72
+
73
+ tables =
74
+ if value.include?("*")
75
+ regex = Regexp.new('\A' + Regexp.escape(value).gsub('\*','[^\.]*') + '\z')
76
+ shared_tables.select { |t| regex.match(t.full_name) || regex.match(t.name) }
77
+ else
78
+ [to_table(value)]
79
+ end
80
+
81
+ tables.map do |table|
82
+ {
83
+ table: table,
84
+ sql: sql_arg # doesn't support params
85
+ }
86
+ end
87
+ end
88
+
89
+ # treats identifiers as if they were quoted (Users == "Users")
90
+ # this is different from Postgres (Users == "users")
91
+ #
92
+ # TODO add support for quoted identifiers like "my.schema"."my.table"
93
+ # so it's possible to specify identifiers with "." in them
94
+ def to_table(value)
95
+ parts = value.split(".")
96
+ case parts.size
97
+ when 1
98
+ # unknown schema
99
+ Table.new(nil, parts[0])
100
+ when 2
101
+ Table.new(*parts)
102
+ else
103
+ raise Error, "Cannot resolve table: #{value}"
104
+ end
105
+ end
106
+
107
+ def default_tasks
108
+ shared_tables.map do |table|
109
+ {
110
+ table: table
111
+ }
112
+ end
113
+ end
114
+
115
+ # tables that exists in both source and destination
116
+ # used when no tables specified, or a wildcard
117
+ # removes excluded tables and filters by schema
118
+ def shared_tables
119
+ tables = filter_tables(source.tables)
120
+
121
+ unless opts[:schema_only] || opts[:schema_first]
122
+ from_tables = tables
123
+ to_tables = filter_tables(destination.tables)
124
+
125
+ extra_tables = to_tables - from_tables
126
+ notes << "Extra tables: #{extra_tables.map { |t| friendly_name(t) }.join(", ")}" if extra_tables.any?
127
+
128
+ missing_tables = from_tables - to_tables
129
+ notes << "Missing tables: #{missing_tables.map { |t| friendly_name(t) }.join(", ")}" if missing_tables.any?
130
+
131
+ tables &= to_tables
132
+ end
133
+
134
+ tables
135
+ end
136
+
137
+ def filter_tables(tables)
138
+ tables = tables.dup
139
+
140
+ unless opts[:all_schemas]
141
+ # could support wildcard schemas as well
142
+ schemas = Set.new(opts[:schemas] ? to_arr(opts[:schemas]) : source.search_path)
143
+ tables.select! { |t| schemas.include?(t.schema) }
144
+ end
145
+
146
+ to_arr(opts[:exclude]).each do |value|
147
+ if value.include?("*")
148
+ regex = Regexp.new('\A' + Regexp.escape(value).gsub('\*','[^\.]*') + '\z')
149
+ tables.reject! { |t| regex.match(t.full_name) || regex.match(t.name) }
150
+ else
151
+ tables -= [fully_resolve(to_table(value))]
152
+ end
153
+ end
154
+
155
+ tables
156
+ end
157
+
158
+ def process_args
159
+ groups = to_arr(opts[:groups])
160
+ tables = to_arr(opts[:tables])
161
+ if args[0]
162
+ # could be a group, table, or mix
163
+ to_arr(args[0]).each do |value|
164
+ if group?(value.split(":", 2)[0])
165
+ groups << value
166
+ else
167
+ tables << value
168
+ end
169
+ end
170
+ end
171
+ [groups, tables]
172
+ end
173
+
174
+ def no_schema_tables
175
+ @no_schema_tables ||= begin
176
+ search_path_index = source.search_path.map.with_index.to_h
177
+ source.tables.group_by(&:name).map do |group, t2|
178
+ [group, t2.select { |t| search_path_index[t.schema] }.sort_by { |t| search_path_index[t.schema] }.first]
179
+ end.to_h
180
+ end
181
+ end
182
+
183
+ # for tables without a schema, find the table in the search path
184
+ def fully_resolve(table)
185
+ return table if table.schema
186
+ no_schema_tables[table.name] || (raise Error, "Table not found in source: #{table.name}")
187
+ end
188
+
189
+ # parse command line arguments and YAML
190
+ def to_arr(value)
191
+ if value.is_a?(Array)
192
+ value
193
+ else
194
+ # Split by commas, but don't use commas inside double quotes
195
+ # https://stackoverflow.com/questions/21105360/regex-find-comma-not-inside-quotes
196
+ value.to_s.split(/(?!\B"[^"]*),(?![^"]*"\B)/)
197
+ end
198
+ end
199
+
200
+ def sql_arg
201
+ args[1]
202
+ end
203
+
204
+ def expand_sql(sql, param)
205
+ # command line option takes precedence over group option
206
+ sql = sql_arg if sql_arg
207
+
208
+ return unless sql
209
+
210
+ # vars must match \w
211
+ missing_vars = sql.scan(/{\w+}/).map { |v| v[1..-2] }
212
+
213
+ vars = {}
214
+ if param
215
+ vars["id"] = cast(param)
216
+ vars["1"] = cast(param)
217
+ end
218
+
219
+ sql = sql.dup
220
+ vars.each do |k, v|
221
+ # only sub if in var list
222
+ sql.gsub!("{#{k}}", cast(v)) if missing_vars.delete(k)
223
+ end
224
+
225
+ raise Error, "Missing variables: #{missing_vars.uniq.join(", ")}" if missing_vars.any?
226
+
227
+ sql
228
+ end
229
+
230
+ # TODO quote vars in next major version
231
+ def cast(value)
232
+ value.to_s.gsub(/\A\"|\"\z/, '')
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,86 @@
1
+ module PgSync
2
+ module Utils
3
+ COLOR_CODES = {
4
+ red: 31,
5
+ green: 32,
6
+ yellow: 33
7
+ }
8
+
9
+ def log(message = nil)
10
+ output.puts message
11
+ end
12
+
13
+ def colorize(message, color)
14
+ if output.tty?
15
+ "\e[#{COLOR_CODES[color]}m#{message}\e[0m"
16
+ else
17
+ message
18
+ end
19
+ end
20
+
21
+ def warning(message)
22
+ log colorize(message, :yellow)
23
+ end
24
+
25
+ def deprecated(message)
26
+ warning "[DEPRECATED] #{message}"
27
+ end
28
+
29
+ def output
30
+ $stderr
31
+ end
32
+
33
+ def db_config_file(db)
34
+ ".pgsync-#{db}.yml"
35
+ end
36
+
37
+ def confirm_tables_exist(data_source, tasks, description)
38
+ tasks.map(&:table).each do |table|
39
+ unless data_source.table_exists?(table)
40
+ raise Error, "Table not found in #{description}: #{table}"
41
+ end
42
+ end
43
+ end
44
+
45
+ def first_schema
46
+ @first_schema ||= source.search_path.find { |sp| sp != "pg_catalog" }
47
+ end
48
+
49
+ def task_name(task)
50
+ friendly_name(task.table)
51
+ end
52
+
53
+ def friendly_name(table)
54
+ if table.schema == first_schema
55
+ table.name
56
+ else
57
+ table.full_name
58
+ end
59
+ end
60
+
61
+ def quote_ident_full(ident)
62
+ if ident.is_a?(Table)
63
+ [quote_ident(ident.schema), quote_ident(ident.name)].join(".")
64
+ else # temp table names are strings
65
+ quote_ident(ident)
66
+ end
67
+ end
68
+
69
+ def quote_ident(value)
70
+ PG::Connection.quote_ident(value)
71
+ end
72
+
73
+ def escape(value)
74
+ if value.is_a?(String)
75
+ "'#{quote_string(value)}'"
76
+ else
77
+ value
78
+ end
79
+ end
80
+
81
+ # activerecord
82
+ def quote_string(s)
83
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''")
84
+ end
85
+ end
86
+ end
@@ -1,3 +1,3 @@
1
1
  module PgSync
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgsync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-27 00:00:00.000000000 Z
11
+ date: 2020-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parallel
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 4.2.0
47
+ version: 4.8.1
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 4.2.0
54
+ version: 4.8.1
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: tty-spinner
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -123,8 +123,14 @@ files:
123
123
  - lib/pgsync.rb
124
124
  - lib/pgsync/client.rb
125
125
  - lib/pgsync/data_source.rb
126
- - lib/pgsync/table_list.rb
126
+ - lib/pgsync/init.rb
127
+ - lib/pgsync/schema_sync.rb
128
+ - lib/pgsync/sync.rb
129
+ - lib/pgsync/table.rb
127
130
  - lib/pgsync/table_sync.rb
131
+ - lib/pgsync/task.rb
132
+ - lib/pgsync/task_resolver.rb
133
+ - lib/pgsync/utils.rb
128
134
  - lib/pgsync/version.rb
129
135
  homepage: https://github.com/ankane/pgsync
130
136
  licenses:
@@ -1,107 +0,0 @@
1
- module PgSync
2
- class TableList
3
- attr_reader :args, :opts, :source, :config
4
-
5
- def initialize(args, options, source, config)
6
- @args = args
7
- @opts = options
8
- @source = source
9
- @config = config
10
- end
11
-
12
- def tables
13
- tables = nil
14
-
15
- if opts[:groups]
16
- tables ||= Hash.new { |hash, key| hash[key] = {} }
17
- specified_groups = to_arr(opts[:groups])
18
- specified_groups.map do |tag|
19
- group, id = tag.split(":", 2)
20
- if (t = (config["groups"] || {})[group])
21
- add_tables(tables, t, id, args[1])
22
- else
23
- raise PgSync::Error, "Group not found: #{group}"
24
- end
25
- end
26
- end
27
-
28
- if opts[:tables]
29
- tables ||= Hash.new { |hash, key| hash[key] = {} }
30
- to_arr(opts[:tables]).each do |tag|
31
- table, id = tag.split(":", 2)
32
- raise PgSync::Error, "Cannot use parameters with tables" if id
33
- add_table(tables, table, id, args[1])
34
- end
35
- end
36
-
37
- if args[0]
38
- # could be a group, table, or mix
39
- tables ||= Hash.new { |hash, key| hash[key] = {} }
40
- specified_groups = to_arr(args[0])
41
- specified_groups.map do |tag|
42
- group, id = tag.split(":", 2)
43
- if (t = (config["groups"] || {})[group])
44
- add_tables(tables, t, id, args[1])
45
- else
46
- raise PgSync::Error, "Cannot use parameters with tables" if id
47
- add_table(tables, group, id, args[1])
48
- end
49
- end
50
- end
51
-
52
- tables ||= begin
53
- exclude = to_arr(opts[:exclude])
54
- exclude = source.fully_resolve_tables(exclude).keys if exclude.any?
55
-
56
- tabs = source.tables
57
- unless opts[:all_schemas]
58
- schemas = Set.new(opts[:schemas] ? to_arr(opts[:schemas]) : source.search_path)
59
- tabs.select! { |t| schemas.include?(t.split(".", 2)[0]) }
60
- end
61
-
62
- Hash[(tabs - exclude).map { |k| [k, {}] }]
63
- end
64
-
65
- source.fully_resolve_tables(tables)
66
- end
67
-
68
- private
69
-
70
- def to_arr(value)
71
- if value.is_a?(Array)
72
- value
73
- else
74
- # Split by commas, but don't use commas inside double quotes
75
- # https://stackoverflow.com/questions/21105360/regex-find-comma-not-inside-quotes
76
- value.to_s.split(/(?!\B"[^"]*),(?![^"]*"\B)/)
77
- end
78
- end
79
-
80
- def add_tables(tables, t, id, boom)
81
- t.each do |table|
82
- sql = nil
83
- if table.is_a?(Array)
84
- table, sql = table
85
- end
86
- add_table(tables, table, id, boom || sql)
87
- end
88
- end
89
-
90
- def add_table(tables, table, id, boom, wildcard = false)
91
- if table.include?("*") && !wildcard
92
- regex = Regexp.new('\A' + Regexp.escape(table).gsub('\*','[^\.]*') + '\z')
93
- t2 = source.tables.select { |t| regex.match(t) }
94
- t2.each do |tab|
95
- add_table(tables, tab, id, boom, true)
96
- end
97
- else
98
- tables[table] = {}
99
- tables[table][:sql] = boom.gsub("{id}", cast(id)).gsub("{1}", cast(id)) if boom
100
- end
101
- end
102
-
103
- def cast(value)
104
- value.to_s.gsub(/\A\"|\"\z/, '')
105
- end
106
- end
107
- end