flydata 0.6.14 → 0.7.0

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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/flydata-core/Gemfile +1 -0
  4. data/flydata-core/Gemfile.lock +5 -0
  5. data/flydata-core/lib/flydata-core/errors.rb +4 -2
  6. data/flydata-core/lib/flydata-core/mysql/binlog_pos.rb +4 -0
  7. data/flydata-core/lib/flydata-core/postgresql/compatibility_checker.rb +119 -0
  8. data/flydata-core/lib/flydata-core/postgresql/config.rb +58 -0
  9. data/flydata-core/lib/flydata-core/postgresql/pg_client.rb +170 -0
  10. data/flydata-core/lib/flydata-core/postgresql/snapshot.rb +49 -0
  11. data/flydata-core/lib/flydata-core/postgresql/source_pos.rb +71 -10
  12. data/flydata-core/lib/flydata-core/table_def/mysql_table_def.rb +1 -1
  13. data/flydata-core/lib/flydata-core/table_def/postgresql_table_def.rb +76 -17
  14. data/flydata-core/lib/flydata-core/table_def/redshift_table_def.rb +59 -10
  15. data/flydata-core/spec/mysql/binlog_pos_spec.rb +10 -2
  16. data/flydata-core/spec/postgresql/compatibility_checker_spec.rb +148 -0
  17. data/flydata-core/spec/postgresql/config_spec.rb +85 -0
  18. data/flydata-core/spec/postgresql/pg_client_spec.rb +195 -0
  19. data/flydata-core/spec/postgresql/snapshot_spec.rb +55 -0
  20. data/flydata-core/spec/postgresql/source_pos_spec.rb +70 -8
  21. data/flydata-core/spec/table_def/postgresql_table_def_spec.rb +80 -19
  22. data/flydata-core/spec/table_def/redshift_table_def_spec.rb +211 -14
  23. data/flydata.gemspec +0 -0
  24. data/lib/flydata.rb +1 -0
  25. data/lib/flydata/command/sender.rb +10 -7
  26. data/lib/flydata/command/sync.rb +4 -1
  27. data/lib/flydata/fluent-plugins/flydata_plugin_ext/base.rb +1 -0
  28. data/lib/flydata/fluent-plugins/flydata_plugin_ext/fluent_log_ext.rb +73 -0
  29. data/lib/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync.rb +35 -10
  30. data/lib/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_diff_based.rb +29 -0
  31. data/lib/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_query_based.rb +26 -0
  32. data/lib/flydata/fluent-plugins/flydata_plugin_ext/preference.rb +29 -13
  33. data/lib/flydata/fluent-plugins/in_mysql_binlog_flydata.rb +10 -18
  34. data/lib/flydata/fluent-plugins/in_postgresql_query_based_flydata.rb +64 -0
  35. data/lib/flydata/helpers.rb +1 -3
  36. data/lib/flydata/plugin_support/context.rb +14 -2
  37. data/lib/flydata/plugin_support/source_position_file.rb +35 -0
  38. data/lib/flydata/plugin_support/sync_record_emittable.rb +2 -1
  39. data/lib/flydata/query_based_sync/client.rb +101 -0
  40. data/lib/flydata/query_based_sync/record_size_estimator.rb +39 -0
  41. data/lib/flydata/query_based_sync/resource_requester.rb +70 -0
  42. data/lib/flydata/query_based_sync/response.rb +122 -0
  43. data/lib/flydata/query_based_sync/response_handler.rb +30 -0
  44. data/lib/flydata/source/sync_generate_table_ddl.rb +1 -1
  45. data/lib/flydata/source_mysql/plugin_support/binlog_record_dispatcher.rb +2 -2
  46. data/lib/flydata/source_mysql/plugin_support/binlog_record_handler.rb +3 -9
  47. data/lib/flydata/source_mysql/plugin_support/context.rb +26 -2
  48. data/lib/flydata/source_mysql/plugin_support/source_position_file.rb +14 -0
  49. data/lib/flydata/source_mysql/table_ddl.rb +3 -3
  50. data/lib/flydata/source_mysql/{plugin_support/table_meta.rb → table_meta.rb} +3 -10
  51. data/lib/flydata/source_postgresql/generate_source_dump.rb +44 -63
  52. data/lib/flydata/source_postgresql/parse_dump_and_send.rb +2 -0
  53. data/lib/flydata/source_postgresql/plugin_support/context.rb +13 -0
  54. data/lib/flydata/source_postgresql/plugin_support/source_position_file.rb +14 -0
  55. data/lib/flydata/source_postgresql/query_based_sync/client.rb +16 -0
  56. data/lib/flydata/source_postgresql/query_based_sync/diff_query_generator.rb +135 -0
  57. data/lib/flydata/source_postgresql/query_based_sync/resource_requester.rb +86 -0
  58. data/lib/flydata/source_postgresql/query_based_sync/response.rb +12 -0
  59. data/lib/flydata/source_postgresql/query_based_sync/response_handler.rb +12 -0
  60. data/lib/flydata/source_postgresql/sync_generate_table_ddl.rb +25 -79
  61. data/lib/flydata/source_postgresql/table_meta.rb +168 -0
  62. data/lib/flydata/sync_file_manager.rb +5 -5
  63. data/lib/flydata/table_meta.rb +19 -0
  64. data/spec/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_context.rb +85 -0
  65. data/spec/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_diff_based_shared_examples.rb +36 -0
  66. data/spec/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_query_based_shared_examples.rb +37 -0
  67. data/spec/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_shared_examples.rb +67 -0
  68. data/spec/flydata/fluent-plugins/in_mysql_binlog_flydata_spec.rb +119 -96
  69. data/spec/flydata/fluent-plugins/in_postgresql_query_based_flydata_spec.rb +82 -0
  70. data/spec/flydata/fluent-plugins/sync_source_plugin_context.rb +29 -0
  71. data/spec/flydata/plugin_support/context_spec.rb +37 -3
  72. data/spec/flydata/query_based_sync/client_spec.rb +79 -0
  73. data/spec/flydata/query_based_sync/query_based_sync_context.rb +116 -0
  74. data/spec/flydata/query_based_sync/record_size_estimator_spec.rb +54 -0
  75. data/spec/flydata/query_based_sync/resource_requester_spec.rb +58 -0
  76. data/spec/flydata/query_based_sync/response_handler_spec.rb +36 -0
  77. data/spec/flydata/query_based_sync/response_spec.rb +157 -0
  78. data/spec/flydata/source_mysql/plugin_support/context_spec.rb +7 -1
  79. data/spec/flydata/source_mysql/plugin_support/dml_record_handler_spec.rb +2 -15
  80. data/spec/flydata/source_mysql/plugin_support/drop_database_query_handler_spec.rb +1 -1
  81. data/spec/flydata/source_mysql/plugin_support/shared_query_handler_context.rb +12 -11
  82. data/spec/flydata/source_mysql/plugin_support/source_position_file_spec.rb +53 -0
  83. data/spec/flydata/source_mysql/plugin_support/truncate_query_handler_spec.rb +1 -1
  84. data/spec/flydata/source_mysql/table_ddl_spec.rb +5 -5
  85. data/spec/flydata/source_mysql/{plugin_support/table_meta_spec.rb → table_meta_spec.rb} +6 -7
  86. data/spec/flydata/source_postgresql/generate_source_dump_spec.rb +165 -77
  87. data/spec/flydata/source_postgresql/query_based_sync/diff_query_generator_spec.rb +213 -0
  88. data/spec/flydata/source_postgresql/query_based_sync/query_based_sync_postgresql_context.rb +76 -0
  89. data/spec/flydata/source_postgresql/query_based_sync/resource_requester_spec.rb +70 -0
  90. data/spec/flydata/source_postgresql/table_meta_spec.rb +77 -0
  91. metadata +49 -6
  92. data/lib/flydata/source_mysql/plugin_support/binlog_position_file.rb +0 -23
  93. data/lib/flydata/source_postgresql/pg_client.rb +0 -43
