Added support to compare schemas and databases in schema diff. Fixes #5891

This commit is contained in:
Akshay Joshi
2020-10-27 16:36:10 +05:30
parent 5284a1c66b
commit b76bb58378
34 changed files with 468 additions and 62 deletions

View File

@@ -30,6 +30,7 @@ from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS,\
from sqlalchemy import or_
MODULE_NAME = 'schema_diff'
COMPARE_MSG = gettext("Comparing objects...")
class SchemaDiffModule(PgAdminModule):
@@ -64,7 +65,9 @@ class SchemaDiffModule(PgAdminModule):
'schema_diff.panel',
'schema_diff.servers',
'schema_diff.databases',
'schema_diff.compare',
'schema_diff.schemas',
'schema_diff.compare_database',
'schema_diff.compare_schema',
'schema_diff.poll',
'schema_diff.ddl_compare',
'schema_diff.connect_server',
@@ -430,32 +433,53 @@ def databases(sid):
@blueprint.route(
'/compare/<int:trans_id>/<int:source_sid>/<int:source_did>/'
'<int:target_sid>/<int:target_did>',
'/schemas/<int:sid>/<int:did>',
methods=["GET"],
endpoint="compare"
endpoint="schemas"
)
@login_required
def compare(trans_id, source_sid, source_did, target_sid, target_did):
def schemas(sid, did):
"""
This function will compare the two schemas.
This function will return the list of schemas for the specified
server id and database id.
"""
# Check the transaction and connection status
res = []
try:
schemas = get_schemas(sid, did)
if schemas is not None:
for sch in schemas:
res.append({
"value": sch['_id'],
"label": sch['label'],
"_id": sch['_id'],
"image": sch['icon'],
})
except Exception as e:
app.logger.exception(e)
return make_json_response(data=res)
@blueprint.route(
'/compare_database/<int:trans_id>/<int:source_sid>/<int:source_did>/'
'<int:target_sid>/<int:target_did>',
methods=["GET"],
endpoint="compare_database"
)
@login_required
def compare_database(trans_id, source_sid, source_did, target_sid, target_did):
"""
This function will compare the two databases.
"""
# Check the pre validation before compare
status, error_msg, diff_model_obj, session_obj = \
check_transaction_status(trans_id)
if error_msg == ERROR_MSG_TRANS_ID_NOT_FOUND:
return make_json_response(success=0, errormsg=error_msg, status=404)
# Server version compatibility check
status, msg = check_version_compatibility(source_sid, target_sid)
compare_pre_validation(trans_id, source_sid, target_sid)
if not status:
return make_json_response(success=0, errormsg=msg, status=428)
return error_msg
comparison_result = []
diff_model_obj.set_comparison_info(gettext("Comparing objects..."), 0)
diff_model_obj.set_comparison_info(COMPARE_MSG, 0)
update_session_diff_transaction(trans_id, session_obj,
diff_model_obj)
@@ -552,6 +576,60 @@ def compare(trans_id, source_sid, source_did, target_sid, target_did):
return make_json_response(data=comparison_result)
@blueprint.route(
'/compare_schema/<int:trans_id>/<int:source_sid>/<int:source_did>/'
'<int:source_scid>/<int:target_sid>/<int:target_did>/<int:target_scid>',
methods=["GET"],
endpoint="compare_schema"
)
@login_required
def compare_schema(trans_id, source_sid, source_did, source_scid,
target_sid, target_did, target_scid):
"""
This function will compare the two schema.
"""
# Check the pre validation before compare
status, error_msg, diff_model_obj, session_obj = \
compare_pre_validation(trans_id, source_sid, target_sid)
if not status:
return error_msg
comparison_result = []
diff_model_obj.set_comparison_info(COMPARE_MSG, 0)
update_session_diff_transaction(trans_id, session_obj,
diff_model_obj)
try:
all_registered_nodes = SchemaDiffRegistry.get_registered_nodes()
node_percent = round(100 / len(all_registered_nodes))
total_percent = 0
comparison_schema_result, total_percent = \
compare_schema_objects(
trans_id=trans_id, session_obj=session_obj,
source_sid=source_sid, source_did=source_did,
source_scid=source_scid, target_sid=target_sid,
target_did=target_did, target_scid=target_scid,
schema_name=gettext('Schema Objects'),
diff_model_obj=diff_model_obj,
total_percent=total_percent,
node_percent=node_percent)
comparison_result = \
comparison_result + comparison_schema_result
msg = gettext("Successfully compare the specified schemas.")
total_percent = 100
diff_model_obj.set_comparison_info(msg, total_percent)
# Update the message and total percentage done in session object
update_session_diff_transaction(trans_id, session_obj, diff_model_obj)
except Exception as e:
app.logger.exception(e)
return make_json_response(data=comparison_result)
@blueprint.route(
'/poll/<int:trans_id>', methods=["GET"], endpoint="poll"
)
@@ -573,7 +651,7 @@ def poll(trans_id):
msg, diff_percentage = diff_model_obj.get_comparison_info()
if diff_percentage == 100:
diff_model_obj.set_comparison_info(gettext("Comparing objects..."), 0)
diff_model_obj.set_comparison_info(COMPARE_MSG, 0)
update_session_diff_transaction(trans_id, session_obj,
diff_model_obj)
@@ -755,9 +833,13 @@ def compare_schema_objects(**kwargs):
for node_name, node_view in all_registered_nodes.items():
view = SchemaDiffRegistry.get_node_view(node_name)
if hasattr(view, 'compare'):
msg = gettext('Comparing {0} of schema \'{1}\''). \
format(gettext(view.blueprint.collection_label),
gettext(schema_name))
if schema_name == 'Schema Objects':
msg = gettext('Comparing {0} '). \
format(gettext(view.blueprint.collection_label))
else:
msg = gettext('Comparing {0} of schema \'{1}\''). \
format(gettext(view.blueprint.collection_label),
gettext(schema_name))
app.logger.debug(msg)
diff_model_obj.set_comparison_info(msg, total_percent)
# Update the message and total percentage in session object
@@ -832,3 +914,28 @@ def fetch_compare_schemas(source_sid, source_did, target_sid, target_did):
'in_both_database': in_both_database}
return schema_result
def compare_pre_validation(trans_id, source_sid, target_sid):
"""
This function is used to validate transaction id and version compatibility
:param trans_id:
:param source_sid:
:param target_sid:
:return:
"""
status, error_msg, diff_model_obj, session_obj = \
check_transaction_status(trans_id)
if error_msg == ERROR_MSG_TRANS_ID_NOT_FOUND:
res = make_json_response(success=0, errormsg=error_msg, status=404)
return False, res, None, None
# Server version compatibility check
status, msg = check_version_compatibility(source_sid, target_sid)
if not status:
res = make_json_response(success=0, errormsg=msg, status=428)
return False, res, None, None
return True, '', diff_model_obj, session_obj

