require 'spec_helper' require 'flydata-core/table_def' require 'timecop' module FlydataCore module TableDef describe RedshiftTableDef do let(:subject_object) { described_class } let(:table_name) { 'test_table' } describe '.from_flydata_tabledef' do subject { subject_object.from_flydata_tabledef(flydata_tabledef, option) } let(:value_column_hash) { { column: "value", type: "text" } } let(:value_column) { value_column_hash } let(:value_type_body) { 'text' } let(:value_type) { value_type_body } let(:flydata_tabledef) { { table_name: "test_table", columns: [ { column: "id", type: "int4(11)", not_null: true, primary_key: true }, { column: "age", type: "int4(11) unsigned" }, value_column, ], default_charset: "UTF_8", src_ddl: "dummy_src_ddl"} } let(:option) { { flydata_ctl_table: false } } let(:schema_prefix) { "" } let(:drop_table_subquery) { < 5+33 digits (precision 38) end # Preserve digits over decimal as much as possible, # and truncate scale. let(:type_sql) { %Q|"value" numeric(38,33)| } it_behaves_like *examples end context 'when both precision and scale exceed max allowed' do #possible in postgresql let(:default_value) { "'4'" } let(:default_value_sql) { default_value } let(:not_null_default_sql) { " DEFAULT '0'" } before do column[:column] = "value" column[:type] = "numeric(1000,44)" #956+44 => 38+0 digits end let(:type_sql) { %Q|"value" numeric(38,0)| } it_behaves_like *examples end context 'when only scale exceed max allowed' do let(:default_value) { "'4'" } let(:default_value_sql) { default_value } let(:not_null_default_sql) { " DEFAULT '0'" } before do column[:column] = "value" column[:type] = "numeric(38,38)" #0+38 => 0+37 digits end let(:type_sql) { %Q|"value" numeric(37,37)| } it_behaves_like *examples end context 'when precision and scale are not given' do #possible in postgresql let(:default_value) { "'4'" } let(:default_value_sql) { default_value } let(:not_null_default_sql) { " DEFAULT '0'" } before do column[:column] = "value" column[:type] = "numeric" end let(:type_sql) { %Q|"value" numeric(18,8)| } it_behaves_like *examples end context 'when precision and scale exceeds max allowed (for unsigned)' do let(:default_value) { "'4'" } let(:default_value_sql) { default_value } let(:not_null_default_sql) { " DEFAULT '0'" } before do column[:column] = "value" column[:type] = "numeric(65,44) unsigned" end let(:type_sql) {%Q|"value" numeric(38,17)| } it_behaves_like *examples end context 'when precision and scale are within limit (for unsigned)' do let(:default_value) { "'4'" } let(:default_value_sql) { default_value } let(:not_null_default_sql) { " DEFAULT '0'" } before do column[:column] = "value" column[:type] = "numeric(37,29) unsigned" end let(:type_sql) { %Q|"value" numeric(37,29)| } it_behaves_like *examples end end context 'with bit column def' do before do column[:column] = "value_bit" column[:type] = "bit(1)" end let(:type_sql) { %Q|"value_bit" bigint| } let(:not_null_default_sql) { " DEFAULT '0'" } context 'when default_value is small bit' do let(:default_value) { "b'1'" } let(:default_value_sql) { "1" } it_behaves_like *examples end context 'when default_value is large bit' do let(:default_value) { "b'10110'" } let(:default_value_sql) { "22" } it_behaves_like *examples end context 'when default_value uses the capital B prefix' do let(:default_value) { "B'1'" } let(:default_value_sql) { "1" } it_behaves_like *examples end context 'when default_value has a type cast' do let(:default_value) { %Q|B'1'::"bit"| } let(:default_value_sql) { "1" } it_behaves_like *examples end # it's not real case context "when default_value is x'' format" do let(:default_value) { "x'12'" } let(:default_value_sql) { "18" } it_behaves_like *examples end context "when default_value is X'' format" do let(:default_value) { "X'2a'" } let(:default_value_sql) { "42" } it_behaves_like *examples end context 'when default_value is 0x format' do let(:default_value) { "0x0013" } let(:default_value_sql) { "19" } it_behaves_like *examples end context 'when default_value is 0b format' do let(:default_value) { "0b1011" } let(:default_value_sql) { "11" } it_behaves_like *examples end end context 'with timetz column def' do before do column[:column] = "value_timetz" column[:type] = "timetz" end let(:type_sql) { %Q|"value_timetz" timestamp| } let(:not_null_default_sql) { " DEFAULT '0000-01-01'" } context 'when default_value is normal' do let(:default_value) { "'22:22:22+00'::time with time zone" } let(:default_value_sql) { "'0001-01-01 22:22:22.000000'" } it_behaves_like *examples end context 'when default_value is current_time' do let(:default_value) { 'current_time' } let(:default_value_sql) { "SYSDATE" } it_behaves_like *examples end context 'when default_value is 00:00:00-07' do let(:default_value) { "'00:00:00-07'" } let(:default_value_sql) { "'0001-01-01 07:00:00.000000'" } it_behaves_like *examples end context 'when default_value is 00:00:00+07' do let(:default_value) { "'00:00:00+07'" } # Per ISO 8601, year 0000 is year 1 BC let(:default_value_sql) { "'0000-12-31 17:00:00.000000'" } it_behaves_like *examples end end context 'with year column def' do before do column[:column] = "value_year" column[:type] = "year(4)" end let(:type_sql) { %Q|"value_year" date| } let(:not_null_default_sql) { " DEFAULT '0001-01-01'" } context 'when default_value is normal' do let(:default_value) { "'2014'" } let(:default_value_sql) { "'2014-01-01'" } it_behaves_like *examples end context 'when default_value is 69' do let(:default_value) { "'69'" } let(:default_value_sql) { "'2069-01-01'" } it_behaves_like *examples end context 'when default_value is 70' do let(:default_value) { "'70'" } let(:default_value_sql) { "'1970-01-01'" } it_behaves_like *examples end context 'when default_value is 0' do let(:default_value) { "'0'" } let(:default_value_sql) { "'0001-01-01'" } it_behaves_like *examples end context 'when the type has no width' do before do column[:type] = "year" end context 'when default_value is normal' do let(:default_value) { "'2014'" } let(:default_value_sql) { "'2014-01-01'" } it_behaves_like *examples end end end context 'with money column def' do before do column[:column] = "value_money" column[:type] = "money(19,2)" end let(:type_sql) { %Q|"value_money" numeric(19,2)| } let(:not_null_default_sql) { " DEFAULT '0'" } context 'when default_value is normal' do let(:default_value) { "'10.40'" } let(:default_value_sql) { "'10.40'" } it_behaves_like *examples end context 'when default_value is in a monetary format' do let(:default_value) { "'$1,430.00'" } let(:default_value_sql) { "'1430.00'" } it_behaves_like *examples end context 'when default_value is in Japanese yen' do let(:default_value) { "'143,000 yen'" } let(:default_value_sql) { "'143000'" } it_behaves_like *examples end end context 'with unsupported column def' do before do column[:column] = "value_unsupported" column[:type] = "_unsupported" end let(:type_sql) { %Q|"value_unsupported" varchar(max)| } let(:not_null_default_sql) { " DEFAULT ''" } context 'when default_value is normal' do let(:default_value) { "'happy'" } let(:default_value_sql) { "'happy'" } it_behaves_like *examples end context 'when default value has a PostgreSQL-style type cast' do let(:default_value) { "'ok'::mood" } let(:default_value_sql) { "'ok'" } it_behaves_like *examples end end end context 'for create table' do let(:opt) { {} } let(:expected_query) { %Q| #{type_sql}#{not_null_sql}#{default_sql}| } let(:no_default_sql) { "" } it_behaves_like "test column types", "not null values", "default values", "test generated Redshift SQL for column" end context 'for alter table' do let(:opt) { { for: :alter_table } } context "without not null" do let(:expected_query) { %Q| #{type_sql}#{default_sql}| } let(:no_default_sql) { "" } it_behaves_like "test column types", "default values", "test generated Redshift SQL for column" end context "with not null false" do let(:expected_query) { %Q| #{type_sql}#{default_sql}| } before do column[:not_null] = false end let(:no_default_sql) { "" } it_behaves_like "test column types", "default values", "test generated Redshift SQL for column" end context "with not null true" do let(:expected_query) { %Q| #{type_sql}#{default_sql}| } # Redshift supports NOT NULL but we do not respect it in order to accept NULL values which may come in future. before do column[:not_null] = true end let(:no_default_sql) { "" } it_behaves_like "test column types", "default values", "test generated Redshift SQL for column" end end end describe '.flydata_ctl_columns_sql' do subject { subject_object.flydata_ctl_columns_sql(flydata_tabledef, schema_name) } let(:flydata_tabledef) { { table_name: table_name, columns: [ { column: column_name, type: "varchar(12)" }, ], default_charset: "UTF_8"} } let(:schema_name) { nil } let(:expected_sql) { <= 1970' do let(:value){ '2000' } it { is_expected.to eq('2000-01-01') } end context 'with year(4) < 1970' do let(:value){ '1969' } it { is_expected.to eq('1969-01-01') } end context 'with year(2) >= 70' do let(:value){ '07' } it { is_expected.to eq('2007-01-01') } end context 'with year(2) < 70' do let(:value){ '69' } it { is_expected.to eq('2069-01-01') } end context 'with value 259392-00-00' do let(:value) { '259392-00-00' } it { is_expected.to eq('259392-01-01') } end context 'with value 259392-00-04' do let(:value) { '259392-00-04' } it { is_expected.to eq('259392-01-04') } end context 'with value 0000-01-04' do let(:value) { '0000-01-04' } it { is_expected.to eq('0001-01-04') } end context 'with value 259392-40-04' do let(:value) { '259392-40-04' } it { expect{subject}.to raise_error(ArgumentError) } end end context 'with date values' do shared_examples "returning the input value as is" do it { is_expected.to eq(value) } end context 'with valid date' do let(:value){ '1920-01-01' } it_behaves_like "returning the input value as is" end context 'with zero date' do let(:value){ '0000-00-00' } it { is_expected.to eq('0001-01-01') } end end context 'with nil' do it { is_expected.to eq(nil) } end end shared_examples "expecting no underscore in front of numbers" do # Redshift accepts a table/column name starting with numbers (e.g. 20grams) or even a number-only name (e.g. 12345) context "with a key including numbers" do let(:key) { "year2000" } let(:expected_value) { key } it { is_expected.to eq expected_value } end context "with a key only including numbers" do let(:key) { "2000" } let(:expected_value) { key } it { is_expected.to eq expected_value } end context "with a key starting with numbers" do let(:key) { "2000stars" } let(:expected_value) { key } it { is_expected.to eq expected_value } end end describe ".convert_to_valid_name" do subject { subject_object.convert_to_valid_name(key) } it_behaves_like "expecting no underscore in front of numbers" context "with a key that has spaces" do let(:key) { "space test" } let(:expected_value) { key } it { is_expected.to eq expected_value } end context "with a key that has non-ASCII chars" do let(:key) { "行列123" } let(:expected_value) { "__123" } it "Redshift does not support non-ASCII table/column name. Those characters will be replaced with '_'" do is_expected.to eq expected_value end end context "with a key that has special chars" do let(:key) { %q| !"#$%&'()*+,-/:;<=>?@[\\]^_{\|}~`| } let(:expected_value) { %q| !_#$%&'()*+,-/:;<=>?@[\\]^_{\|}~`| } it "Redshift supports all these special characters but double quote (\") which is replaced with underscore (_)" do is_expected.to eq expected_value end end context "with a key that has a period in it" do let(:key) { %q|my.table| } let(:expected_value) { key } it "Redshift supports period (.) in a table/column name" do is_expected.to eq expected_value end end end describe ".convert_to_valid_column_name" do subject { subject_object.convert_to_valid_name(key) } it_behaves_like "expecting no underscore in front of numbers" end describe ".convert_to_valid_table_name" do subject { subject_object.convert_to_valid_name(key) } it_behaves_like "expecting no underscore in front of numbers" end describe ".fq_table_name" do subject { subject_object.fq_table_name(table_name, schema_name) } context "with no schema name" do let(:schema_name) { nil } context "with a table name with no schema prefix" do let(:table_name) { "test_table" } it { is_expected.to eq "test_table" } end context "with a table name with a schema prefix" do let(:table_name) { "some_schema.test_table" } it { is_expected.to eq "test_table" } end context "with a mixed-case table name" do let(:table_name) { "Test_Table" } it { is_expected.to eq "test_table" } end end context "with a schema name" do let(:schema_name) { "test_schema" } context "with a table name with no schema prefix" do let(:table_name) { "test_table" } it { is_expected.to eq "test_schema.test_table" } end context "with a table name with a schema prefix" do let(:table_name) { "some_schema.test_table" } it { is_expected.to eq "test_schema.test_table" } end end context "with a mixed-case schema name" do let(:schema_name) { "Test_Schema" } context "with a table name with no schema prefix" do let(:table_name) { "test_table" } it { is_expected.to eq "test_schema.test_table" } end end end describe '.replace_default_value' do subject { subject_object.replace_default_value(flydata_type, redshift_type, default_value) } context 'with cast expression from PostgreSQL' do context 'with multiple-lines in a default value' do let(:flydata_type) { "text" } let(:redshift_type) { "varchar(max)" } let(:default_value) { "'--- {} \nthe 2nd line'::text" } it { is_expected.to eq "'--- {} \nthe 2nd line'" } end context 'with :: in a default value' do let(:flydata_type) { "varchar" } let(:redshift_type) { "varchar" } let(:default_value) { "'FlyDataCore::TableDef'::character varying" } it { is_expected.to eq "'FlyDataCore::TableDef'" } end context 'with a single-quote in a default value' do let(:flydata_type) { "varchar" } let(:redshift_type) { "varchar" } let(:default_value) { "'That''s it!'::character varying" } it { is_expected.to eq "'That''s it!'" } end end context 'with integer type' do let(:flydata_type) { "int8" } let(:redshift_type) { "int8" } context "nextval() as the value" do let(:default_value) { "nextval('test_id_seq'::regclass)" } it 'returns nil because Redshift does not support nextval()' do is_expected.to be_nil end end end context 'with moeny type' do let(:flydata_type) { "money" } let(:redshift_type) { "money" } context 'in dollar format' do let(:default_value) { "'$2,392.40'" } it { is_expected.to eq "'2392.40'" } end context 'in JPY format' do let(:default_value) { "'JPY2,382,929'" } it { is_expected.to eq "'2382929'" } end context 'in INR format' do let(:default_value) { "'12,23,382.29'" } it { is_expected.to eq "'1223382.29'" } end context 'in DKK format' do let(:default_value) { "'10.382,29'" } it { is_expected.to eq "'10382.29'" } end end end end end end