@@ -1,4 +1,5 @@
1
1
  require 'json'
2
+ require 'flydata-core/postgresql/snapshot'
2
3
 
3
4
  module FlydataCore
4
5
  module Postgresql
@@ -6,27 +7,87 @@ module Postgresql
6
7
  class SourcePos
7
8
  include Comparable
8
9
 
9
- def initialize(snapshot_id, pk_values = nil)
10
- @snapshot_id = snapshot_id
11
- @pk_values = pk_values
10
+ # Source Position data for PostgreSQL
11
+ #
12
+ # Format:
13
+ # <from-snaphost><TAB><to-snapshot><TAB><pk-values(JSON)>
14
+ #
15
+ # from-snapshot: Store txid_snapshot guaranteeing the changes up to this snapshot are sent
16
+ # pk_values: Store primary key values of the last record on the last request/transaction
17
+ #
18
+ #
19
+ # ex1: No resume position
20
+ #
21
+ # 1031:1031:<TAB>
22
+ #
23
+ # -> Changes up to 1031:1031: were sent.
24
+ # Agent will fetch the change between the latest snapshot and 1031:1031:
25
+ #
26
+ # ex2: Resume position with single primary key
27
+ #
28
+ # 1031:1031:<TAB>1032:1032:<TAB>[{"id":10000}]
29
+ #
30
+ # -> Changes up to 1031:1031: and partial changes (id <= 10000) between 1032:1032: and 1031:1031: were sent.
31
+ # Agent will fetch the change between 1032:1032: and 1031:1031: with a primary key condisions(id > 10000)
32
+ #
33
+ # ex3: Resume position with multiple primary keys
34
+ #
35
+ # 1031:1031:<TAB>1033:1033:<TAB>[{"group":"apple"},{"name":"TOM"}]
36
+ #
37
+ # -> Changes up to 1031:1031: and partial changes (group<='apple' and name<='TOM') between 1033:1033: and 1031:1031: were sent.
38
+ # Agent will fetch the change between 1033:1033: and 1031:1031: with a primary key condisions(group >= 'apple' and name >= 'TOM')
39
+ #
40
+ def initialize(snapshot_or_obj, to_snapshot = nil, pk_values = nil)
41
+ if snapshot_or_obj.kind_of?(self.class)
42
+ snapshot_or_obj.tap do |s|
43
+ @snapshot = s.snapshot
44
+ @to_snapshot = s.to_snapshot
45
+ @pk_values = s.pk_values
46
+ end
47
+ else
48
+ @snapshot = Snapshot.new(snapshot_or_obj)
49
+ @to_snapshot = if to_snapshot.to_s.empty?
50
+ nil
51
+ else
52
+ Snapshot.new(to_snapshot)
53
+ end
54
+ @pk_values = pk_values # must be array or nil
55
+ end
12
56
  end
