# NOTE that these tests are very sensitive to precisce versions of GDAL (1.9.0) # 747 # Table post import processing tests should add a point the_geom column after importing a CSV # 1210 # Table merging two+ tables should import and then export file twitters.csv # 1235 # Table merging two+ tables should import and then export file SHP1.zip # 1256 # Table merging two+ tables should import and then export file SHP1.zip as kml # 1275 # Table merging two+ tables should import and then export file SHP1.zip as sql require_relative '../spec_helper' def check_schema(table, expected_schema, options={}) table_schema = table.schema(:cartodb_types => options[:cartodb_types] || false) schema_differences = (expected_schema - table_schema) + (table_schema - expected_schema) # Filter out timestamp columns for backwards compatibility with new CDB_CartodbfyTable schema_differences.reject! {|x| [:created_at, :updated_at].include?(x[0]) } schema_differences.should be_empty, "difference: #{schema_differences.inspect}" end def create_import(user, file_name, name=nil) @data_import = DataImport.create( user_id: @user.id, data_source: file_name, table_name: name ) def @data_import.data_source=(filepath) self.values[:data_type] = 'file' self.values[:data_source] = filepath end @data_import.data_source = file_name @data_import.send :dispatch @data_import end def expect_save_to_fail_validation(table) if table.model_class == Carto::UserTable expect(table.valid?).to be_false else expect { table.save }.to raise_error(Sequel::ValidationFailed) end end describe Table do shared_examples_for 'table service' do context "table setups" do it "should set a default name different than the previous" do table = Table.new table.user_id = @user.id table.save.reload table.name.should == "untitled_table" table2 = Table.new table2.user_id = @user.id table2.save.reload table2.name.should == "untitled_table_1" end it 'is renames "layergroup" to "layergroup_t"' do table = Table.new table.user_id = @user.id table.name = 'layergroup' table.name.should eq 'layergroup_t' table.valid?.should == true end it 'renames "all" to "all_t"' do table = Table.new table.user_id = @user.id table.name = 'all' table.name.should eq 'all_t' table.valid?.should == true end it 'renames "public" to "public_t"' do table = Table.new table.user_id = @user.id table.name = 'public' table.name.should eq 'public_t' table.valid?.should == true end it 'renames table when its name is prefixed by an underscore' do table = Table.new table.user_id = @user.id table.name = '_coffee' table.save table.name.should eq 'table_coffee' table.valid?.should == true end it "should set a valid table_id value (OID)" do table = create_table(name: 'this_is_a_table', user_id: @user.id) table.table_id.should be_a(Integer) oid = table.owner.in_database.fetch(%Q{SELECT '#{table.qualified_table_name}'::regclass::oid}).first[:oid].to_i table.table_id.should == oid end it "should return nil on get_table_id when the physical table doesn't exist" do table = create_table(name: 'this_is_a_table', user_id: @user.id) @user.in_database.drop_table table.name table.get_table_id.should be_nil end it "should not allow to create tables using system names" do table = create_table(name: "cdb_tablemetadata", user_id: @user.id) table.name.should == "cdb_tablemetadata_t" end it 'propagates name changes to table visualization' do table = create_table(name: 'bogus_name', user_id: @user.id) table.table_visualization.name.should == table.name table.name = 'bogus_name_1' table.save table.reload table.name .should == 'bogus_name_1' table.table_visualization.name .should == table.name table.name = 'viva la pepa' table.save table.reload table.name .should == 'viva_la_pepa' table.table_visualization.name .should == table.name table.name = ' viva el pepe ' table.save table.reload table.name .should == 'viva_el_pepe' table.table_visualization.name .should == table.name end it 'propagates name changes to analyses' do table = create_table(name: 'bogus_name', user_id: @user.id) carto_layer = Carto::Layer.find(table.layers.first.id) analysis = Carto::Analysis.source_analysis_for_layer(carto_layer, 0) analysis.save table.name.should eq 'bogus_name' table.name = 'new_name' table.save analysis.reload analysis.analysis_definition[:options][:table_name].should eq 'new_name' analysis.analysis_definition[:params][:query].should include('new_name') analysis.destroy table.destroy end it 'receives a name change if table visualization name changed' do Table.any_instance.stubs(:update_cdb_tablemetadata) table = create_table(name: 'bogus_name', user_id: @user.id) table.table_visualization.name.should == table.name table.table_visualization.name = 'bogus_name_2' table.table_visualization.store table.reload table.table_visualization.name.should == 'bogus_name_2' table.name.should == 'bogus_name_2' table.name.should == table.table_visualization.name CartoDB::UserModule::DBService.new(@user).tables_effective.should include('bogus_name_2') visualization_id = table.table_visualization.id # TODO: should this model be also "dynamic"? visualization = CartoDB::Visualization::Member.new(id: visualization_id) .fetch visualization.name = 'bogus name 3' visualization.store table.reload table.name.should == 'bogus_name_3' # TODO: should this model be also "dynamic"? visualization = CartoDB::Visualization::Member.new(id: visualization.id) .fetch visualization.name.should == 'bogus_name_3' table.reload table.name.should == 'bogus_name_3' end it 'propagates name changes to affected layers' do table = create_table(name: 'bogus_name', user_id: @user.id) layer = table.layers.first table.name = 'bogus_name_1' table.save table.reload layer.reload layer.options.fetch('table_name').should == table.name end it "should create default associated map and layers" do Cartodb.with_config( basemaps: { CartoDB: { "waduson" => { "default" => true, "urlTemplate" => "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png", "urlTemplate2x" => "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}@2x.png", "subdomains" => "abcd", "minZoom" => "0", "maxZoom" => "18", "name" => "Waduson", "className" => "waduson", "attribution" => "© OpenStreetMap contributors © CARTO" } } } ) do visualizations = CartoDB::Visualization::Collection.new.fetch.to_a.length table = create_table(name: "epaminondas_pantulis", user_id: @user.id) CartoDB::Visualization::Collection.new.fetch.to_a.length.should == visualizations + 1 map = table.map map.should be map.zoom.should eq Carto::Map::DEFAULT_OPTIONS[:zoom] map.bounding_box_sw.should eq Carto::Map::DEFAULT_OPTIONS[:bounding_box_sw] map.bounding_box_ne.should eq Carto::Map::DEFAULT_OPTIONS[:bounding_box_ne] map.center.should eq Carto::Map::DEFAULT_OPTIONS[:center] map.provider.should eq 'leaflet' map.layers.count.should == 2 map.layers.map(&:kind).should == ['tiled', 'carto'] map.data_layers.first.infowindow["fields"].should == [] map.data_layers.first.options["table_name"].should == "epaminondas_pantulis" map.layers[0].options["urlTemplate"].should == "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png" map.layers[0].options["urlTemplate2x"].should == "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}@2x.png" map.layers[0].options["subdomains"].should == "abcd" map.layers[0].options["minZoom"].should == "0" map.layers[0].options["maxZoom"].should == "18" map.layers[0].options["name"].should == "Waduson" map.layers[0].options["className"].should == "waduson" map.layers[0].options["attribution"].should == "© OpenStreetMap contributors © CARTO" map.layers[0].order.should == 0 map.visualization.overlays.count.should eq 5 end end it "should add a layer with labels if the baselayer has that option enabled" do Cartodb.with_config( basemaps: { CartoDB: { "waduson" => { "default" => true, "urlTemplate" => "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png", "urlTemplate2x" => "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}@2x.png", "subdomains" => "abcd", "minZoom" => "0", "maxZoom" => "18", "name" => "Waduson", "className" => "waduson", "attribution" => "© OpenStreetMap contributors © CARTO", "labels" => { "urlTemplate" => "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png", "urlTemplate2x" => "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}@2x.png" } } } } ) do visualizations = CartoDB::Visualization::Collection.new.fetch.to_a.length table = create_table(name: "epaminondas_pantulis", user_id: @user.id) CartoDB::Visualization::Collection.new.fetch.to_a.length.should == visualizations + 1 table.map.layers.count.should == 3 table.map.layers.map(&:kind).should == ['tiled', 'carto', 'tiled'] table.map.layers[0].options["urlTemplate"].should == "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png" table.map.layers[0].options["urlTemplate2x"].should == "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}@2x.png" table.map.layers[0].options["subdomains"].should == "abcd" table.map.layers[0].options["minZoom"].should == "0" table.map.layers[0].options["maxZoom"].should == "18" table.map.layers[0].options["name"].should == "Waduson" table.map.layers[0].options["className"].should == "waduson" table.map.layers[0].options["attribution"].should == "© OpenStreetMap contributors © CARTO" table.map.layers[0].order.should == 0 table.map.layers[2].options["urlTemplate"].should == "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png" table.map.layers[2].options["urlTemplate2x"].should == "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}@2x.png" table.map.layers[2].options["subdomains"].should == "abcd" table.map.layers[2].options["minZoom"].should == "0" table.map.layers[2].options["maxZoom"].should == "18" table.map.layers[2].options["name"].should == "Waduson Labels" table.map.layers[2].options["className"].should be_nil table.map.layers[2].options["attribution"].should == "© OpenStreetMap contributors © CARTO" table.map.layers[2].options["type"].should == "Tiled" table.map.layers[2].options["labels"].should be_nil table.map.layers[2].order.should == 2 end end it "should return a sequel interface" do table = create_table :user_id => @user.id table.sequel.is_a?(Sequel::Postgres::Dataset).should be_true end it "should have a privacy associated and it should be private by default" do table = create_table :user_id => @user.id table.should be_private end it 'changes to and from public-with-link privacy' do table = create_table :user_id => @user.id table.privacy = UserTable::PRIVACY_LINK table.save table.reload table.should be_public_with_link_only table.table_visualization.should be_public_with_link table.privacy = UserTable::PRIVACY_PUBLIC table.save table.reload table .should be_public table.table_visualization .should be_public end it 'propagates privacy changes to the associated visualization' do # Need to at least have this decorated in the user data or checks before becoming private will raise an error CartoDB::Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) table = create_table(user_id: @user.id) table.should be_private table.table_visualization.should be_private table.privacy = UserTable::PRIVACY_PUBLIC table.save table.reload table .should be_public table.table_visualization .should be_public rehydrated = UserTable.where(id: table.id).first rehydrated .should be_public rehydrated.table_visualization .should be_public table.privacy = UserTable::PRIVACY_PRIVATE table.save table.reload table .should be_private table.table_visualization .should be_private rehydrated = UserTable.where(id: table.id).first rehydrated .should be_private rehydrated.table_visualization .should be_private end it 'receives privacy changes from the associated visualization' do # Need to at least have this decorated in the user data or checks before becoming private will raise an error CartoDB::Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) table = create_table(user_id: @user.id) table.should be_private table.table_visualization.should be_private table.table_visualization.privacy = CartoDB::Visualization::Member::PRIVACY_PUBLIC table.table_visualization.store table.reload table .should be_public table.table_visualization .should be_public rehydrated = UserTable.where(id: table.id).first rehydrated .should be_public rehydrated.table_visualization .should be_public table.table_visualization.privacy = CartoDB::Visualization::Member::PRIVACY_PRIVATE table.table_visualization.store table.reload table .should be_private table.table_visualization .should be_private rehydrated = UserTable.where(id: table.id).first rehydrated .should be_private rehydrated.table_visualization .should be_private end it "should be public if the creating user doesn't have the ability to make private tables" do @user.private_tables_enabled = false @user.save table = create_table(:user_id => @user.id) table.privacy.should == UserTable::PRIVACY_PUBLIC end it "should be private if it's creating user has the ability to make private tables" do @user.private_tables_enabled = true @user.save table = create_table(:user_id => @user.id) table.privacy.should == UserTable::PRIVACY_PRIVATE end it "should be able to make private tables if the user gets the ability to do it" do @user.private_tables_enabled = false @user.save table = create_table(:user_id => @user.id) table.privacy.should == UserTable::PRIVACY_PUBLIC @user.private_tables_enabled = true @user.save table = create_table(:user_id => @user.id) table.privacy.should == UserTable::PRIVACY_PRIVATE end it "should only be able to make public tables if the user is stripped of permissions" do @user.private_tables_enabled = true @user.save table = create_table(:user_id => @user.id) table.privacy.should == UserTable::PRIVACY_PRIVATE @user.private_tables_enabled = false @user.save table = create_table(:user_id => @user.id) table.privacy.should == UserTable::PRIVACY_PUBLIC end it "should still be able to edit the private table if the user is stripped of permissions" do @user.private_tables_enabled = true @user.save table = create_table(user_id: @user.id) table.privacy.should == UserTable::PRIVACY_PRIVATE @user.private_tables_enabled = false @user.save table.name = "my_super_test" table.save.should be_true end it "should be able to convert to public table if the user is stripped of permissions" do @user.private_tables_enabled = true @user.save table = create_table(:user_id => @user.id) table.privacy.should == UserTable::PRIVACY_PRIVATE @user.private_tables_enabled = false @user.save table.privacy = UserTable::PRIVACY_PUBLIC table.save.should be_true end it "should not be able to convert to public table if the user has no permissions" do @user.private_tables_enabled = false @user.save table = create_table(:user_id => @user.id) table.privacy.should == UserTable::PRIVACY_PUBLIC table.privacy = UserTable::PRIVACY_PRIVATE expect_save_to_fail_validation(table) end it "should not be able to convert to public table if the user is stripped of " do @user.private_tables_enabled = true @user.save table = create_table(:user_id => @user.id) table.privacy.should == UserTable::PRIVACY_PRIVATE @user.private_tables_enabled = false @user.save table.privacy = UserTable::PRIVACY_PUBLIC table.save table.owner.reload # this is because the ORM is stupid table.privacy = UserTable::PRIVACY_PRIVATE expect_save_to_fail_validation(table) end it "should not allow public user access to a table when it is private" do @user.private_tables_enabled = true @user.save table = create_table(:user_id => @user.id) table.should be_private expect { @user.in_database(:as => :public_user).run("select * from #{table.name}") }.to raise_error(Sequel::DatabaseError) end it "should allow public user access when the table is public" do @user.private_tables_enabled = true @user.save table = create_table(:user_id => @user.id) table.should be_private table.privacy = UserTable::PRIVACY_PUBLIC table.save expect { @user.in_database(:as => :public_user).run("select * from #{table.name}") }.to_not raise_error end it "should be associated to a database table" do @user.private_tables_enabled = false @user.save table = create_table({:name => 'Wadus table', :user_id => @user.id}) SequelRails.connection.table_exists?(table.name.to_sym).should be_false @user.in_database do |user_database| user_database.table_exists?(table.name.to_sym).should be_true end end it "should store the name of its database" do @user.private_tables_enabled = false @user.save table = create_table(:user_id => @user.id) table.owner.database_name.should == @user.database_name end it "should rename a database table when the attribute name is modified" do delete_user_data @user @user.private_tables_enabled = false @user.save table = create_table(name: 'Wadus table', user_id: @user.id) SequelRails.connection.table_exists?(table.name.to_sym).should be_false @user.in_database do |user_database| user_database.table_exists?(table.name.to_sym).should be_true end table.name = 'Wadus table #23' table.save table.reload table.name.should == CartoDB::Importer2::StringSanitizer.sanitize("Wadus table #23") @user.in_database do |user_database| user_database.table_exists?('wadus_table'.to_sym).should be_false user_database.table_exists?('wadus_table_23'.to_sym).should be_true end table.name = '' table.save table.reload table.name.should == CartoDB::Importer2::StringSanitizer.sanitize("Wadus table #23") @user.in_database do |user_database| user_database.table_exists?('wadus_table_23'.to_sym).should be_true end end it 'converts all names to downcase' do delete_user_data @user @user.private_tables_enabled = false @user.save table = create_table({:name => 'Wadus table', :user_id => @user.id}) table.name.should == 'wadus_table' SequelRails.connection.table_exists?(table.name.to_sym).should be_false @user.in_database do |user_database| user_database.table_exists?(table.name.to_sym).should be_true end table.name = 'Wadus_table' table.name.should == 'wadus_table' end it "should invoke update_cdb_tablemetadata when the table is renamed" do delete_user_data @user @user.private_tables_enabled = false @user.save table = create_table(name: 'Wadus table', user_id: @user.id) CartoDB::TablePrivacyManager.any_instance table.expects(:update_cdb_tablemetadata) table.name = 'Wadus table #23' table.save end it "should rename the pk sequence when renaming the table" do table1 = new_table(name: 'table 1', user_id: @user.id) table1.stubs(:update_cdb_tablemetadata) table1.save.reload table1.name.should == 'table_1' table1.name = 'table 2' table1.save.reload table1.name.should == 'table_2' table2 = new_table(name: 'table 1', user_id: @user.id) table2.stubs(:update_cdb_tablemetadata) table2.save.reload table2.name.should == 'table_1' lambda { table2.destroy }.should_not raise_error end it "can't create a table using a reserved postgresql word as its name" do delete_user_data @user @user.private_tables_enabled = false @user.save table = create_table(name: 'as', user_id: @user.id) @user.in_database do |user_database| user_database.table_exists?('as_t'.to_sym).should be_true end table.name = 'where' table.save table.reload @user.in_database do |user_database| user_database.table_exists?('where_t'.to_sym).should be_true end end it 'raises QuotaExceeded when trying to create a table while over quota' do pending "Deactivated until table creation paths are unified - Issue 2974" quota_in_bytes = 524288000 table_quota = 5 new_user = new_user user = create_user(quota_in_bytes: quota_in_bytes, table_quota: table_quota) 5.times { |t| create_table(name: "table #{t}", user_id: user.id) } expect { create_table(name: "table 6", user_id: user.id) }.to raise_error(CartoDB::QuotaExceeded) user.destroy end end it "should remove varnish cache when updating the table privacy" do Carto::NamedMaps::Api.any_instance.stubs(get: nil, create: true, update: true) @user.private_tables_enabled = true @user.save table = create_table(user_id: @user.id, name: "varnish_privacy", privacy: UserTable::PRIVACY_PRIVATE) id = table.table_visualization.id CartoDB::Varnish.any_instance.expects(:purge) .once .with(".*#{id}:vizjson") .returns(true) CartoDB::TablePrivacyManager.any_instance.expects(:update_cdb_tablemetadata) table.privacy = UserTable::PRIVACY_PUBLIC table.save end context "when removing the table" do before(:all) do bypass_named_maps CartoDB::Varnish.any_instance.stubs(:send_command).returns(true) @doomed_table = create_table(user_id: @user.id) @doomed_table.destroy end before(:each) do bypass_named_maps end it "should remove the table from the user database" do expect { @user.in_database["select * from #{@doomed_table.name}"].all }.to raise_error end it "should not remove the table from the user database if specified" do table = create_table(user_id: @user.id) table.keep_user_database_table = true table.destroy @user.in_database["select * from #{table.name}"].all.should == [] end it "should update denormalized counters" do @user.reload Carto::Tag.count.should == 0 UserTable.count == 0 end it "should remove the metadata table even when the physical table does not exist" do table = create_table(user_id: @user.id) @user.in_database.drop_table(table.name.to_sym) table.destroy UserTable[table.id].should be_nil end end context 'schema and columns' do it 'has a default schema' do table = create_table(:user_id => @user.id) table.reload table.schema(:cartodb_types => false).should be_equal_to_default_db_schema table.schema.should be_equal_to_default_cartodb_schema end it "can be associated to many tags" do delete_user_data @user table = create_table :user_id => @user.id, :tags => "tag 1, tag 2,tag 3, tag 3" Carto::Tag.count.should == 3 tag1 = Carto::Tag.where(name: 'tag 1').first tag1.user_id.should == @user.id tag1.table_id.should == table.id tag2 = Carto::Tag.where(name: 'tag 2').first tag2.user_id.should == @user.id tag2.table_id.should == table.id tag3 = Carto::Tag.where(name: 'tag 3').first tag3.user_id.should == @user.id tag3.table_id.should == table.id table.tags = "tag 1" table.save_changes Carto::Tag.count.should == 1 tag1 = Carto::Tag.where(name: 'tag 1').first tag1.user_id.should == @user.id tag1.table_id.should == table.id table.tags = " " table.save_changes Carto::Tag.count.should == 0 end it "can add a column of a CartoDB::TYPE type" do table = create_table(:user_id => @user.id) table.schema(:cartodb_types => false).should be_equal_to_default_db_schema resp = table.add_column!(:name => "my new column", :type => "number") resp.should == {:name => "my_new_column", :type => "double precision", :cartodb_type => "number"} table.reload table.schema(:cartodb_types => false).should include([:my_new_column, "double precision"]) end it "can modify a column using a CartoDB::TYPE type" do table = create_table(user_id: @user.id) resp = table.modify_column!(name: "name", type: "number") resp.should == { name: "name", type: "double precision", cartodb_type: "number" } end it "can modify a column using a CartoDB::TYPE type" do table = create_table(:user_id => @user.id) resp = table.modify_column!(:name => "name", :type => "number") resp.should == {:name => "name", :type => "double precision", :cartodb_type => "number"} end it "should not modify the name of a column to a number, sanitizing it to make it valid" do table = create_table(:user_id => @user.id) resp = table.modify_column!(:name => "name", :new_name => "1") resp.should == {:name => "_1", :type => "text", :cartodb_type => "string"} end it "should invoke update_cdb_tablemetadata after modifying a column" do table = create_table(user_id: @user.id) table.expects(:update_cdb_tablemetadata) table.modify_column!(name: 'name', type: 'number') end it "should update public.cdb_tablemetadata after modifying a column" do table = create_table(:user_id => @user.id) table.expects(:update_cdb_tablemetadata).once table.modify_column!(:name => "name", :type => "number") end it "can modify its schema" do table = create_table(user_id: @user.id) table.schema(cartodb_types: false).should be_equal_to_default_db_schema lambda { table.add_column!(name: "my column with bad type", type: "textttt") }.should raise_error(CartoDB::InvalidType) resp = table.add_column!(name: "my new column", type: "integer") resp.should == { name: 'my_new_column', type: 'integer', cartodb_type: 'number'} table.reload table.schema(cartodb_types: false).should include([:my_new_column, "integer"]) resp = table.modify_column!(name: "my_new_column", new_name: "my new column new name", type: "text") resp.should == { name: 'my_new_column_new_name', type: 'text', cartodb_type: 'string' } table.reload table.schema(cartodb_types: false).should include([:my_new_column_new_name, "text"]) resp = table.modify_column!(name: "my_new_column_new_name", new_name: "my new column") resp.should == { name: 'my_new_column', type: "text", cartodb_type: "string"} table.reload table.schema(cartodb_types: false).should include([:my_new_column, "text"]) resp = table.modify_column!(:name => "my_new_column", :type => "text") resp.should == {:name => 'my_new_column', :type => 'text', :cartodb_type => 'string'} table.reload table.schema(:cartodb_types => false).should include([:my_new_column, "text"]) table.drop_column!(:name => "description") table.reload table.schema(:cartodb_types => false).should_not include([:description, "text"]) lambda { table.drop_column!(:name => "description") }.should raise_error end it "cannot modify :cartodb_id column" do table = create_table(:user_id => @user.id) original_schema = table.schema(:cartodb_types => false) lambda { table.modify_column!(:name => "cartodb_id", :new_name => "new_id", :type => "integer") }.should raise_error table.reload table.schema(:cartodb_types => false).should == original_schema lambda { table.modify_column!(:name => "cartodb_id", :new_name => "cartodb_id", :type => "float") }.should raise_error table.reload table.schema(:cartodb_types => false).should == original_schema lambda { table.drop_column!(:name => "cartodb_id") }.should raise_error table.reload table.schema(:cartodb_types => false).should == original_schema end it "should be able to modify it's schema with castings the DB engine doesn't support" do table = create_table(user_id: @user.id) table.add_column!(name: "my new column", type: "text") table.reload table.schema(:cartodb_types => false) .should include([:my_new_column, "text"]) pk = table.insert_row!(name: "Text", my_new_column: "1") table.modify_column!( name: "my_new_column", new_name: "my new column new name", type: "integer", force_value: "NULL" ) table.reload table.schema(cartodb_types: false) .should include([:my_new_column_new_name, "integer"]) rows = table.records rows[:rows][0][:my_new_column_new_name].should == 1 end it "can be created with a given schema if it is valid" do table = new_table(:user_id => @user.id) table.force_schema = "code char(5) CONSTRAINT firstkey PRIMARY KEY, title varchar(40) NOT NULL, did integer NOT NULL, date_prod date, kind varchar(10)" table.save check_schema(table, [ [:cartodb_id, "integer"], [:code, "character(5)"], [:title, "character varying(40)"], [:did, "integer"], [:date_prod, "date"], [:kind, "character varying(10)"], [:the_geom, "geometry", "geometry", "geometry"] ]) end it "should sanitize columns from a given schema" do delete_user_data @user table = new_table(:user_id => @user.id) table.force_schema = "\"code wadus\" char(5) CONSTRAINT firstkey PRIMARY KEY, title varchar(40) NOT NULL, did integer NOT NULL, date_prod date, kind varchar(10)" table.save check_schema(table, [ [:cartodb_id, "integer"], [:code_wadus, "character(5)"], [:title, "character varying(40)"], [:did, "integer"], [:date_prod, "date"], [:kind, "character varying(10)"], [:the_geom, "geometry", "geometry", "geometry"] ]) end it "should alter the schema automatically to a a wide range of numbers when inserting" do table = new_table(:user_id => @user.id) table.force_schema = "name varchar, age integer" table.save pk_row1 = table.insert_row!(:name => 'Fernando Blat', :age => "29") table.rows_counted.should == 1 pk_row2 = table.insert_row!(:name => 'Javi Jam', :age => "30.3") table.rows_counted.should == 2 table.schema(:cartodb_types => false).should include([:age, "double precision"]) table.schema.should include([:age, "number"]) end it "should alter the schema automatically to a a wide range of numbers when inserting a number with 0" do table = new_table(:user_id => @user.id) table.force_schema = "name varchar, age integer" table.save pk_row1 = table.insert_row!(:name => 'Fernando Blat', :age => "29") table.rows_counted.should == 1 pk_row2 = table.insert_row!(:name => 'Javi Jam', :age => "30.0") table.rows_counted.should == 2 table.schema(:cartodb_types => false).should include([:age, "double precision"]) table.schema.should include([:age, "number"]) end it "should alter the schema automatically to a a wide range of numbers when updating" do table = new_table(:user_id => @user.id) table.force_schema = "name varchar, age integer" table.save pk_row1 = table.insert_row!(:name => 'Fernando Blat', :age => "29") table.rows_counted.should == 1 pk_row2 = table.update_row!(pk_row1, :name => 'Javi Jam', :age => "25.4") table.rows_counted.should == 1 table.schema(:cartodb_types => false).should include([:age, "double precision"]) table.schema.should include([:age, "number"]) end pending "should alter the schema automatically when trying to insert a big string (greater than 200 chars)" do table = new_table(:user_id => @user.id) table.force_schema = "name varchar(40)" table.save table.schema(:cartodb_types => false).should_not include([:name, "text"]) pk_row1 = table.insert_row!(:name => 'f'*201) table.rows_counted.should == 1 table.reload table.schema(:cartodb_types => false).should include([:name, "text"]) end it "should not remove an existing table when the creation of a new table with default schema and the same name has raised an exception" do table = new_table({:name => 'table1', :user_id => @user.id}) table.save pk = table.insert_row!({:name => "name #1", :description => "description #1"}) Table.any_instance.stubs(:the_geom_type=).raises(CartoDB::InvalidGeomType) table = new_table({:name => 'table1', :user_id => @user.id}) lambda { table.save }.should raise_error(CartoDB::InvalidGeomType) table.run_query("select name from table1 where cartodb_id = '#{pk}'")[:rows].first[:name].should == "name #1" end it "should not remove an existing table when the creation of a new table from a file with the same name has raised an exception" do table = new_table({:name => 'table1', :user_id => @user.id}) table.save pk = table.insert_row!({:name => "name #1", :description => "description #1"}) Table.any_instance.stubs(:schema).raises(CartoDB::QueryNotAllowed) data_import = DataImport.create( :user_id => @user.id, :table_name => 'rescol', :data_source => fake_data_path('reserved_columns.csv') ) data_import.run_import! table.run_query("select name from table1 where cartodb_id = '#{pk}'")[:rows].first[:name].should == "name #1" end it "can add a column called 'action' but gets renamed" do column_name = "action" sanitized_column_name = "_action" table = create_table(:user_id => @user.id) resp = table.add_column!(:name => column_name, :type => "number") resp.should == {:name => sanitized_column_name, :type => "double precision", :cartodb_type => "number"} table.reload table.schema(:cartodb_types => false).should include([sanitized_column_name.to_sym, "double precision"]) end it "can have a column with a reserved psql word as its name" do column_name = "where" sanitized_column_name = "_where" table = create_table(:user_id => @user.id) resp = table.add_column!(:name => column_name, :type => "number") resp.should == {:name => sanitized_column_name, :type => "double precision", :cartodb_type => "number"} table.reload table.schema(:cartodb_types => false).should include([sanitized_column_name.to_sym, "double precision"]) end it 'nullifies the collumn when converting from boolean to date' do column_name = "new" sanitized_column_name = "_new" table = create_table(user_id: @user.id) table.add_column!(name: column_name, type: 'boolean') table.insert_row!(sanitized_column_name.to_sym => 't') table.modify_column!(name: sanitized_column_name, type: 'date') table.records[:rows][0][sanitized_column_name.to_sym].should be_nil table = create_table(user_id: @user.id) table.add_column!(name: sanitized_column_name, type: 'boolean') table.insert_row!(sanitized_column_name.to_sym => 'f') table.modify_column!(name: sanitized_column_name, type: 'date') table.records[:rows][0][sanitized_column_name.to_sym].should be_nil end it 'nullifies the collumn when converting from number to date' do table = create_table(user_id: @user.id) table.add_column!(name: 'numeric_col', type: 'double precision') table.insert_row!(numeric_col: 12345.67) table.modify_column!(name: 'numeric_col', type: 'date') table.records[:rows][0][:numeric_col].should be_nil table = create_table(user_id: @user.id) table.add_column!(name: 'numeric_col', type: 'double precision') table.insert_row!(numeric_col: 12345) table.modify_column!(name: 'numeric_col', type: 'date') table.records[:rows][0][:numeric_col].should be_nil end it 'normalizes digit separators when converting from string to number' do table = create_table(user_id: @user.id) table.add_column!(name: 'balance', type: 'text') table.insert_row!(balance: '1.234,56') table.modify_column!(name: 'balance', type: 'double precision') table.records[:rows][0][:balance].should == 1234.56 table = create_table(user_id: @user.id) table.add_column!(name: 'balance', type: 'text') table.insert_row!(balance: '123.456,789') table.modify_column!(name: 'balance', type: 'double precision') table.records[:rows][0][:balance].should == 123456.789 table = create_table(user_id: @user.id) table.add_column!(name: 'balance', type: 'text') table.insert_row!(balance: '9.123.456,789') table.modify_column!(name: 'balance', type: 'double precision') table.records[:rows][0][:balance].should == 9123456.789 table = create_table(user_id: @user.id) table.add_column!(name: 'balance', type: 'text') table.insert_row!(balance: '1,234.56') table.modify_column!(name: 'balance', type: 'double precision') table.records[:rows][0][:balance].should == 1234.56 table = create_table(user_id: @user.id) table.add_column!(name: 'balance', type: 'text') table.insert_row!(balance: '123,456.789') table.modify_column!(name: 'balance', type: 'double precision') table.records[:rows][0][:balance].should == 123456.789 table = create_table(user_id: @user.id) table.add_column!(name: 'balance', type: 'text') table.insert_row!(balance: '9,123,456.789') table.modify_column!(name: 'balance', type: 'double precision') table.records[:rows][0][:balance].should == 9123456.789 end it 'does not raise error when tables with the same name exist on separate schemas' do @user.in_database.run("CREATE TABLE cdb_importer.repeated_table (id integer)") expect { create_table(user_id: @user.id, name: 'repeated_table') }.to_not raise_error end it 'correctly returns dates' do table = create_table(user_id: @user.id) table.add_column!(name: "without_tz", type: "timestamp without time zone") table.add_column!(name: "with_tz", type: "timestamp with time zone") table.add_column!(name: "date", type: "date ") # Add a space to avoid auto-conversion to timestampz table.insert_row!( with_tz: '2016-12-02T10:10:10+01:00', without_tz: '2016-12-02T10:10:10', date: '2016-12-02' ) record = table.record(1) record[:with_tz].is_a?(DateTime).should be_true record[:with_tz].should eq DateTime.parse('2016-12-02T10:10:10+01:00') record[:without_tz].is_a?(DateTime).should be_true record[:without_tz].to_s.should eq '2016-12-02T10:10:10+00:00' record[:date].is_a?(DateTime).should be_true record[:date].to_s.should eq '2016-12-02T00:00:00+00:00' end end context "insert and update rows" do it "should be able to insert a new row" do table = create_table(:user_id => @user.id) table.rows_counted.should == 0 primary_key = table.insert_row!({:name => String.random(10), :description => "bla bla bla"}) table.reload table.rows_counted.should == 1 primary_key.should == table.records(:rows_per_page => 1)[:rows].first[:cartodb_id] lambda { table.insert_row!({}) }.should_not raise_error(CartoDB::EmptyAttributes) lambda { table.insert_row!({:non_existing => "bad value"}) }.should raise_error(CartoDB::InvalidAttributes) end it "fails with long values" do table = create_table(user_id: @user.id) table.add_column!(name: 'text_col', type: 'varchar(3)') expect { table.insert_row!(text_col: 'hola') }.to raise_error(Sequel::DatabaseError, /value too long for type/) end it "updates data_last_modified when changing data" do table = create_table(:user_id => @user.id) table.insert_row!({}) time1 = table.data_last_modified.to_f sleep(0.5) table.insert_row!({}) time2 = table.data_last_modified.to_f (time2 > time1).should be_true end it "should be able to insert a row with a geometry value" do table = new_table(:user_id => @user.id) table.save.reload lat = -43.941 lon = 3.429 the_geom = %Q{{"type":"Point","coordinates":[#{lon},#{lat}]}} pk = table.insert_row!({:name => "First check_in", :the_geom => the_geom}) query_result = @user.db_service.run_pg_query("select ST_X(the_geom) as lon, ST_Y(the_geom) as lat from #{table.name} where cartodb_id = #{pk} limit 1") ("%.3f" % query_result[:rows][0][:lon]).should == ("%.3f" % lon) ("%.3f" % query_result[:rows][0][:lat]).should == ("%.3f" % lat) end it "should update null value to nil when inserting and updating" do table = new_table(:user_id => @user.id) table.force_schema = "valid boolean" table.save.reload pk = table.insert_row!({:valid => "null"}) table.record(pk)[:valid].should be_nil pk = table.insert_row!({:valid => true}) table.update_row!(pk, {:valid => "null"}) table.record(pk)[:valid].should be_nil end it "should be able to update a row" do table = create_table(:user_id => @user.id) pk = table.insert_row!({:name => String.random(10), :description => ""}) table.update_row!(pk, :description => "Description 123") row = table.records(:rows_per_page => 1, :page => 0)[:rows].first row[:description].should == "Description 123" lambda { table.update_row!(pk, :non_existing => 'ignore it, please', :description => "Description 123") }.should raise_error(CartoDB::InvalidAttributes) end it "should be able to update a row with a geometry value" do table = new_table(:user_id => @user.id) table.save.reload lat = -43.941 lon = 3.429 pk = table.insert_row!({:name => "First check_in"}) the_geom = %Q{{"type":"Point","coordinates":[#{lon},#{lat}]}} table.update_row!(pk, {:the_geom => the_geom}) query_result = @user.db_service.run_pg_query("select ST_X(the_geom) as lon, ST_Y(the_geom) as lat from #{table.name} where cartodb_id = #{pk} limit 1") ("%.3f" % query_result[:rows][0][:lon]).should == ("%.3f" % lon) ("%.3f" % query_result[:rows][0][:lat]).should == ("%.3f" % lat) end it "can insert and update records in a table with a reserved word as its name" do table = create_table(:name => 'where', :user_id => @user.id) pk1 = table.insert_row!({:name => String.random(10), :description => "bla bla bla"}) pk2 = table.insert_row!({:name => String.random(10), :description => "bla bla bla"}) table.records[:rows].should have(2).rows table.update_row!(pk1, :description => "Description 123") table.records[:rows].first[:description].should be == "Description 123" end it "should be able to update data in rows with column names with multiple underscores" do data_import = DataImport.create( :user_id => @user.id, :table_name => 'elecciones2008', :data_source => Rails.root.join('spec/support/data/elecciones2008.csv').to_s) data_import.run_import! table = create_table(user_table: UserTable[data_import.table_id], user_id: @user.id) table.should_not be_nil, "Import failure: #{data_import.log}" update_data = {:upo_nombre_partido=>"PSOEE"} id = 5 lambda { table.update_row!(id, update_data) }.should_not raise_error res = table.sequel.where(:cartodb_id => 5).first res[:upo_nombre_partido].should == "PSOEE" end it "should be able to insert data in rows with column names with multiple underscores" do data_import = DataImport.create( :user_id => @user.id, :data_source => Rails.root.join('spec/support/data/elecciones2008.csv').to_s) data_import.run_import! table = create_table(user_table: UserTable[data_import.table_id], user_id: @user.id) table.should_not be_nil, "Import failure: #{data_import.log}" pk = nil insert_data = {:upo_nombre_partido=>"PSOEE"} lambda { pk = table.insert_row!(insert_data) }.should_not raise_error res = table.sequel.where(:cartodb_id => pk).first res[:upo_nombre_partido].should == "PSOEE" end # No longer used, now we automatically rename reserved word columns #it "can insert and update records in a table which one of its columns uses a reserved word as its name" do #table = create_table(:name => 'where', :user_id => @user.id) #table.add_column!(:name => 'where', :type => 'string') #pk1 = table.insert_row!({:_where => 'random string'}) #table.records[:rows].should have(1).rows #table.records[:rows].first[:_where].should be == 'random string' #end end context "preimport tests" do it "rename a table to a name that exists should add a _1 to the new name" do table = new_table :name => 'empty_file', :user_id => @user.id table.save.reload table.name.should == 'empty_file' table2 = new_table :name => 'empty_file', :user_id => @user.id table2.save.reload table2.name.should == 'empty_file_1' end it "should escape table names starting with numbers" do table = new_table :user_id => @user.id, :name => '123_table_name' table.save.reload table.name.should == "table_123_table_name" end it "should get a valid name when a table when a name containing the current name exists" do table = create_table :name => 'Table #20', :user_id => @user.id table2 = create_table :name => 'Table #2', :user_id => @user.id table2.reload table2.name.should == 'table_2' table3 = create_table :name => nil, :user_id => @user.id table4 = create_table :name => nil, :user_id => @user.id table5 = create_table :name => nil, :user_id => @user.id table6 = create_table :name => nil, :user_id => @user.id end it "should allow creating multiple tables with the same name by adding a number at the and and incrementing it" do table = create_table :name => 'Wadus The Table', :user_id => @user.id table.name.should == "wadus_the_table" # Renaming starts at 1 1.upto(25) do |n| table = create_table :name => 'Wadus The Table', :user_id => @user.id table.should_not be_nil table.name.should == "wadus_the_table_#{n}" end end end context "post import processing tests" do before(:all) do @old_user_timeout = @user.user_timeout @old_user_db_timeout = @user.database_timeout end after(:each) do @user.user_timeout = @old_user_timeout @user.database_timeout = @old_user_db_timeout @user.save end it "should optimize the table" do fixture = fake_data_path("SHP1.zip") Table.any_instance.expects(:optimize).once data_import = create_import(@user, fixture) end it "should assign table_id" do fixture = fake_data_path("SHP1.zip") data_import = create_import(@user, fixture) data_import.table_id.should_not be_nil end it "should add a the_geom column after importing a CSV" do delete_user_data @user data_import = DataImport.create( :user_id => @user.id, :data_source => fake_data_path('twitters.csv') ) data_import.run_import! table = Table.new(user_table: UserTable[data_import.table_id]) table.should_not be_nil, "Import failure: #{data_import.log}" table.name.should match(/^twitters/) table.rows_counted.should == 7 table.schema.should include([:the_geom, "geometry", "geometry", "geometry"]) end it "should not fail when the analyze is executed in update_table_geom_pg_stats and raises a PG::UndefinedColumn" do next unless @user.in_database.table_exists?('raster_overviews') delete_user_data @user data_import = DataImport.create(user_id: @user.id, data_source: fake_data_path('import_raster.tif.zip')) data_import.run_import! table = Table.new(user_table: UserTable[data_import.table_id]) table.should_not be_nil, "Import failure: #{data_import.log}" table.name.should match(/^import_raster/) table.update_table_geom_pg_stats end it "should not drop a table that exists when upload fails" do delete_user_data @user table = new_table :name => 'empty_file', :user_id => @user.id table.should_not be_nil table.save.reload table.name.should == 'empty_file' fixture = fake_data_path("empty_file.csv") data_import = create_import(@user, fixture, table.name) @user.in_database do |user_database| user_database.table_exists?(table.name.to_sym).should be_true end end it "should not drop a table that exists when upload does not fail" do delete_user_data @user table = new_table :name => 'empty_file', :user_id => @user.id table.save.reload table.name.should == 'empty_file' data_import = DataImport.create( :user_id => @user.id, :data_source => fake_data_path('csv_no_quotes.csv') ) data_import.run_import! table2 = Table.new(user_table: UserTable[data_import.table_id]) table2.should_not be_nil, "Import failure: #{data_import.log}" table2.name.should == 'csv_no_quotes' @user.in_database do |user_database| user_database.table_exists?(table.name.to_sym).should be_true user_database.table_exists?(table2.name.to_sym).should be_true end end it "should raise an error when creating a column with reserved name" do table = create_table(user_id: @user.id) lambda { table.add_column!(:name => "xmin", :type => "number") }.should raise_error(CartoDB::InvalidColumnName) end it "should raise an error when renaming a column with reserved name" do table = create_table(:user_id => @user.id) lambda { table.rename_column('name', 'xmin') }.should raise_error(CartoDB::InvalidColumnName) end it "should not raise an error when renaming a column with reserved name" do table = create_table(:user_id => @user.id) resp = table.modify_column!(:name => "name", :new_name => "xmin") resp.should == {:name => "_xmin", :type => "text", :cartodb_type => "string"} end it "should add a cartodb_id serial column as primary key when importing a file without a column with name cartodb_id" do fixture = fake_data_path("gadm4_export.csv") data_import = create_import(@user, fixture) table = Table.new(user_table: UserTable[data_import.table_id]) table.should_not be_nil, "Import failure: #{data_import.log.inspect}" table_schema = @user.in_database.schema(table.name) cartodb_id_schema = table_schema.detect {|s| s[0].to_s == "cartodb_id"} cartodb_id_schema.should be_present cartodb_id_schema = cartodb_id_schema[1] cartodb_id_schema[:db_type].should == "integer" cartodb_id_schema[:default].should == "nextval('#{table.name}_cartodb_id_seq'::regclass)" cartodb_id_schema[:primary_key].should == true cartodb_id_schema[:allow_null].should == false end it "should return geometry types when guessing is enabled" do data_import = DataImport.create( :user_id => @user.id, :data_source => fake_data_path('gadm4_export.csv'), :type_guessing => true ) data_import.run_import! table = Table.new(user_table: UserTable[data_import.table_id]) table.should_not be_nil, "Import failure: #{data_import.log.inspect}" table.geometry_types.should == ['ST_Point'] # Now remove the_geom and should not break @user.in_database.run(%Q{ ALTER TABLE #{table.name} DROP COLUMN the_geom CASCADE; }) # Schema gets cached, force reload table.reload table.schema(reload:true) # This is no longer true: it should be considered a stat or guessing # for the UI to plot icons on table listings and similar stuff and will # not be invalidated from the editor. #table.geometry_types.should == [] table.destroy end it "returns null values at the end when ordering desc" do table = create_table(user_id: @user.id) resp = table.add_column!(name: "numbercolumn", type: "number") table.insert_row!(numbercolumn: 1) table.insert_row!(numbercolumn: nil) table.insert_row!(numbercolumn: 2) rows = table.records(order_by: 'numbercolumn', mode: 'desc')[:rows] rows.last[:numbercolumn].should eq nil rows.first[:numbercolumn].should eq 2 end it "should normalize strings if there is a non-convertible entry when converting string to number" do fixture = fake_data_path("short_clubbing.csv") data_import = create_import(@user, fixture) table = Table.new(user_table: UserTable[data_import.table_id]) table.modify_column! :name=> "club_id", :type=>"number" table.sequel.where(:cartodb_id => '1').first[:club_id].should == 709 table.sequel.where(:cartodb_id => '2').first[:club_id].should == 892 table.sequel.where(:cartodb_id => '3').first[:club_id].should == 992 table.sequel.where(:cartodb_id => '4').first[:club_id].should == nil table.sequel.where(:cartodb_id => '5').first[:club_id].should == 941 end it "should normalize string if there is a non-convertible entry when converting string to boolean" do fixture = fake_data_path("column_string_to_boolean.csv") data_import = create_import(@user, fixture) table = Table.new(user_table: UserTable[data_import.table_id]) # configure nil column table.sequel.where(:test_id => '4').update(:f1 => '0') # configure nil column table.sequel.where(:test_id => '11').update(:f1 => nil) # configure blank column table.sequel.insert(:test_id => '12', :f1 => "") # update datatype table.modify_column! :type=>"boolean", :name=>"f1", :new_name=>nil # test table.sequel.where(:cartodb_id => '1').first[:f1].should == true table.sequel.select(:f1).where(:cartodb_id => '2').first[:f1].should == true table.sequel.select(:f1).where(:cartodb_id => '3').first[:f1].should == true table.sequel.select(:f1).where(:cartodb_id => '4').first[:f1].should == false table.sequel.select(:f1).where(:cartodb_id => '5').first[:f1].should == true table.sequel.select(:f1).where(:cartodb_id => '6').first[:f1].should == true table.sequel.select(:f1).where(:cartodb_id => '7').first[:f1].should == true table.sequel.select(:f1).where(:test_id => '8').first[:f1].should == false table.sequel.select(:f1).where(:test_id => '9').first[:f1].should == false table.sequel.select(:f1).where(:test_id => '10').first[:f1].should == false table.sequel.select(:f1).where(:test_id => '11').first[:f1].should == nil table.sequel.select(:f1).where(:test_id => '12').first[:f1].should == nil end it "should normalize boolean if there is a non-convertible entry when converting boolean to string" do fixture = fake_data_path("column_string_to_boolean.csv") data_import = create_import(@user, fixture) table = Table.new(user_table: UserTable[data_import.table_id]) table.modify_column! :name=>"f1", :type=>"boolean" table.modify_column! :name=>"f1", :type=>"string" table.sequel.select(:f1).where(:test_id => '1').first[:f1].should == 'true' table.sequel.select(:f1).where(:test_id => '8').first[:f1].should == 'false' end it "should normalize boolean if there is a non-convertible entry when converting boolean to number" do fixture = fake_data_path("column_string_to_boolean.csv") data_import = create_import(@user, fixture) table = Table.new(user_table: UserTable[data_import.table_id]) table.modify_column! :name=>"f1", :type=>"boolean" table.modify_column! :name=>"f1", :type=>"number" table.sequel.select(:f1).where(:test_id => '1').first[:f1].should == 1 table.sequel.select(:f1).where(:test_id => '8').first[:f1].should == 0 end it "should normalize number if there is a non-convertible entry when converting number to boolean" do fixture = fake_data_path("column_number_to_boolean.csv") data_import = create_import(@user, fixture) table = Table.new(user_table: UserTable[data_import.table_id]) table.modify_column! :name=>"f1", :type=>"number" table.modify_column! :name=>"f1", :type=>"boolean" table.sequel.select(:f1).where(:test_id => '1').first[:f1].should == true table.sequel.select(:f1).where(:test_id => '2').first[:f1].should == false table.sequel.select(:f1).where(:test_id => '3').first[:f1].should == true table.sequel.select(:f1).where(:test_id => '4').first[:f1].should == true end it "should not fail when the analyze is executed in update_table_pg_stats and raises a timeout" do delete_user_data @user old_user_timeout = @user.user_timeout old_user_db_timeout = @user.database_timeout data_import = DataImport.create(user_id: @user.id, data_source: fake_data_path('twitters.csv')) data_import.run_import! table = Table.new(user_table: UserTable[data_import.table_id]) table.should_not be_nil, "Import failure: #{data_import.log}" table.name.should match(/^twitters/) @user.user_timeout = 1 @user.database_timeout = 1 @user.save table.update_table_pg_stats @user.user_timeout = @old_user_timeout @user.database_timeout = @old_user_db_timeout @user.save table.rows_counted.should == 7 end it "should not fail when the analyze is executed in update_table_geom_pg_stats and raises a timeout" do delete_user_data @user old_user_timeout = @user.user_timeout old_user_db_timeout = @user.database_timeout data_import = DataImport.create(user_id: @user.id, data_source: fake_data_path('twitters.csv')) data_import.run_import! table = Table.new(user_table: UserTable[data_import.table_id]) table.should_not be_nil, "Import failure: #{data_import.log}" table.name.should match(/^twitters/) @user.user_timeout = 1 @user.database_timeout = 1 @user.save table.update_table_geom_pg_stats @user.user_timeout = @old_user_timeout @user.database_timeout = @old_user_db_timeout @user.save table.rows_counted.should == 7 end end context "geoms and projections" do it "should set valid geometry types" do table = new_table :user_id => @user.id table.force_schema = "address varchar, the_geom geometry" table.the_geom_type = "line" table.save table.reload table.the_geom_type.should == "multilinestring" end it "should create a the_geom_webmercator column with the_geom projected to 3785" do table = new_table :user_id => @user.id table.save.reload lat = -43.941 lon = 3.429 pk = table.insert_row!({:name => "First check_in"}) the_geom = %Q{{"type":"Point","coordinates":[#{lon},#{lat}]}} table.update_row!(pk, {:the_geom => the_geom}) query_result = @user.db_service.run_pg_query("select ST_X(ST_TRANSFORM(the_geom_webmercator,4326)) as lon, ST_Y(ST_TRANSFORM(the_geom_webmercator,4326)) as lat from #{table.name} where cartodb_id = #{pk} limit 1") ("%.3f" % query_result[:rows][0][:lon]).should == ("%.3f" % lon) ("%.3f" % query_result[:rows][0][:lat]).should == ("%.3f" % lat) end it "should create a the_geom_webmercator column with the_geom projected to 3785 even when schema is forced" do table = new_table :user_id => @user.id table.force_schema = "name varchar, the_geom geometry" table.save.reload lat = -43.941 lon = 3.429 pk = table.insert_row!({:name => "First check_in"}) the_geom = %Q{{"type":"Point","coordinates":[#{lon},#{lat}]}} table.update_row!(pk, {:the_geom => the_geom}) query_result = @user.db_service.run_pg_query("select ST_X(ST_TRANSFORM(the_geom_webmercator,4326)) as lon, ST_Y(ST_TRANSFORM(the_geom_webmercator,4326)) as lat from #{table.name} where cartodb_id = #{pk} limit 1") ("%.3f" % query_result[:rows][0][:lon]).should == ("%.3f" % lon) ("%.3f" % query_result[:rows][0][:lat]).should == ("%.3f" % lat) end it "should be able to set a the_geom column from numeric latitude column and a longitude column" do table = Table.new table.user_id = @user.id table.name = 'Madrid Bars' table.force_schema = "name varchar, address varchar, latitude float, longitude float" table.save table.insert_row!({:name => "Hawai", :address => "Calle de Pérez Galdós 9, Madrid, Spain", :latitude => 40.423012, :longitude => -3.699732}) table.georeference_from!(:latitude_column => :latitude, :longitude_column => :longitude) # Check if the schema stored in memory is fresh and contains latitude and longitude still check_schema(table, [ [:cartodb_id, "number"], [:name, "string"], [:address, "string"], [:the_geom, "geometry", "geometry", "point"], [:latitude, "number"], [:longitude, "number"] ], :cartodb_types => true) # confirm coords are correct res = table.sequel.select{[st_x(the_geom),st_y(the_geom)]}.first res.should == {:st_x=>-3.699732, :st_y=>40.423012} end it "should be able to set a the_geom column from dirty string latitude and longitude columns" do table = Table.new table.user_id = @user.id table.name = 'Madrid Bars' table.force_schema = "name varchar, address varchar, latitude varchar, longitude varchar" table.save table.insert_row!({:name => "Hawai", :address => "Calle de Pérez Galdós 9, Madrid, Spain", :latitude => "40.423012", :longitude => " -3.699732 "}) table.georeference_from!(:latitude_column => :latitude, :longitude_column => :longitude) # Check if the schema stored in memory is fresh and contains latitude and longitude still check_schema(table, [ [:cartodb_id, "number"], [:name, "string"], [:address, "string"], [:the_geom, "geometry", "geometry", "point"], [:latitude, "string"], [:longitude, "string"] ], :cartodb_types => true) # confirm coords are correct res = table.sequel.select{[st_x(the_geom),st_y(the_geom)]}.first res.should == {:st_x=>-3.699732, :st_y=>40.423012} end context "geojson tests" do it "should return a geojson for the_geom if it is a point" do table = new_table :user_id => @user.id table.the_geom_type = "point" table.save.reload lat = -43.941 lon = 3.429 the_geom = %Q{{"type":"Point","coordinates":[#{lon},#{lat}]}} pk = table.insert_row!({:name => "First check_in", :the_geom => the_geom}) #update the_geom = %Q{{"type":"Point","coordinates":[0,0]}} table.send :update_the_geom!, { :the_geom => the_geom }, 1 records = table.records(:page => 0, :rows_per_page => 1) records[:rows][0][:the_geom].should == "{\"type\":\"Point\",\"coordinates\":[0,0]}" end it "should raise an error when the geojson provided is invalid" do table = new_table :user_id => @user.id table.save.reload lat = -43.941 lon = 3.429 the_geom = %Q{{"type":""""Point","coordinates":[#{lon},#{lat}]I}} lambda { table.insert_row!({:name => "First check_in", :the_geom => the_geom}) }.should raise_error(CartoDB::InvalidGeoJSONFormat) end it "should return new geojson even if geojson provided had other projection" do table = new_table :user_id => @user.id table.the_geom_type = "point" table.save.reload lat = -43.941 lon = 3.429 the_geom = %Q{{"type":"Point","coordinates":[#{lon},#{lat}]}} pk = table.insert_row!({:name => "First check_in", :the_geom => the_geom}) #update the_geom = %Q{{"type":"Point","coordinates":[0,0], "crs":{"type":"name","properties":{"name":"EPSG:232323"}} }} table.send :update_the_geom!, { :the_geom => the_geom }, 1 records = table.records(:page => 0, :rows_per_page => 1) records[:rows][0][:the_geom].should == "{\"type\":\"Point\",\"coordinates\":[0,0]}" end end end context "migrate existing postgresql tables into cartodb" do it "create table via SQL statement and then migrate table into CartoDB" do table = new_table :name => nil, :user_id => @user.id table.migrate_existing_table = "exttable" @user.db_service.run_pg_query("CREATE TABLE exttable (go VARCHAR, ttoo INT, bed VARCHAR)") @user.db_service.run_pg_query("INSERT INTO exttable (go, ttoo, bed) VALUES ( 'c', 1, 'p'); INSERT INTO exttable (go, ttoo, bed) VALUES ( 'c', 2, 'p')") table.save table.name.should == 'exttable' table.rows_counted.should == 2 end it "create and migrate a table containing a the_geom and cartodb_id" do delete_user_data @user table = new_table :name => nil, :user_id => @user.id table.migrate_existing_table = "exttable" @user.db_service.run_pg_query("CREATE TABLE exttable (the_geom VARCHAR, cartodb_id INT, bed VARCHAR)") @user.db_service.run_pg_query("INSERT INTO exttable (the_geom, cartodb_id, bed) VALUES ( 'c', 1, 'p'); INSERT INTO exttable (the_geom, cartodb_id, bed) VALUES ( 'c', 2, 'p')") table.save table.name.should == 'exttable' table.rows_counted.should == 2 end end context "imports" do it "file twitters.csv" do fixture = fake_data_path("twitters.csv") data_import = create_import(@user, fixture) data_import.table_name.should match(/^twitters/) Table.new(user_table: UserTable[data_import.table_id]).rows_counted.should == 7 end it "file SHP1.zip" do fixture = fake_data_path("SHP1.zip") data_import = create_import(@user, fixture) data_import.table_name.should == "esp_adm1" Table.new(user_table: UserTable[data_import.table_id]).rows_counted.should == 18 end end context "retrieving tables from ids" do it "should be able to find a table by name or by identifier" do table = new_table :user_id => @user.id table.name = 'awesome name' table.save.reload UserTable.find_by_identifier(@user.id, table.name).id.should == table.id lambda { UserTable.find_by_identifier(666, table.name) }.should raise_error end end describe '#get_by_table_id' do it 'returns table service' do id = Carto::UserTable.first.id table = Table.get_by_table_id(id) table.should_not be_nil table.id.should eq id end end describe '#table_size' do it 'returns nil for unknown tables' do Table.table_size(String.random(10), connection: @user.in_database).should be_nil end end describe '#has_index?' do let(:table) { create_table name: 'table_with_indexes', user_id: @user.id } it 'returns true when the index exists' do table.has_index?('cartodb_id').should be_true table.has_index?('the_geom').should be_true table.has_index?('the_geom_webmercator').should be_true end it 'returns false when the index does not exist' do table.has_index?('cartodb_id2').should be_false table.has_index?('the_geom_wadus').should be_false end end describe '#name=' do it 'does not change the name if it is equivalent to the current one' do table = new_table(user_id: @user.id, name: 'new name') table.name.should == 'new_name' table.name = 'new name' table.name.should == 'new_name' end end describe '#validation_for_link_privacy' do it 'checks that only users with private tables enabled can set LINK privacy' do table_id = Carto::UUIDHelper.random_uuid user_mock = mock user_mock.stubs(:private_tables_enabled).returns(true) user_mock.stubs(:database_name).returns(nil) user_mock.stubs(:over_table_quota?).returns(false) user_mock.stubs(:database_schema).returns('public') user_mock.stubs(:viewer).returns(false) ::Table.any_instance.stubs(:get_valid_name).returns('test') ::Table.any_instance.stubs(:owner).returns(user_mock) ::Table.any_instance.stubs(:create_table_in_database!) ::Table.any_instance.stubs(:set_table_id).returns(table_id) ::Table.any_instance.stubs(:set_the_geom_column!).returns(true) ::UserTable.any_instance.stubs(:after_create) ::Table.any_instance.stubs(:after_save) ::Table.any_instance.stubs(:cartodbfy) ::Table.any_instance.stubs(:schema) CartoDB::TablePrivacyManager.any_instance.stubs(:owner).returns(user_mock) # A user who can create private tables has by default private tables user_table = ::UserTable.new user_table.stubs(:user).returns(user_mock) user_table.send(:default_privacy_value).should eq ::UserTable::PRIVACY_PRIVATE user_table.user_id = Carto::UUIDHelper.random_uuid user_table.privacy = UserTable::PRIVACY_PUBLIC user_table.name = 'test' user_table.validate user_table.errors.size.should eq 0 user_table.privacy = UserTable::PRIVACY_PRIVATE user_table.validate user_table.errors.size.should eq 0 user_table.privacy = UserTable::PRIVACY_LINK user_table.validate user_table.errors.size.should eq 0 user_table.privacy = UserTable::PRIVACY_PUBLIC user_mock.stubs(:private_tables_enabled).returns(false) # Anybody can "keep" a table being type link if it is new or hasn't changed (changed meaning had a previous privacy value) user_table.privacy = UserTable::PRIVACY_LINK user_table.validate user_table.errors.size.should eq 0 # Save so privacy changes instead of being "new" user_table.privacy = UserTable::PRIVACY_PUBLIC user_table.save user_table.privacy = UserTable::PRIVACY_LINK user_table.validate user_table.errors.size.should eq 1 expected_errors_hash = { privacy: ['unauthorized to modify privacy status to public with link'] } user_table.errors.should eq expected_errors_hash # A user who cannot create private tables has by default public user_table = ::UserTable.new user_table.stubs(:user).returns(user_mock) user_table.send(:default_privacy_value).should eq ::UserTable::PRIVACY_PUBLIC end end #validation_for_link_privacy describe '#the_geom_conversions' do it 'tests the_geom conversions and expected results' do def check_query_geometry(query, schema) tablename = unique_name('table') table = new_table(name: nil, user_id: @user.id) table.migrate_existing_table = tablename @user.db_service.run_pg_query("CREATE TABLE #{tablename} AS #{query}") table.save table.the_geom_type_value = nil # Reset geometry cache check_schema(table, schema) end # Empty table/default schema (no conversion) table = new_table(:name => 'one', :user_id => @user.id) table.save check_schema(table, [ [:cartodb_id, 'integer'], [:description, 'text'], [:name, 'text'], [:the_geom, 'geometry', 'geometry', 'geometry'] ]) # latlong projection check_query_geometry('SELECT CDB_LatLng(0,0) AS the_geom', [ [:cartodb_id, 'bigint'], [:the_geom, 'geometry', 'geometry', 'point'] ]) # single multipoint, without srid check_query_geometry('SELECT ST_Collect(ST_MakePoint(0,0),ST_MakePoint(1,1)) AS the_geom;', [ [:cartodb_id, 'bigint'], [:the_geom, 'geometry', 'geometry', 'point'], ]) # same as above (single multipoint), but with a SRID=4326 (latlong) check_query_geometry('SELECT ST_SetSRID(ST_Collect(ST_MakePoint(0,0),ST_MakePoint(1,1)),4326) AS the_geom', [ [:cartodb_id, 'bigint'], [:the_geom, 'geometry', 'geometry', 'point'] ]) # single polygon check_query_geometry('SELECT ST_SetSRID(ST_Buffer(ST_MakePoint(0,0),10), 4326) AS the_geom', [ [:cartodb_id, 'bigint'], [:the_geom, 'geometry', 'geometry', 'multipolygon'] ]) # single line check_query_geometry('SELECT ST_SetSRID(ST_Boundary(ST_Buffer(ST_MakePoint(0,0),10,1)), 4326) AS the_geom', [ [:cartodb_id, 'bigint'], [:the_geom, 'geometry', 'geometry', 'multilinestring'] ]) # field named "the_geom" being _not_ of type geometry check_query_geometry("SELECT 'wadus' AS the_geom;", [ [:cartodb_id, 'bigint'], [:the_geom, 'geometry', 'geometry', 'geometry'], [:the_geom_str, 'text'] ]) # geometrycollection (concrete type) Unsupported table = new_table(:name => nil, :user_id => @user.id) table.migrate_existing_table = 'eight' @user.db_service.run_pg_query(' CREATE TABLE eight AS SELECT ST_SetSRID(ST_Collect(ST_MakePoint(0,0), ST_Buffer(ST_MakePoint(10,0),1)), 4326) AS the_geom ') expect { table.save }.to raise_exception end end describe '#test_import_cleanup' do it 'tests correct removal of some fields upon importing a table' do ogc_fid_field = 'ogc_fid' gid_field = 'gid' # Assumptions: imported_id_2 > imported_id_1 and cartodb_id_2 > cartodb_id_1 cartodb_id_1 = 1 cartodb_id_2 = 2 imported_id_1 = 3 imported_id_2 = 4 description_1 = 'blabla' description_2 = 'blablabla' table = new_table :name => nil, :user_id => @user.id table.migrate_existing_table = 'only_ogc_fid' @user.db_service.run_pg_query(%Q{ CREATE TABLE #{table.migrate_existing_table} (#{ogc_fid_field} INT, description VARCHAR) }) @user.db_service.run_pg_query(%Q{ INSERT INTO #{table.migrate_existing_table} (#{ogc_fid_field}, description) VALUES (#{imported_id_1}, '#{description_1}'), (#{imported_id_2}, '#{description_2}') }) table.save check_schema(table, [ [:cartodb_id, 'integer'], [:the_geom, 'geometry', 'geometry', 'geometry'], [:description, 'text'] ]) rows = table.records rows[:rows][0][:cartodb_id].should eq imported_id_1 rows[:rows][1][:cartodb_id].should eq imported_id_2 rows[:rows][0][:description].should eq description_1 rows[:rows][1][:description].should eq description_2 table = new_table :name => nil, :user_id => @user.id table.migrate_existing_table = 'only_gid' @user.db_service.run_pg_query(%Q{ CREATE TABLE #{table.migrate_existing_table} (#{gid_field} INT, description VARCHAR) }) @user.db_service.run_pg_query(%Q{ INSERT INTO #{table.migrate_existing_table} (#{gid_field}, description) VALUES (#{imported_id_1}, '#{description_1}'), (#{imported_id_2}, '#{description_2}') }) table.save check_schema(table, [ [:cartodb_id, 'integer'], [:the_geom, 'geometry', 'geometry', 'geometry'], [:description, 'text'] ]) rows = table.records rows[:rows][0][:cartodb_id].should eq imported_id_1 rows[:rows][1][:cartodb_id].should eq imported_id_2 rows[:rows][0][:description].should eq description_1 rows[:rows][1][:description].should eq description_2 table = new_table :name => nil, :user_id => @user.id table.migrate_existing_table = 'cartodb_id_and_ogc_fid' @user.db_service.run_pg_query(%Q{ CREATE TABLE #{table.migrate_existing_table} (cartodb_id INT, #{ogc_fid_field} INT, description VARCHAR) }) @user.db_service.run_pg_query(%Q{ INSERT INTO #{table.migrate_existing_table} (cartodb_id, #{ogc_fid_field}, description) VALUES (#{cartodb_id_1}, #{imported_id_1}, '#{description_1}'), (#{cartodb_id_2}, #{imported_id_2}, '#{description_2}') }) table.save check_schema(table, [ [:cartodb_id, 'integer'], [:the_geom, 'geometry', 'geometry', 'geometry'], [:description, 'text'] ]) rows = table.records rows[:rows][0][:cartodb_id].should eq cartodb_id_1 rows[:rows][1][:cartodb_id].should eq cartodb_id_2 rows[:rows][0][:description].should eq description_1 rows[:rows][1][:description].should eq description_2 table = new_table :name => nil, :user_id => @user.id table.migrate_existing_table = 'cartodb_id_and_gid' @user.db_service.run_pg_query(%Q{ CREATE TABLE #{table.migrate_existing_table} (cartodb_id INT, #{gid_field} INT, description VARCHAR) }) @user.db_service.run_pg_query(%Q{ INSERT INTO #{table.migrate_existing_table} (cartodb_id, #{gid_field}, description) VALUES (#{cartodb_id_1}, #{imported_id_1}, '#{description_1}'), (#{cartodb_id_2}, #{imported_id_2}, '#{description_2}') }) table.save check_schema(table, [ [:cartodb_id, 'integer'], [:the_geom, 'geometry', 'geometry', 'geometry'], [:description, 'text'] ]) rows = table.records rows[:rows][0][:cartodb_id].should eq cartodb_id_1 rows[:rows][1][:cartodb_id].should eq cartodb_id_2 rows[:rows][0][:description].should eq description_1 rows[:rows][1][:description].should eq description_2 end end describe '#key' do it 'computes a suitable key for a table' do table = create_table(name: "any_name", user_id: @user.id) table.redis_key.should == "rails:table:#{table.id}" end it 'computes different keys for different tables' do table_1 = create_table(user_id: @user.id) table_2 = create_table(user_id: @user.id) table_1.redis_key.should_not == table_2.redis_key end end describe '#geometry_types_key' do it 'computes a suitable key' do table = create_table(name: 'any_other_name', user_id: @user.id) table.geometry_types_key.should == "#{table.redis_key}:geometry_types" end end describe '#geometry_types' do it "returns an empty array and does not cache if there's no column the_geom" do table = create_table(user_id: @user.id) cache = mock() cache.expects(:get).once cache.expects(:setex).once table.stubs(:cache).returns(cache) # A bit extreme way of getting a table without the_geom table.owner.in_database.run(%Q{ALTER TABLE #{table.name} DROP COLUMN "the_geom" CASCADE}) table.schema(reload: true) table.geometry_types.should == [] end it "returns an empty array and does not cache if there are no geometries in the query" do table = create_table(user_id: @user.id) cache = mock() cache.expects(:get).once.returns(nil) cache.expects(:setex).once table.stubs(:cache).returns(cache) table.geometry_types.should == [] end it "caches if there are geometries" do table = create_table(user_id: @user.id) cache = mock() cache.expects(:get).once cache.expects(:setex).once table.stubs(:cache).returns(cache) table.owner.in_database.run(%Q{ INSERT INTO #{table.name}(the_geom) VALUES(ST_GeomFromText('POINT(-71.060316 48.432044)', 4326)) }) table.geometry_types.should == ['ST_Point'] end it "returns the value from the cache if it is there" do table = create_table(user_id: @user.id) any_types = ['ST_Any_Type', 'ST_Any_Other_Type'] table.expects(:query_geometry_types).once.returns(any_types) table.geometry_types.should eq(any_types), "cache miss failure" table.geometry_types.should eq(any_types), "cache hit failure" $tables_metadata.get(table.geometry_types_key).should eq(any_types.to_s), "it should be actually cached" end end describe 'self.table_and_schema' do it 'returns nil schema if schema is "public"' do Table.table_and_schema("public.sm_org_line_cartotest").should == ["sm_org_line_cartotest", nil] end it 'returns the schema if it is different from "public"' do Table.table_and_schema("manolito.sm_org_line_cartotest").should == ["sm_org_line_cartotest", "manolito"] end it 'returns the table name when there is no schema' do Table.table_and_schema("sm_org_line_cartotest").should == ["sm_org_line_cartotest", nil] end it 'returns schema without quotes' do Table.table_and_schema("\"user-hyphen\".table").should == ["table", "user-hyphen"] end end describe 'self.get_valid_column_name' do it 'returns the same candidate name if it is ok' do Table.expects(:get_column_names).once.returns(%w{a b c}) version = CartoDB::Importer2::Column::CURRENT_COLUMN_SANITIZATION_VERSION Table.get_valid_column_name('dummy_table_name', 'a', version).should == 'a' end it 'returns an alternative name if using a reserved word' do Table.expects(:get_column_names).once.returns(%w{column b c}) version = CartoDB::Importer2::Column::CURRENT_COLUMN_SANITIZATION_VERSION Table.get_valid_column_name( 'dummy_table_name', 'column', version ).should == '_column' end it 'avoids collisions when a renamed column already exists' do Table.expects(:get_column_names).once.returns(%w{_column column c}) version = CartoDB::Importer2::Column::CURRENT_COLUMN_SANITIZATION_VERSION Table.get_valid_column_name( 'dummy_table_name', 'column', version ).should == '_column_1' end end describe '#estimated_row_count and #actual_row_count' do it "should return row counts" do table = new_table(:user_id => @user.id) table.save pk_row1 = table.insert_row!(:name => 'name1') table.actual_row_count.should == 1 [0, 1].should include(table.estimated_row_count) end context 'organization' do include_context 'organization with users helper' it 'returns the right row count estimation without mixing tables from other users' do table1 = new_table(user_id: @org_user_1.id, name: 'wadus1') table1.save table1.insert_row!(name: '1') @org_user_1.in_database.run("ANALYZE #{table1.qualified_table_name}") table2 = new_table(user_id: @org_user_2.id, name: 'wadus1') table2.save table2.insert_row!(name: '1') table2.insert_row!(name: '2') @org_user_2.in_database.run("ANALYZE #{table2.qualified_table_name}") table1.estimated_row_count.should eq 1 table2.estimated_row_count.should eq 2 end end end end shared_examples_for 'table service with legacy model' do context 'table setups' do before(:all) do @user.private_tables_enabled = true @user.save end before(:each) do @carto_user = ::Table.new.model_class == ::UserTable ? @user : Carto::User.find(@user.id) end it 'propagates changes to affected visualizations if privacy set to PRIVATE' do # Need to at least have this decorated in the user data or checks before becoming private will raise an error CartoDB::Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) table = create_table(user_id: @user.id) table.should be_private table.table_visualization.should be_private map = CartoDB::Visualization::TableBlender.new(@carto_user, [table]).blend derived_vis = FactoryGirl.create(:derived_visualization, user_id: @user.id, map_id: map.id, privacy: CartoDB::Visualization::Member::PRIVACY_PRIVATE) bypass_named_maps derived_vis.store table.reload table.privacy = UserTable::PRIVACY_PUBLIC table.save table.affected_visualizations.map do |vis| vis.public?.should == (vis.type == Carto::Visualization::TYPE_CANONICAL) end table.privacy = UserTable::PRIVACY_PRIVATE table.save table.affected_visualizations.map do |vis| vis.private?.should == true end end it "doesn't propagates changes to affected visualizations if privacy set to public with link" do # Need to at least have this decorated in the user data or checks before becoming private will raise an error CartoDB::Visualization::Member.any_instance.stubs(:supports_private_maps?).returns(true) table = create_table(user_id: @user.id) table.privacy = UserTable::PRIVACY_PUBLIC table.save map = CartoDB::Visualization::TableBlender.new(@carto_user, [table]).blend derived_vis = FactoryGirl.create(:derived_visualization, user_id: @user.id, map_id: map.id) bypass_named_maps derived_vis.store table.reload table.privacy = UserTable::PRIVACY_LINK table.save table.reload table.affected_visualizations.map do |vis| vis.public?.should eq vis.derived? # Derived kept public vis.private?.should eq false # None changed to private vis.password_protected?.should eq false # None changed to password protected vis.public_with_link?.should eq (vis.type == Carto::Visualization::TYPE_CANONICAL) # Table/canonical changed end end end context "when removing the table" do before(:all) do bypass_named_maps CartoDB::Varnish.any_instance.stubs(:send_command).returns(true) end before(:each) do bypass_named_maps @carto_user = ::Table.new.model_class == ::UserTable ? @user : Carto::User.find(@user.id) end it 'deletes derived visualizations that depend on this table' do bypass_named_maps table = create_table(name: 'bogus_name', user_id: @user.id) map = CartoDB::Visualization::TableBlender.new(@carto_user, [table]).blend derived = FactoryGirl.create(:derived_visualization, user_id: @user.id, map_id: map.id) table.reload table.destroy expect { CartoDB::Visualization::Member.new(id: derived.id).fetch }.to raise_error KeyError end it 'deletes layers from derived visualizations that partially depend on this table' do bypass_named_maps table = create_table(name: 'bogus_name', user_id: @user.id) map = CartoDB::Visualization::TableBlender.new(@carto_user, [table]).blend derived = FactoryGirl.create(:derived_visualization, user_id: @user.id, map_id: map.id) map.layers << FactoryGirl.create(:carto_layer) CartoDB::Visualization::Member.new(id: derived.id).fetch.data_layers.count.should eq 2 table.reload table.destroy CartoDB::Visualization::Member.new(id: derived.id).fetch.data_layers.count.should eq 1 end end describe '#destroy' do it "invalidates geometry_types cache entry" do table = create_table(user_id: @user.id) any_types = ['ST_Any_Type', 'ST_Any_Other_Type'] table.expects(:query_geometry_types).once.returns(any_types) table.geometry_types.should eq(any_types) key = table.geometry_types_key table.destroy $tables_metadata.get(key).should eq(nil), "the geometry types cache should be invalidated upon table removal" end end describe '#after_save' do it 'invalidates derived visualization cache if there are changes in table privacy' do @user.private_tables_enabled = true @user.save @carto_user = ::Table.new.model_class == ::UserTable ? @user : Carto::User.find(@user.id) table = create_table(user_id: @user.id) table.save table.should be_private Carto::NamedMaps::Api.any_instance.stubs(get: nil, create: true, update: true) map = CartoDB::Visualization::TableBlender.new(@carto_user, [table]).blend derived = FactoryGirl.create(:derived_visualization, user_id: @user.id, map_id: map.id) derived.type.should eq(CartoDB::Visualization::Member::TYPE_DERIVED) # Do not create all member objects anew to be able to set expectations CartoDB::Visualization::Member.stubs(:new).with(has_entry(id: derived.id)).returns(derived) CartoDB::Visualization::Member.stubs(:new).with(has_entry(type: 'table')).returns(table.table_visualization) # Hack to get the correct (Sequel/AR) model CartoDB::Varnish.new.purge(derived.varnish_vizjson_key) table.privacy = UserTable::PRIVACY_PUBLIC table.save @user.private_tables_enabled = false @user.save end it 'privacy reverts if named map update fails' do @user.private_tables_enabled = true @user.save @carto_user = ::Table.new.model_class == ::UserTable ? @user : Carto::User.find(@user.id) table = create_table(user_id: @user.id, privacy: UserTable::PRIVACY_PUBLIC) table.save Carto::NamedMaps::Api.any_instance.stubs(get: nil, create: true, update: true) map = CartoDB::Visualization::TableBlender.new(@carto_user, [table]).blend derived = FactoryGirl.create(:derived_visualization, user_id: @user.id, map_id: map.id) derived.type.should eq(CartoDB::Visualization::Member::TYPE_DERIVED) # Scenario 1: Fail at map saving (can happen due to Map handlers) table.privacy = UserTable::PRIVACY_PRIVATE (table.model_class == ::UserTable ? ::Map : Carto::Map).any_instance.stubs(:save).once.raises(StandardError) expect { table.save }.to raise_exception StandardError table.reload.privacy.should eq UserTable::PRIVACY_PUBLIC if table.model_class == Carto::UserTable # There is something weird with AR. The name unique validation always returns false after throwing from a hook table.instance_variable_set(:@user_table, Carto::UserTable.find(table.id)) end (table.model_class == ::UserTable ? ::Map : Carto::Map).any_instance.stubs(:save).returns(true) # Scenario 2: Fail setting user table privacy (unlikely, but just in case) # Moved to table_privacy_manager_spec # Scenario 3: Fail saving canonical visualization named map viz_class = table.model_class == ::UserTable ? CartoDB::Visualization::Member : Carto::Visualization viz_class.any_instance.stubs(:store_using_table) .raises('Manolo is a nice guy, this test is not.') .then.returns(nil).at_least(2) table.privacy = UserTable::PRIVACY_PRIVATE expect do table.save end.to raise_error 'Manolo is a nice guy, this test is not.' table.reload.privacy.should eq UserTable::PRIVACY_PUBLIC viz_class.any_instance.unstub(:store_using_table) # Scenario 4: Fail saving affected visualizations named map viz_class.any_instance.stubs(:store_using_table) .raises('Manolo is a nice guy, this test is not.') .then.returns(nil).at_least(2) table.privacy = UserTable::PRIVACY_PRIVATE expect do table.save end.to raise_error 'Manolo is a nice guy, this test is not.' table.reload.privacy.should eq UserTable::PRIVACY_PUBLIC viz_class.any_instance.unstub(:store_using_table) # Scenario 5: All went fine table.privacy = UserTable::PRIVACY_PRIVATE table.save table.reload.privacy.should eq UserTable::PRIVACY_PRIVATE table.privacy = UserTable::PRIVACY_PUBLIC table.save table.reload.privacy.should eq UserTable::PRIVACY_PUBLIC @user.private_tables_enabled = false @user.save end end end describe 'with Carto::UserTable model' do before(:all) do @user = FactoryGirl.create(:valid_user, quota_in_bytes: 524288000, table_quota: 500, private_tables_enabled: true) end before(:each) do CartoDB::UserModule::DBService.any_instance.stubs(:enable_remote_db_user).returns(true) CartoDB::Varnish.any_instance.stubs(:send_command).returns(true) Table.any_instance.stubs(:update_cdb_tablemetadata) bypass_named_maps end after(:all) do @user.destroy end it_behaves_like 'table service' it_behaves_like 'table service with legacy model' end end