View File

@@ -12,12 +12,13 @@
from flask import render_template
from pgadmin.utils.driver import get_driver
from config import PG_DEFAULT_DRIVER
from pgadmin.utils.ajax import internal_server_error
from pgadmin.tools.schema_diff.directory_compare import compare_dictionaries
class SchemaDiffObjectCompare:
keys_to_ignore = ['oid', 'oid-2', 'is_sys_obj']
keys_to_ignore = ['oid', 'oid-2', 'is_sys_obj', 'schema']
@staticmethod
def get_schema(sid, did, scid):
@@ -62,6 +63,12 @@ class SchemaDiffObjectCompare:
source = {}
target = {}
status, target_schema = self.get_schema(kwargs.get('target_sid'),
kwargs.get('target_did'),
kwargs.get('target_scid'))
if not status:
return internal_server_error(errormsg=target_schema)
if group_name == 'Database Objects':
source = self.fetch_objects_to_compare(**source_params)
target = self.fetch_objects_to_compare(**target_params)
@@ -83,6 +90,7 @@ class SchemaDiffObjectCompare:
return compare_dictionaries(view_object=self,
source_params=source_params,
target_params=target_params,
target_schema=target_schema,
source_dict=source,
target_dict=target,
node=self.node_type,

View File

@@ -36,6 +36,7 @@ def _get_source_list(**kwargs):
node_label = kwargs.get('node_label')
group_name = kwargs.get('group_name')
source_schema_name = kwargs.get('source_schema_name')
target_schema = kwargs.get('target_schema')
global count
source_only = []
@@ -50,6 +51,7 @@ def _get_source_list(**kwargs):
temp_src_params['json_resp'] = False
source_ddl = \
view_object.get_sql_from_table_diff(**temp_src_params)
temp_src_params.update({'target_schema': target_schema})
diff_ddl = view_object.get_sql_from_table_diff(**temp_src_params)
source_dependencies = \
view_object.get_table_submodules_dependencies(
@@ -65,6 +67,7 @@ def _get_source_list(**kwargs):
temp_src_params['fsid'] = source_dict[item]['fsid']
source_ddl = view_object.get_sql_from_diff(**temp_src_params)
temp_src_params.update({'target_schema': target_schema})
diff_ddl = view_object.get_sql_from_diff(**temp_src_params)
source_dependencies = view_object.get_dependencies(
view_object.conn, source_object_id, where=None,
@@ -223,6 +226,7 @@ def _get_identical_and_different_list(intersect_keys, source_dict, target_dict,
source_params = kwargs['source_params']
target_params = kwargs['target_params']
group_name = kwargs['group_name']
target_schema = kwargs.get('target_schema')
for key in intersect_keys:
source_object_id, target_object_id = \
get_source_target_oid(source_dict, target_dict, key)
@@ -281,7 +285,8 @@ def _get_identical_and_different_list(intersect_keys, source_dict, target_dict,
diff_ddl = view_object.get_sql_from_submodule_diff(
source_params=temp_src_params,
target_params=temp_tgt_params,
source=dict1[key], target=dict2[key], diff_dict=diff_dict)
source=dict1[key], target=dict2[key], diff_dict=diff_dict,
target_schema=target_schema)
else:
temp_src_params = copy.deepcopy(source_params)
temp_tgt_params = copy.deepcopy(target_params)
@@ -303,7 +308,7 @@ def _get_identical_and_different_list(intersect_keys, source_dict, target_dict,
show_system_objects=None, is_schema_diff=True)
target_ddl = view_object.get_sql_from_diff(**temp_tgt_params)
temp_tgt_params.update(
{'data': diff_dict})
{'data': diff_dict, 'target_schema': target_schema})
diff_ddl = view_object.get_sql_from_diff(**temp_tgt_params)
different.append({
@@ -336,6 +341,7 @@ def compare_dictionaries(**kwargs):
view_object = kwargs.get('view_object')
source_params = kwargs.get('source_params')
target_params = kwargs.get('target_params')
target_schema = kwargs.get('target_schema')
group_name = kwargs.get('group_name')
source_dict = kwargs.get('source_dict')
target_dict = kwargs.get('target_dict')
@@ -364,7 +370,8 @@ def compare_dictionaries(**kwargs):
view_object=view_object,
node_label=node_label,
group_name=group_name,
source_schema_name=source_schema_name)
source_schema_name=source_schema_name,
target_schema=target_schema)
target_only = []
# Keys that are available in target and missing in source.
@@ -389,7 +396,8 @@ def compare_dictionaries(**kwargs):
"ignore_keys": ignore_keys,
"source_params": source_params,
"target_params": target_params,
"group_name": group_name
"group_name": group_name,
"target_schema": target_schema
}
identical, different = _get_identical_and_different_list(
@@ -507,7 +515,8 @@ def are_dictionaries_identical(source_dict, target_dict, ignore_keys):
current_app.logger.debug(
"Schema Diff: Object name: '{0}', Source Value: '{1}', "
"Target Value: '{2}', Key: '{3}'".format(
source_dict['name'], source_value, target_value, key))
source_dict['name'] if 'name' in source_dict else '',
source_value, target_value, key))
return False
return True

View File

@@ -105,7 +105,7 @@ let SchemaDiffSelect2Control =
controlsClassName: 'pgadmin-controls pg-el-sm-11 pg-el-12',
}),
className: function() {
return 'pgadmin-controls pg-el-sm-6';
return 'pgadmin-controls pg-el-sm-4';
},
events: {
'focus select': 'clearInvalid',

View File

@@ -43,8 +43,10 @@ export default class SchemaDiffUI {
this.model = new Backbone.Model({
source_sid: undefined,
source_did: undefined,
source_scid: undefined,
target_sid: undefined,
target_did: undefined,
target_scid: undefined,
source_ddl: undefined,
target_ddl: undefined,
diff_ddl: undefined,
@@ -162,7 +164,12 @@ export default class SchemaDiffUI {
url_params[key] = parseInt(val, 10);
});
var baseUrl = url_for('schema_diff.compare', url_params);
var baseUrl = url_for('schema_diff.compare_database', url_params);
// If compare two schema then change the base url
if (url_params['source_scid'] != '' && !_.isUndefined(url_params['source_scid']) &&
url_params['target_scid'] != '' && !_.isUndefined(url_params['target_scid'])) {
baseUrl = url_for('schema_diff.compare_schema', url_params);
}
self.model.set({
'source_ddl': undefined,
@@ -305,7 +312,7 @@ export default class SchemaDiffUI {
// Format Schema object title with appropriate icon
var formatColumnTitle = function (row, cell, value, columnDef, dataContext) {
let icon = 'icon-' + dataContext.type;
return '<i class="ml-3 wcTabIcon '+ icon +'"></i><span>' + value + '</span>';
return '<i class="ml-2 wcTabIcon '+ icon +'"></i><span>' + value + '</span>';
};
// Grid Columns
@@ -648,6 +655,30 @@ export default class SchemaDiffUI {
connect: function() {
self.connect_database(this.model.get('source_sid'), arguments[0], arguments[1]);
},
}, {
name: 'source_scid',
control: SchemaDiffSelect2Control,
group: 'source',
deps: ['source_sid', 'source_did'],
url: function() {
if (this.get('source_sid') && this.get('source_did'))
return url_for('schema_diff.schemas', {'sid': this.get('source_sid'), 'did': this.get('source_did')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select schema...'),
},
disabled: function(m) {
if (!_.isUndefined(m.get('source_did')) && !_.isNull(m.get('source_did'))
&& m.get('source_did') !== '') {
return false;
}
setTimeout(function() {
m.set('source_scid', undefined);
}, 10);
return true;
},
}, {
name: 'target_sid', label: false,
control: SchemaDiffSelect2Control,
@@ -708,6 +739,30 @@ export default class SchemaDiffUI {
connect: function() {
self.connect_database(this.model.get('target_sid'), arguments[0], arguments[1]);
},
}, {
name: 'target_scid',
control: SchemaDiffSelect2Control,
group: 'target',
deps: ['target_sid', 'target_did'],
url: function() {
if (this.get('target_sid') && this.get('target_did'))
return url_for('schema_diff.schemas', {'sid': this.get('target_sid'), 'did': this.get('target_did')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select schema...'),
},
disabled: function(m) {
if (!_.isUndefined(m.get('target_did')) && !_.isNull(m.get('target_did'))
&& m.get('target_did') !== '') {
return false;
}
setTimeout(function() {
m.set('target_scid', undefined);
}, 10);
return true;
},
}],
});
@@ -739,7 +794,9 @@ export default class SchemaDiffUI {
footer_panel.$container.find('#schema-diff-ddl-comp').append(self.footer.render().$el);
header_panel.$container.find('#schema-diff-grid').append(`<div class='obj_properties container-fluid'>
<div class='pg-panel-message'>` + gettext('Select the server and database for the source and target and click <strong>Compare</strong> to compare them.') + '</div></div>');
<div class='pg-panel-message'>` + gettext('<strong>Database Compare:</strong> Select the server and database for the source and target and Click <strong>Compare</strong>.') +
gettext('</br><strong>Schema Compare:</strong> Select the server, database and schema for the source and target and Click <strong>Compare</strong>.') +
gettext('</br><strong>Note:</strong> The dependencies will not be resolved in the Schema comparison.') + '</div></div>');
self.grid_width = $('#schema-diff-grid').width();
self.grid_height = this.panel_obj.height();

View File

@@ -25,7 +25,7 @@ class SchemaDiffTestCase(BaseTestGenerator):
scenarios = [
# Fetching default URL for database node.
('Schema diff comparison', dict(
url='schema_diff/compare/{0}/{1}/{2}/{3}/{4}'))
url='schema_diff/compare_database/{0}/{1}/{2}/{3}/{4}'))
]
def setUp(self):