13
57
 
14
- attr_reader :snapshot_id
58
+ attr_reader :snapshot
59
+ attr_reader :to_snapshot
60
+ attr_reader :pk_values
15
61
 
16
62
  def to_s
17
63
  pk_values = @pk_values ? @pk_values.to_json : ''
18
- "#{@snapshot_id}\t#{pk_values}"
64
+ "#{@snapshot}\t#{@to_snapshot}\t#{pk_values}"
19
65
  end
20
66
 
67
+ #TODO: Need to revisit the logic along with the spec
68
+ # http://www.postgresql.org/docs/9.5/static/functions-info.html#FUNCTIONS-TXID-SNAPSHOT-PARTS
21
69
  def <=>(other)
22
- # TODO take pk_values into account
23
- @snapshot_id <=> other.snapshot_id
70
+ if @snapshot != other.snapshot
71
+ return @snapshot <=> other.snapshot
72
+ elsif @pk_values.nil? && !other.pk_values.nil?
73
+ 1
74
+ elsif !@pk_values.nil? && other.pk_values.nil?
75
+ -1
76
+ elsif @pk_values == other.pk_values
77
+ 0
78
+ else
79
+ @pk_values.to_s <=> other.pk_values.to_s
80
+ end
24
81
  end
25
82
 
26
83
  def self.load(str)
27
- snapshot_id, pk_valules = str.split("\t")
28
- pk_values = JSON.parse(pk_values) if pk_values
29
- self.new(snapshot_id, pk_values)
84
+ snapshot, to_snapshot, pk_values = str.split("\t").collect{|v| v.strip}
85
+ pk_values = if pk_values.to_s.empty?
86
+ nil
87
+ else
88
+ JSON.parse(pk_values)
89
+ end
90
+ self.new(snapshot, to_snapshot, pk_values)
30
91
  end
31
92
  end
32
93
 
@@ -21,7 +21,7 @@ class MysqlTableDef < Base
21
21
 
22
22
  PROC_override_varbinary = ->(type, mysql_type, flydata_type) do
23
23
  return type unless %w(binary varbinary).include?(mysql_type)
24
- if type =~ /\((\s*\d+\s*)\)/ ###DEBUG/\((\d+)\)/
24
+ if type =~ /\((\s*\d+\s*)\)/
25
25
  # expect 2 bytes for each original byte + 2 bytes for the prefix
26
26
  # ex) 4E5DFF => "0x4e5dff"
27
27
  "#{flydata_type}(#{$1.to_i * 2 + 2})"
@@ -9,39 +9,82 @@ class PostgresqlTableDef < Base
9
9
 
