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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/flydata-core/Gemfile +1 -0
- data/flydata-core/Gemfile.lock +5 -0
- data/flydata-core/lib/flydata-core/errors.rb +4 -2
- data/flydata-core/lib/flydata-core/mysql/binlog_pos.rb +4 -0
- data/flydata-core/lib/flydata-core/postgresql/compatibility_checker.rb +119 -0
- data/flydata-core/lib/flydata-core/postgresql/config.rb +58 -0
- data/flydata-core/lib/flydata-core/postgresql/pg_client.rb +170 -0
- data/flydata-core/lib/flydata-core/postgresql/snapshot.rb +49 -0
- data/flydata-core/lib/flydata-core/postgresql/source_pos.rb +71 -10
- data/flydata-core/lib/flydata-core/table_def/mysql_table_def.rb +1 -1
- data/flydata-core/lib/flydata-core/table_def/postgresql_table_def.rb +76 -17
- data/flydata-core/lib/flydata-core/table_def/redshift_table_def.rb +59 -10
- data/flydata-core/spec/mysql/binlog_pos_spec.rb +10 -2
- data/flydata-core/spec/postgresql/compatibility_checker_spec.rb +148 -0
- data/flydata-core/spec/postgresql/config_spec.rb +85 -0
- data/flydata-core/spec/postgresql/pg_client_spec.rb +195 -0
- data/flydata-core/spec/postgresql/snapshot_spec.rb +55 -0
- data/flydata-core/spec/postgresql/source_pos_spec.rb +70 -8
- data/flydata-core/spec/table_def/postgresql_table_def_spec.rb +80 -19
- data/flydata-core/spec/table_def/redshift_table_def_spec.rb +211 -14
- data/flydata.gemspec +0 -0
- data/lib/flydata.rb +1 -0
- data/lib/flydata/command/sender.rb +10 -7
- data/lib/flydata/command/sync.rb +4 -1
- data/lib/flydata/fluent-plugins/flydata_plugin_ext/base.rb +1 -0
- data/lib/flydata/fluent-plugins/flydata_plugin_ext/fluent_log_ext.rb +73 -0
- data/lib/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync.rb +35 -10
- data/lib/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_diff_based.rb +29 -0
- data/lib/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_query_based.rb +26 -0
- data/lib/flydata/fluent-plugins/flydata_plugin_ext/preference.rb +29 -13
- data/lib/flydata/fluent-plugins/in_mysql_binlog_flydata.rb +10 -18
- data/lib/flydata/fluent-plugins/in_postgresql_query_based_flydata.rb +64 -0
- data/lib/flydata/helpers.rb +1 -3
- data/lib/flydata/plugin_support/context.rb +14 -2
- data/lib/flydata/plugin_support/source_position_file.rb +35 -0
- data/lib/flydata/plugin_support/sync_record_emittable.rb +2 -1
- data/lib/flydata/query_based_sync/client.rb +101 -0
- data/lib/flydata/query_based_sync/record_size_estimator.rb +39 -0
- data/lib/flydata/query_based_sync/resource_requester.rb +70 -0
- data/lib/flydata/query_based_sync/response.rb +122 -0
- data/lib/flydata/query_based_sync/response_handler.rb +30 -0
- data/lib/flydata/source/sync_generate_table_ddl.rb +1 -1
- data/lib/flydata/source_mysql/plugin_support/binlog_record_dispatcher.rb +2 -2
- data/lib/flydata/source_mysql/plugin_support/binlog_record_handler.rb +3 -9
- data/lib/flydata/source_mysql/plugin_support/context.rb +26 -2
- data/lib/flydata/source_mysql/plugin_support/source_position_file.rb +14 -0
- data/lib/flydata/source_mysql/table_ddl.rb +3 -3
- data/lib/flydata/source_mysql/{plugin_support/table_meta.rb → table_meta.rb} +3 -10
- data/lib/flydata/source_postgresql/generate_source_dump.rb +44 -63
- data/lib/flydata/source_postgresql/parse_dump_and_send.rb +2 -0
- data/lib/flydata/source_postgresql/plugin_support/context.rb +13 -0
- data/lib/flydata/source_postgresql/plugin_support/source_position_file.rb +14 -0
- data/lib/flydata/source_postgresql/query_based_sync/client.rb +16 -0
- data/lib/flydata/source_postgresql/query_based_sync/diff_query_generator.rb +135 -0
- data/lib/flydata/source_postgresql/query_based_sync/resource_requester.rb +86 -0
- data/lib/flydata/source_postgresql/query_based_sync/response.rb +12 -0
- data/lib/flydata/source_postgresql/query_based_sync/response_handler.rb +12 -0
- data/lib/flydata/source_postgresql/sync_generate_table_ddl.rb +25 -79
- data/lib/flydata/source_postgresql/table_meta.rb +168 -0
- data/lib/flydata/sync_file_manager.rb +5 -5
- data/lib/flydata/table_meta.rb +19 -0
- data/spec/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_context.rb +85 -0
- data/spec/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_diff_based_shared_examples.rb +36 -0
- data/spec/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_query_based_shared_examples.rb +37 -0
- data/spec/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync_shared_examples.rb +67 -0
- data/spec/flydata/fluent-plugins/in_mysql_binlog_flydata_spec.rb +119 -96
- data/spec/flydata/fluent-plugins/in_postgresql_query_based_flydata_spec.rb +82 -0
- data/spec/flydata/fluent-plugins/sync_source_plugin_context.rb +29 -0
- data/spec/flydata/plugin_support/context_spec.rb +37 -3
- data/spec/flydata/query_based_sync/client_spec.rb +79 -0
- data/spec/flydata/query_based_sync/query_based_sync_context.rb +116 -0
- data/spec/flydata/query_based_sync/record_size_estimator_spec.rb +54 -0
- data/spec/flydata/query_based_sync/resource_requester_spec.rb +58 -0
- data/spec/flydata/query_based_sync/response_handler_spec.rb +36 -0
- data/spec/flydata/query_based_sync/response_spec.rb +157 -0
- data/spec/flydata/source_mysql/plugin_support/context_spec.rb +7 -1
- data/spec/flydata/source_mysql/plugin_support/dml_record_handler_spec.rb +2 -15
- data/spec/flydata/source_mysql/plugin_support/drop_database_query_handler_spec.rb +1 -1
- data/spec/flydata/source_mysql/plugin_support/shared_query_handler_context.rb +12 -11
- data/spec/flydata/source_mysql/plugin_support/source_position_file_spec.rb +53 -0
- data/spec/flydata/source_mysql/plugin_support/truncate_query_handler_spec.rb +1 -1
- data/spec/flydata/source_mysql/table_ddl_spec.rb +5 -5
- data/spec/flydata/source_mysql/{plugin_support/table_meta_spec.rb → table_meta_spec.rb} +6 -7
- data/spec/flydata/source_postgresql/generate_source_dump_spec.rb +165 -77
- data/spec/flydata/source_postgresql/query_based_sync/diff_query_generator_spec.rb +213 -0
- data/spec/flydata/source_postgresql/query_based_sync/query_based_sync_postgresql_context.rb +76 -0
- data/spec/flydata/source_postgresql/query_based_sync/resource_requester_spec.rb +70 -0
- data/spec/flydata/source_postgresql/table_meta_spec.rb +77 -0
- metadata +49 -6
- data/lib/flydata/source_mysql/plugin_support/binlog_position_file.rb +0 -23
- 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 :
|
|
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
|
-
"#{@
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
pk_values =
|
|
29
|
-
|
|
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*)\)/
|
|
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
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
'
|
|
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
|
|
63
|
+
'time' => {type: 'time'},
|
|
35
64
|
'time without time zone' => {type: 'time'},
|
|
36
|
-
'
|
|
65
|
+
'time with time zone' => {type: 'timetz'},
|
|
66
|
+
'timetz' => {type: 'timetz'},
|
|
67
|
+
'timestamp' => {type: 'datetime'},
|
|
37
68
|
'timestamp without time zone' => {type: 'datetime'},
|
|
38
|
-
|
|
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
|
-
|
|
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] =
|
|
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
|
-
'
|
|
32
|
-
'numeric
|
|
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]
|
|
213
|
-
|
|
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
|
-
|
|
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 /^
|
|
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
|
-
|
|
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
|