445 lines
17 KiB
Ruby
445 lines
17 KiB
Ruby
|
require_relative '../../spec_helper_min.rb'
|
||
|
require_relative '../../../lib/carto/ghost_tables_manager'
|
||
|
require 'helpers/database_connection_helper'
|
||
|
|
||
|
module Carto
|
||
|
describe GhostTablesManager do
|
||
|
include DatabaseConnectionHelper
|
||
|
|
||
|
let(:sequel_user) { create(:valid_user) }
|
||
|
let!(:user) { Carto::User.find(sequel_user.id) }
|
||
|
let!(:ghost_tables_manager) { Carto::GhostTablesManager.new(user.id) }
|
||
|
|
||
|
before(:each) do
|
||
|
bypass_named_maps
|
||
|
Rails.logger.expects(:error).never
|
||
|
end
|
||
|
|
||
|
after { Carto::User.destroy_all }
|
||
|
|
||
|
def run_in_user_database(query)
|
||
|
sequel_user.in_database.run(query)
|
||
|
end
|
||
|
|
||
|
it 'should be consistent when no new/renamed/dropped tables' do
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_synchronously).never
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_asynchronously).never
|
||
|
ghost_tables_manager.send(:link_ghost_tables)
|
||
|
end
|
||
|
|
||
|
it 'should not run sync when the user has more than MAX_USERTABLES_FOR_SYNC_CHECK tables' do
|
||
|
# We simulate the deletion of 2 tables, which would trigger a sync run in a small user
|
||
|
ghost_tables_manager.stubs(:fetch_user_tables)
|
||
|
.returns([*1..Carto::GhostTablesManager::MAX_USERTABLES_FOR_SYNC_CHECK + 2].map do |id|
|
||
|
Carto::TableFacade.new(id, "name #{id}", user.id)
|
||
|
end
|
||
|
)
|
||
|
|
||
|
ghost_tables_manager.stubs(:fetch_cartodbfied_tables)
|
||
|
.returns([*1..Carto::GhostTablesManager::MAX_USERTABLES_FOR_SYNC_CHECK].map do |id|
|
||
|
Carto::TableFacade.new(id, "name #{id}", user.id)
|
||
|
end
|
||
|
)
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_asynchronously).once
|
||
|
# In big databases, `:should_run_synchronously?` is expensive so we want to ensure it isn't called at all
|
||
|
ghost_tables_manager.expects(:should_run_synchronously?).never
|
||
|
|
||
|
ghost_tables_manager.send(:link_ghost_tables)
|
||
|
end
|
||
|
|
||
|
it 'should not run sync when more than MAX_TABLES_FOR_SYNC_RUN need to be linked' do
|
||
|
regenerated_tables = []
|
||
|
renamed_tables = []
|
||
|
new_tables = []
|
||
|
dropped_tables = [*1..Carto::GhostTablesManager::MAX_TABLES_FOR_SYNC_RUN]
|
||
|
ghost_tables_manager.stubs(:fetch_altered_tables)
|
||
|
.returns([regenerated_tables, renamed_tables, new_tables, dropped_tables])
|
||
|
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_synchronously).never
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_asynchronously).once
|
||
|
ghost_tables_manager.send(:link_ghost_tables)
|
||
|
end
|
||
|
|
||
|
it 'should not run sync when more than MAX_TABLES_FOR_SYNC_RUN need to be linked including new tables' do
|
||
|
regenerated_tables = []
|
||
|
renamed_tables = []
|
||
|
new_tables = [*1..Carto::GhostTablesManager::MAX_TABLES_FOR_SYNC_RUN - 2]
|
||
|
dropped_tables = ['manolo', 'pepito']
|
||
|
ghost_tables_manager.stubs(:fetch_altered_tables)
|
||
|
.returns([regenerated_tables, renamed_tables, new_tables, dropped_tables])
|
||
|
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_synchronously).never
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_asynchronously).once
|
||
|
ghost_tables_manager.send(:link_ghost_tables)
|
||
|
end
|
||
|
|
||
|
it 'should run sync when more than 0 but less than MAX_TABLES_FOR_SYNC_RUN need to be linked' do
|
||
|
regenerated_tables = []
|
||
|
renamed_tables = []
|
||
|
new_tables = []
|
||
|
dropped_tables = [*1..Carto::GhostTablesManager::MAX_TABLES_FOR_SYNC_RUN - 1]
|
||
|
ghost_tables_manager.stubs(:fetch_altered_tables)
|
||
|
.returns([regenerated_tables, renamed_tables, new_tables, dropped_tables])
|
||
|
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_synchronously).once
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_asynchronously).never
|
||
|
ghost_tables_manager.send(:link_ghost_tables)
|
||
|
end
|
||
|
|
||
|
it 'should not run sync when no tables are stale or dropped' do
|
||
|
regenerated_tables = []
|
||
|
renamed_tables = []
|
||
|
new_tables = [*1..Carto::GhostTablesManager::MAX_TABLES_FOR_SYNC_RUN]
|
||
|
dropped_tables = []
|
||
|
ghost_tables_manager.stubs(:fetch_altered_tables)
|
||
|
.returns([regenerated_tables, renamed_tables, new_tables, dropped_tables])
|
||
|
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_synchronously).never
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_asynchronously).once
|
||
|
ghost_tables_manager.send(:link_ghost_tables)
|
||
|
end
|
||
|
|
||
|
it 'should link sql created table, relink sql renamed tables and unlink sql dropped tables' do
|
||
|
run_in_user_database(%{
|
||
|
CREATE TABLE manoloescobar ("description" text);
|
||
|
SELECT * FROM CDB_CartodbfyTable('manoloescobar');
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
|
||
|
::Resque.expects(:enqueue).with(::Resque::UserDBJobs::UserDBMaintenance::LinkGhostTablesByUsername, user.username).never
|
||
|
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
user.tables.first.name.should == 'manoloescobar'
|
||
|
|
||
|
run_in_user_database(%{
|
||
|
ALTER TABLE manoloescobar RENAME TO escobar;
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
user.tables.first.name.should == 'escobar'
|
||
|
|
||
|
run_in_user_database(%{
|
||
|
DROP TABLE escobar;
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
end
|
||
|
|
||
|
it 'should link sql created table using regular api key with create permissions' do
|
||
|
grants = [
|
||
|
{
|
||
|
type: 'apis',
|
||
|
apis: ['maps', 'sql']
|
||
|
},
|
||
|
{
|
||
|
type: "database",
|
||
|
schemas: [
|
||
|
{
|
||
|
name: "#{user.database_schema}",
|
||
|
permissions: ['create']
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
]
|
||
|
api_key = user.api_keys.create_regular_key!(name: 'ghost_tables', grants: grants)
|
||
|
with_connection_from_api_key(api_key) do |connection|
|
||
|
sql = %{
|
||
|
CREATE TABLE test_table ("description" text);
|
||
|
SELECT * FROM CDB_CartodbfyTable('test_table');
|
||
|
}
|
||
|
connection.execute(sql)
|
||
|
end
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
|
||
|
::Resque.expects(:enqueue).with(::Resque::UserDBJobs::UserDBMaintenance::LinkGhostTablesByUsername, user.username).never
|
||
|
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
user.tables.first.name.should == 'test_table'
|
||
|
|
||
|
with_connection_from_api_key(api_key) do |connection|
|
||
|
connection.execute('DROP TABLE test_table')
|
||
|
end
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
api_key.destroy
|
||
|
end
|
||
|
|
||
|
it 'should link sql created table using oauth_app api key with create permissions' do
|
||
|
scopes = ['offline', 'user:profile', 'schemas:c']
|
||
|
app = FactoryGirl.create(:oauth_app, user: user)
|
||
|
oau = OauthAppUser.create!(user: user, oauth_app: app, scopes: scopes)
|
||
|
refresh_token = oau.oauth_refresh_tokens.create!(scopes: scopes)
|
||
|
access_token = refresh_token.exchange!(requested_scopes: scopes)[0]
|
||
|
|
||
|
with_connection_from_api_key(access_token.api_key) do |connection|
|
||
|
sql = %{
|
||
|
CREATE TABLE test_table ("description" text);
|
||
|
SELECT * FROM CDB_CartodbfyTable('test_table');
|
||
|
}
|
||
|
connection.execute(sql)
|
||
|
end
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
|
||
|
::Resque.expects(:enqueue).with(::Resque::UserDBJobs::UserDBMaintenance::LinkGhostTablesByUsername, user.username).never
|
||
|
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
user.tables.first.name.should == 'test_table'
|
||
|
|
||
|
with_connection_from_api_key(access_token.api_key) do |connection|
|
||
|
connection.execute('DROP TABLE test_table')
|
||
|
end
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
oau.destroy
|
||
|
app.destroy
|
||
|
end
|
||
|
|
||
|
it 'should not link non cartodbyfied tables' do
|
||
|
run_in_user_database(%{
|
||
|
CREATE TABLE manoloescobar ("description" text);
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
run_in_user_database(%{
|
||
|
DROP TABLE manoloescobar;
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
end
|
||
|
|
||
|
it 'should link raster tables' do
|
||
|
next unless user.db_service.tables_effective.include?('raster_overviews')
|
||
|
|
||
|
run_in_user_database(%{
|
||
|
CREATE TABLE manolo_raster ("cartodb_id" uuid, "the_raster_webmercator" raster);
|
||
|
CREATE TRIGGER test_quota_per_row
|
||
|
BEFORE INSERT OR UPDATE
|
||
|
ON manolo_raster
|
||
|
FOR EACH ROW
|
||
|
EXECUTE PROCEDURE cdb_checkquota('0.001', '-1', 'public');
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
user.tables.first.name.should == 'manolo_raster'
|
||
|
|
||
|
run_in_user_database(%{
|
||
|
DROP TABLE manolo_raster;
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
end
|
||
|
|
||
|
it 'should regenerate user tables with bad table_ids' do
|
||
|
run_in_user_database(%{
|
||
|
CREATE TABLE manoloescobar ("description" text);
|
||
|
SELECT * FROM CDB_CartodbfyTable('manoloescobar');
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
|
||
|
::Resque.expects(:enqueue).with(::Resque::UserDBJobs::UserDBMaintenance::LinkGhostTablesByUsername, user.username).never
|
||
|
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
user.tables.first.name.should == 'manoloescobar'
|
||
|
|
||
|
user_table = user.tables.first
|
||
|
original_oid = user_table.table_id
|
||
|
|
||
|
user_table.table_id = original_oid + 1
|
||
|
user_table.save
|
||
|
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
user.tables.first.name.should == 'manoloescobar'
|
||
|
|
||
|
user.tables.first.table_id.should == original_oid
|
||
|
|
||
|
run_in_user_database(%{
|
||
|
DROP TABLE manoloescobar;
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
end
|
||
|
|
||
|
it 'should preseve maps in drop create scenarios' do
|
||
|
run_in_user_database(%{
|
||
|
CREATE TABLE manoloescobar ("description" text);
|
||
|
SELECT * FROM CDB_CartodbfyTable('manoloescobar');
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
|
||
|
::Resque.expects(:enqueue).with(::Resque::UserDBJobs::UserDBMaintenance::LinkGhostTablesByUsername, user.username).never
|
||
|
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
|
||
|
original_user_table = user.tables.first
|
||
|
original_user_table.name.should == 'manoloescobar'
|
||
|
|
||
|
original_user_table_id = original_user_table.id
|
||
|
original_map_id = original_user_table.map.id
|
||
|
|
||
|
run_in_user_database(%{
|
||
|
DROP TABLE manoloescobar;
|
||
|
CREATE TABLE manoloescobar ("description" text);
|
||
|
SELECT * FROM CDB_CartodbfyTable('manoloescobar');
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
|
||
|
user.tables.first.id.should == original_user_table_id
|
||
|
user.tables.first.map.id.should == original_map_id
|
||
|
|
||
|
run_in_user_database(%{
|
||
|
DROP TABLE manoloescobar;
|
||
|
})
|
||
|
|
||
|
user.tables.count.should eq 1
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
end
|
||
|
|
||
|
it 'perform a successfully ghost tables execution when is called from LinkGhostTablesByUsername' do
|
||
|
Carto::GhostTablesManager.expects(:new).with(user.id).returns(ghost_tables_manager).once
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_synchronously).once
|
||
|
::Resque::UserDBJobs::UserDBMaintenance::LinkGhostTablesByUsername.perform(user.username)
|
||
|
end
|
||
|
|
||
|
it 'perform a successfully ghost tables execution when is called from LinkGhostTables' do
|
||
|
Carto::GhostTablesManager.expects(:new).with(user.id).returns(ghost_tables_manager).once
|
||
|
ghost_tables_manager.expects(:link_ghost_tables_synchronously).once
|
||
|
::Resque::UserDBJobs::UserDBMaintenance::LinkGhostTables.perform(user.id)
|
||
|
end
|
||
|
|
||
|
it 'should call the fail_function and execute sync twice because other worker tried to get the lock' do
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
main = Thread.new do
|
||
|
run_in_user_database(%{
|
||
|
CREATE TABLE manoloescobar ("description" text);
|
||
|
SELECT * FROM CDB_CartodbfyTable('manoloescobar');
|
||
|
})
|
||
|
rerun_func = lambda do
|
||
|
Carto::GhostTablesManager.new(user.id).send(:sync)
|
||
|
end
|
||
|
gtm = Carto::GhostTablesManager.new(user.id)
|
||
|
gtm.get_bolt.run_locked(fail_function: rerun_func) do
|
||
|
sleep(1)
|
||
|
Carto::GhostTablesManager.new(user.id).send(:sync)
|
||
|
end
|
||
|
end
|
||
|
thr = Thread.new do
|
||
|
run_in_user_database(%{
|
||
|
CREATE TABLE manoloescobar2 ("description" text);
|
||
|
SELECT * FROM CDB_CartodbfyTable('manoloescobar2');
|
||
|
})
|
||
|
Carto::GhostTablesManager.new(user.id).get_bolt.run_locked {}
|
||
|
end
|
||
|
thr.join
|
||
|
main.join
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
user.tables.count.should eq 2
|
||
|
|
||
|
run_in_user_database(%{
|
||
|
DROP TABLE manoloescobar;
|
||
|
DROP TABLE manoloescobar2;
|
||
|
})
|
||
|
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_false
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
user.tables.count.should eq 0
|
||
|
ghost_tables_manager.instance_eval { fetch_user_tables_synced_with_db? }.should be_true
|
||
|
end
|
||
|
|
||
|
it 'should backup visualizations before dropping a table' do
|
||
|
user_table = create(:carto_user_table, :full, user: user)
|
||
|
|
||
|
expect(Carto::VisualizationBackup.count).to eq(0)
|
||
|
|
||
|
run_in_user_database("ALTER TABLE #{user_table.name} DROP COLUMN cartodb_id")
|
||
|
ghost_tables_manager.link_ghost_tables_synchronously
|
||
|
|
||
|
expect(Carto::VisualizationBackup.count).to eq(1)
|
||
|
end
|
||
|
end
|
||
|
end
|