10
10
  TYPE_MAP_P2F = {
11
11
  'bigint' => {type: 'int8'},
12
- 'bigserial' => {type: 'int8'},
13
- 'boolean' => {type: 'int1', def_width: '1'}, # TODO introduce bool type
14
- 'bytea' => {type: 'varbinary'}, # TODO need value conversion
15
- 'character' => {type: 'varchar', def_width: '1'},
12
+ 'int8' => {type: 'int8'},
13
+ 'bigserial' => {type: 'serial8'}, # TODO support
14
+ 'serial8' => {type: 'serial8'}, # TODO support
15
+ 'bit' => {type: 'bit',
16
+ width_attrs:["character_maximum_length"]},
17
+ 'bit varying' => {type: 'varbit',
18
+ width_attrs:["character_maximum_length"]},
19
+ 'varbit' => {type: 'varbit',
20
+ width_attrs:["character_maximum_length"]},
21
+ 'boolean' => {type: 'boolean'},
22
+ # 'box'
23
+ 'bytea' => {type: 'bytea'},
24
+ 'character' => {type: 'varchar', width_attrs:["character_octet_length"], def_width:[1]},
16
25
  'character varying' => {type: 'varchar',
17
26
  width_attrs:["character_octet_length"],
18
27
  def_width:[1]},
28
+ 'varchar' => {type: 'varchar',
29
+ width_attrs:["character_octet_length"],
30
+ def_width:[1]},
31
+ # 'cidr'
32
+ # 'circle'
19
33
  'date' => {type: 'date'},
20
- 'decimal' => {type: 'numeric',
21
- width_attrs:["numeric_precision", "numeric_scale"],
22
- def_width:[nil, 0] },
23
34
  'double precision' => {type: 'float8'},
35
+ 'float8' => {type: 'float8'},
24
36
  'integer' => {type: 'int4'},
25
- 'money' => {type: 'money'},
37
+ 'int' => {type: 'int4'},
38
+ 'int4' => {type: 'int4'},
39
+ # 'interval'
40
+ # 'json'
41
+ # 'line'
42
+ # 'lseg'
43
+ # 'macaddr'
44
+ 'money' => {type: 'money',
45
+ width_attrs:["numeric_precision", "numeric_scale"]},
26
46
  'numeric' => {type: 'numeric',
27
- width_attrs:["numeric_precision", "numeric_scale"],
28
- def_width:[nil, 0],
47
+ width_attrs:["numeric_precision", "numeric_scale"]
48
+ #can be no width values
29
49
  },
50
+ # 'path'
51
+ # 'pg_lsn'
52
+ # 'point'
53
+ # 'polygon'
30
54
  'real' => {type: 'float4'},
31
- 'serial' => {type: 'int4'},
55
+ 'float4' => {type: 'float4'},
32
56
  'smallint' => {type: 'int2'},
57
+ 'int2' => {type: 'int2'},
58
+ 'smallserial' => {type: 'serial2'}, # TODO support
59
+ 'serial2' => {type: 'serial2'}, # TODO support
60
+ 'serial' => {type: 'serial4'}, # TODO support
61
+ 'serial4' => {type: 'serial4'}, # TODO support
33
62
  'text' => {type: 'text'},
34
- 'time with time zone' => {type: 'time'},
63
+ 'time' => {type: 'time'},
35
64
  'time without time zone' => {type: 'time'},
36
- 'timestamp with time zone' => {type: 'datetime'},
65
+ 'time with time zone' => {type: 'timetz'},
66
+ 'timetz' => {type: 'timetz'},
67
+ 'timestamp' => {type: 'datetime'},
37
68
  'timestamp without time zone' => {type: 'datetime'},
38
- # TODO add more types
69
+ 'timestamp with time zone' => {type: 'datetimetz'},
70
+ 'timestamptz' => {type: 'datetimetz'},
71
+ :default => {type: '_unsupported'},
72
+ # 'tsquery'
73
+ # 'tsvector'
74
+ # 'txid_snapshot'
75
+ # 'uuid'
76
+ # 'xml'
77
+ #--- PosgreSQL automatically converts following data types to another data type ---
78
+ # 'decimal' (converted to 'numeric')
79
+ # 'bool' ('boolean')
80
+ # 'char' ('character')
39
81
  }
40
82
 
41
83
  def self.convert_to_flydata_type(information_schema_columns)
42
84
  pg_type = information_schema_columns["data_type"]
