flydata 0.6.14 → 0.7.0

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