85
+ raise "Unknown PostgreSQL type or internal error. type:#{pg_type}" unless pg_type
43
86
  unless TYPE_MAP_P2F.has_key?(pg_type)
44
- raise "Unknown PostgreSQL type or internal error. type:#{pg_type}"
87
+ pg_type = :default
45
88
  end
46
89
  type_hash = TYPE_MAP_P2F[pg_type]
47
90
  flydata_type = type_hash[:type]
@@ -73,7 +116,7 @@ class PostgresqlTableDef < Base
73
116
 
74
117
  if type_hash.has_key?(:def_width)
75
118
  if values.nil? || values.size != type_hash[:def_width].size
76
- raise "The number of the default values must match the number of width attributes def_width:#{type_hash[:def_width].inspect} width_attrs:#{type_hash[:width_attrs].inspect}"
119
+ raise "The number of the default values must match the number of width attributes. column:#{information_schema_columns[:column_name]} type:#{type_hash[:type]} def_width:#{type_hash[:def_width].inspect} width_attrs:#{type_hash[:width_attrs].inspect}"
77
120
  end
78
121
  values = values.each_with_index.collect {|v, i| v ? v : type_hash[:def_width][i]}
79
122
  end
@@ -118,9 +161,25 @@ class PostgresqlTableDef < Base
118
161
  column[:type] = convert_to_flydata_type(information_schema_column)
119
162
  column[:not_null] = true if information_schema_column["is_nullable"] == "NO"
120
163
  column[:primary_key] = true if information_schema_column["is_primary"]
121
- column[:default] = information_schema_column["column_default"] # TODO nill handling
164
+ column[:default] = case column[:type]
165
+ when 'boolean'
166
+ to_boolean(information_schema_column["column_default"])
167
+ else
168
+ information_schema_column["column_default"] # TODO nil handling
169
+ end
122
170
  column
123
171
  end
172
+
173
+ def self.to_boolean(col_value)
174
+ return nil if col_value.nil?
175
+ # Catch all possible literal for boolean type in PostgreSQL.
176
+ # Actual col_value coming in here is:
177
+ # 'true' or 'false' for default value
178
+ # 't' or 'f' for column value
179
+ return true if col_value.to_s =~ /^(t|true|y|yes|on|1)$/i
180
+ return false if col_value.to_s =~ /^(f|false|n|no|off|0)$/i
181
+ raise "Invalid default value for PostgreSQL boolean type:`#{col_value}`"
182
+ end
124
183
  end
125
184
 
126
185
  end
@@ -10,9 +10,13 @@ class RedshiftTableDef
10
10
  TYPE_MAP_F2R = {
11
11
  'binary' => {type: 'varchar', use_params: true, default_value: ''},
12
12
  'bit' => {type: 'bigint', default_value: '0'},
13
+ 'boolean' => {type: 'boolean', default_value: false},
14
+ 'varbit' => {type: 'bigint', default_value: '0'},
15
+ 'bytea' => {type: 'varchar(max)', default_value: ''},
13
16
  'char' => {type: 'char', use_params: true, default_value: ''},
14
17
  'date' => {type: 'date', default_value: '0000-01-01'},
15
18
  'datetime' => {type: 'timestamp', default_value: '0000-01-01'},
19
+ 'datetimetz' => {type: 'timestamp', default_value: '0000-01-01'},
16
20
  'enum' => {type: 'varchar encode bytedict', default_value: ''},
17
21
  'float4' => {type: 'float4', default_value: '0'},
18
22
  'float4 unsigned' => {type: 'float4', default_value: '0'},
@@ -28,14 +32,17 @@ class RedshiftTableDef
28
32
  'int4 unsigned' => {type: 'int8', unsigned: true, default_value: '0'},
29
33
  'int8' => {type: 'int8', default_value: '0'},
30
34
  'int8 unsigned' => {type: 'numeric(20,0)', unsigned: true, default_value: '0'},
31
- 'numeric' => {type: 'numeric', use_params: true, max_size: [38,37], default_value: '0'},
32
- 'numeric unsigned' => {type: 'numeric', use_params: true, max_size: [38,37], default_value: '0'},
35
+ 'money' => {type: 'numeric', use_params: true, max_size: [38,37], default_value: '0'},
36
+ 'numeric' => {type: 'numeric', use_params: true, default_params: [18,8], max_size: [38,37], default_value: '0'},
37
+ 'numeric unsigned' => {type: 'numeric', use_params: true, default_params: [18,8], max_size: [38,37], default_value: '0'},
33
38
  'set' => {type: 'varchar encode bytedict', default_value: ''},
34
39
  'text' => {type: 'varchar(max)', default_value: ''},
35
40
  'time' => {type: 'timestamp', default_value: '0000-01-01'},
41
+ 'timetz' => {type: 'timestamp', default_value: '0000-01-01'},
36
42
  'varbinary' => {type: 'varchar', use_params: true, max_size: 65535, default_value: ''},
37
43
  'varchar' => {type: 'varchar', use_params: true, max_size: 65535, default_value: ''},
38
44
  'year' => {type: 'date', default_value: '0001-01-01'},
45
+ '_unsupported' => {type: 'varchar(max)', default_value: ''},
39
46
  }
40
47
  def self.from_flydata_tabledef(flydata_tabledef, options = {})
41
48
  options[:flydata_ctl_table] = true unless options.has_key?(:flydata_ctl_table)
@@ -209,8 +216,14 @@ EOS
209
216
  type_info = TYPE_MAP_F2R[type]
210
217
  raise "Unsupported type '#{column[:type]}'" if type_info.nil?
211
218
 
212
- rs_type = if type_info[:use_params] && params && !params.nil?
213
- params = check_and_replace_max(params, Array(type_info[:max_size])) if type_info[:max_size]
219
+ rs_type = if type_info[:use_params]
220
+ if params
221
+ params = check_and_replace_max(params, Array(type_info[:max_size])) if type_info[:max_size]
222
+ else
223
+ #source data type has no parameter. use default parameters.
224
+ raise "No default pramameters for type:`#{column[:type]}`" unless type_info[:default_params]
225
+ params = type_info[:default_params].join(',')
226
+ end
214
227
  type_info[:type] + "(#{params})"
215
228
  else
216
229
  type_info[:type]
@@ -246,6 +259,11 @@ EOS
246
259
 
247
260
  def self.replace_default_value(flydata_type, redshift_type, default_value)
248
261
  return NULL_STR if default_value.nil?
262
+ # strip type cast
263
+ if default_value.kind_of?(String) &&
264
+ /^(.+?)(::"?[a-z ]+"?)*$/.match(default_value)
265
+ default_value = $1
266
+ end
249
267
  if flydata_type.start_with?('year')
250
268
  value = convert_year_into_date(remove_single_quote(default_value))
251
269
  begin
@@ -253,31 +271,36 @@ EOS
253
271
  rescue
254
272
  raise "default value of YEAR type must be 2 or 4-digit, value:'#{default_value}'"
255
273
  end
274
+ elsif flydata_type.start_with?('money')
275
+ default_value = self.parse_money(remove_single_quote(default_value))
256
276
  end
257
277
 
258
278
  case redshift_type
259
279
  when 'timestamp'
260
- if /^CURRENT_TIMESTAMP/ === default_value.upcase
280
+ case default_value.upcase
281
+ when /^CURRENT_TIMESTAMP\b/, /^CURRENT_TIME\b/, /^NOW\(\)/
261
282
  'SYSDATE'
262
283
  else
263
284
  "'#{self.parse_timestamp(remove_single_quote(default_value))}'"
264
285
  end
265
286
  when 'date'
266
287
  "'#{self.parse_date(remove_single_quote(default_value))}'"
288
+ when 'boolean'
289
+ default_value.to_s.upcase
267
290
  else
268
291
  if !default_value.kind_of?(String)
269
292
  "'#{default_value}'"
270
- elsif /^b'.+'$/.match(default_value)
293
+ elsif /^[Bb]'.+'$/.match(default_value)
271
294
  "0b#{default_value[2..-2]}".oct
272
295
  elsif /^[xX]'.+'$/.match(default_value)
273
296
  "0x#{default_value[2..-2]}".oct
274
297
  elsif /^0[bx].+/.match(default_value)
275
298
  default_value.oct
276
- elsif /^'.*'$/.match(default_value)
277
- default_value
278
299
  elsif /\s*nextval\(/.match(default_value)
279
300
  # Redshift does not support nextval() function. Set no default.
280
301
  nil
302
+ elsif /^'.+'/.match(default_value)
303
+ default_value
281
304
  else
282
305
  "'#{default_value}'"
283
306
  end
@@ -400,7 +423,7 @@ EOS
400
423
  end
401
424
 
402
425
  APACHE_TIMESTAMP_REGEXP = Regexp.new('^(?<apache_time_format>\[[0-3]\d\/\D{3}\/[1-2]\d{3}:[0-2]\d:[0-5]\d:[0-5]\d ?[\+\-]\d{2}:?\d{2}\])$')
403
- TIME_REGEXP = Regexp.new('^(?<sign>-)?(?<hour>\d{2,3}):(?<minute>[0-5][0-9]):(?<second>[0-5][0-9](\.\d+)?)$')
426
+ TIME_REGEXP = Regexp.new('^(?<sign>-)?(?<hour>\d{2,3}):(?<minute>[0-5][0-9]):(?<second>[0-5][0-9](\.\d+)?)\s*(?<tz>[+-][0-2][0-9](:?\d{2})?)?$')
404
427
 
405
428
  def self.parse_timestamp(value)
406
429
  value_str = value.to_s
@@ -412,7 +435,13 @@ EOS
412
435
  # apache time format
413
436
  t = DateTime.strptime(value, "[%d/%b/%Y:%H:%M:%S %Z]")
414
437
  elsif time_match = TIME_REGEXP.match(value_str)
415
- t = convert_time_into_timestamp(time_match)
438
+ if time_match[:tz]
439
+ # let DateTime.parse handle timezone
440
+ t = DateTime.parse("0001-01-01 #{value_str}")
441
+ else
442
+ # use own converter which takes care of negative hours
443
+ t = convert_time_into_timestamp(time_match)
444
+ end
416
445
  elsif /^(\d+)\.(\d+)$/ === value_str # epoch with fraction
417
446
  epoch = $1
418
447
  fraction = $2
@@ -477,6 +506,26 @@ EOS
477
506
  value # Return the value as is
478
507
  end
479
508
  end
509
+
510
+ def self.parse_money(value)
511
+ # It's impossible to determine a decimal point of a currency sting without
512
+ # knowing its format. Here, we're making a best effort guess to support
513
+ # as many currencty formats as we can without knowing the format.
514
+ value = value.gsub(/[^0-9\.,]/, '') # remove all chars but numbers and possible decimal point chars
515
+
516
+ whole_part = value
517
+ fractional_part = nil
518
+ if idx = value.rindex(/[\.,]/)
519
+ if idx == value.size - 3
520
+ # it's the decimal point with two digit fraction
521
+ whole_part = value[0...idx]
522
+ fractional_part = value[-2..-1]
523
+ end
524
+ end
525
+ value = whole_part.gsub(/[^0-9]/, '')
526
+ value += ".#{fractional_part}" if fractional_part
527
+ value
528
+ end
480
529
  end
481
530
 
482
531
  end
@@ -8,7 +8,7 @@ module Mysql
8
8
  let(:binlog_str) { "#{filename}\t#{pos}" }
9
9
  let(:filename) { 'mysql-bin.000064' }
10
10
  let(:pos) { 104 }
11
-
11
+
12
12
  describe '#pos' do
13
13
  subject { subject_object.pos }
14
14
  it { is_expected.to eq(pos) }
@@ -35,7 +35,7 @@ module Mysql
35
35
  let(:args) { [filename, pos] }
36
36
  it_behaves_like "properly constructed"
37
37
  end
38
-
38
+
39
39
  let(:args) { [ arg1 ] }
40
40
  context 'when given a String' do
41
41
  let(:arg1) { binlog_str }
@@ -48,6 +48,14 @@ module Mysql
48
48
  end
49
49
  end
50
50
 
51
+ describe '.load' do
52
+ subject { described_class.load(binlog_str) }
53
+ it do
54
+ expect(subject.filename).to eq(filename)
55
+ expect(subject.pos).to eq(pos)
56
+ end
57
+ end
58
+
51
59
  let(:arg) { described_class.new (arg_str) }
52
60
  let(:arg_str) { arg_binlog_str }
53
61
  let(:arg_binlog_str) { "#{arg_filename}\t#{arg_pos}" }
@@ -0,0 +1,148 @@
1
+ require 'spec_helper'
2
+ require 'flydata-core/postgresql/compatibility_checker'
3
+
4
+ module FlydataCore
5
+ module Postgresql
6
+ describe CompatibilityChecker do
7
+ let(:database) { 'test_db' }
8
+ let(:tables) { %w(table_1 table_2 table_3) }
9
+ let(:option) do
10
+ { database: database, tables: tables }
11
+ end
12
+
13
+ describe TableExistenceChecker do
14
+ let(:subject_object) { described_class.new(option) }
15
+
16
+ describe '#create_query' do
17
+ subject { subject_object.create_query }
18
+
19
+ context 'when schema is not set' do
20
+ it { is_expected.to eq <<EOT
21
+ SELECT
22
+ table_name
23
+ FROM
24
+ information_schema.tables
25
+ WHERE
26
+ table_schema in (select current_schema)
27
+ AND
28
+ table_name in ('table_1','table_2','table_3');
29
+ EOT
30
+ }
31
+ end
32
+ context 'when schema is set' do
33
+ before { option.merge!(schema: 'test_schema') }
34
+ it { is_expected.to eq <<EOT
35
+ SELECT
36
+ table_name
37
+ FROM
38
+ information_schema.tables
39
+ WHERE
40
+ table_schema in ('test_schema')
41
+ AND
42
+ table_name in ('table_1','table_2','table_3');
43
+ EOT
44
+ }
45
+ end
46
+
47
+ end
48
+
49
+ describe '#check_reesult' do
50
+ let(:result) { [] }
51
+ subject { subject_object.check_result(result) }
52
+
53
+ context 'when all tables exist' do
54
+ let(:result) do
55
+ [ {'table_name' => 'table_1'},
56
+ {'table_name' => 'table_2'},
57
+ {'table_name' => 'table_3'}, ]
58
+ end
59
+ it { expect{subject}.not_to raise_error }
60
+ end
61
+
62
+ context 'when some tables does not exist' do
63
+ let(:result) do
64
+ [{"table_name" => "table_2"}]
65
+ end
66
+ it do
67
+ expect{subject}.to raise_error {|e|
68
+ expect(e).to be_kind_of(FlydataCore::PostgresqlCompatibilityError)
69
+ expect(e.to_s.include?("table_1")).to be_truthy
70
+ expect(e.to_s.include?("table_2")).to be_falsey
71
+ expect(e.to_s.include?("table_3")).to be_truthy
72
+ }
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ describe PrimaryKeyChecker do
79
+ let(:subject_object) { described_class.new(option) }
80
+
81
+ describe '#create_query' do
82
+ subject { subject_object.create_query }
83
+
84
+ context 'when schema is not set' do
85
+ it { is_expected.to eq <<EOT
86
+ SELECT
87
+ t.table_name
88
+ FROM
89
+ (select * from information_schema.tables where table_schema in (select current_schema) AND table_name in ('table_1','table_2','table_3')) t
90
+ LEFT OUTER JOIN
91
+ (select * from information_schema.table_constraints where table_schema in (select current_schema) AND table_name in ('table_1','table_2','table_3')) tc
92
+ USING (table_schema, table_name)
93
+ GROUP BY
94
+ t.table_schema, t.table_name
95
+ HAVING
96
+ SUM(CASE WHEN tc.constraint_type='PRIMARY KEY' THEN 1 ELSE 0 END) = 0;
97
+ EOT
98
+ }
99
+ end
100
+
101
+ context 'when schema is set' do
102
+ before { option.merge!(schema: 'test_schema') }
103
+ it { is_expected.to eq <<EOT
104
+ SELECT
105
+ t.table_name
106
+ FROM
107
+ (select * from information_schema.tables where table_schema in ('test_schema') AND table_name in ('table_1','table_2','table_3')) t
108
+ LEFT OUTER JOIN
109
+ (select * from information_schema.table_constraints where table_schema in ('test_schema') AND table_name in ('table_1','table_2','table_3')) tc
110
+ USING (table_schema, table_name)
111
+ GROUP BY
112
+ t.table_schema, t.table_name
113
+ HAVING
114
+ SUM(CASE WHEN tc.constraint_type='PRIMARY KEY' THEN 1 ELSE 0 END) = 0;
115
+ EOT
116
+ }
117
+ end
118
+ end
119
+
120
+ describe '#check_reesult' do
121
+ let(:result) { [] }
122
+ subject { subject_object.check_result(result) }
123
+
124
+ context 'when all tables have pk' do
125
+ let(:result) do
126
+ []
127
+ end
128
+ it { expect{subject}.not_to raise_error }
129
+ end
130
+
131
+ context 'when some tables does not have pk' do
132
+ let(:result) do
133
+ [{"table_name" => "table_1"}, {"table_name" => "table_3"}]
134
+ end
135
+ it do
136
+ expect{subject}.to raise_error {|e|
137
+ expect(e).to be_kind_of(FlydataCore::PostgresqlCompatibilityError)
138
+ expect(e.to_s.include?("table_1")).to be_truthy
139
+ expect(e.to_s.include?("table_2")).to be_falsey
140
+ expect(e.to_s.include?("table_3")).to be_truthy
141
+ }
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end