Просмотр исходного кода

HUE-2978: [metastore] Offer to delete partitions

Jenny Kim 10 лет назад
Родитель
Сommit
c82424e8d7

+ 13 - 0
apps/beeswax/src/beeswax/server/dbms.py

@@ -686,6 +686,19 @@ class HiveServer2Dbms(object):
     return self.client.get_table(db_name, table_name, partition_spec=partition_spec)
 
 
+  def drop_partitions(self, db_name, table_name, partition_specs, design):
+    hql = []
+
+    for partition_spec in partition_specs:
+        hql.append("ALTER TABLE `%s`.`%s` DROP IF EXISTS PARTITION (%s) PURGE" % (db_name, table_name, partition_spec))
+
+    query = hql_query(';'.join(hql), db_name)
+    design.data = query.dumps()
+    design.save()
+
+    return self.execute_query(query, design)
+
+
   def explain(self, query):
     return self.client.explain(query)
 

+ 83 - 1
apps/metastore/src/metastore/templates/describe_partitions.mako

@@ -62,9 +62,17 @@ ${ components.menubar() }
           </a>
           <div class="clearfix"></div>
 
+          <div class="actionbar-main" style="padding: 10px;">
+            <div class="actionbar-actions">
+              % if has_write_access:
+                <button id="dropBtn" class="btn toolbarBtn" title="${_('Delete the selected partitions')}" disabled="disabled"><i class="fa fa-trash-o"></i>  ${_('Drop')}</button>
+              % endif
+            </div>
+          </div>
 
           <table class="table table-striped table-condensed datatables" data-bind="visible: values().length > 0, style:{'opacity': isLoading() ? '.5': '1' }">
             <tr>
+              <th width="1%"><div class="hueCheckbox selectAll fa" data-selectables="tableCheck"></div></th>
               <!-- ko foreach: keys -->
               <th data-bind="text: $data"></th>
               <!-- /ko -->
@@ -72,6 +80,11 @@ ${ components.menubar() }
             </tr>
             <!-- ko foreach: values -->
             <tr>
+              <td data-row-selector-exclude="true" width="1%">
+                <div class="hueCheckbox partitionCheck fa"
+                       data-bind="attr:{'data-drop-name': partitionSpec}"
+                       data-row-selector-exclude="true"></div>
+              </td>
               <!-- ko foreach: $data.columns -->
               <td><a data-bind="attr:{'href': $parent.readUrl},text:$data"></a></td>
               <!-- /ko -->
@@ -84,6 +97,23 @@ ${ components.menubar() }
       </div>
     </div>
   </div>
+
+  <div id="dropPartition" class="modal hide fade">
+    <form id="dropPartitionForm" action="${ url('metastore:drop_partition', database=database, table=table.name) }" method="POST">
+      ${ csrf_token(request) | n,unicode }
+      <div class="modal-header">
+        <a href="#" class="close" data-dismiss="modal">&times;</a>
+        <h3 id="dropPartitionMessage">${_('Confirm action')}</h3>
+      </div>
+      <div class="modal-footer">
+        <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}" />
+        <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
+      </div>
+      <div class="hide">
+        <select name="partition_selection" data-bind="options: $root.availablePartitions, selectedOptions: $root.chosenPartitions" size="5" multiple="true"></select>
+      </div>
+    </form>
+  </div>
 </div>
 
 <link rel="stylesheet" href="${ static('metastore/css/metastore.css') }" type="text/css">
@@ -99,11 +129,18 @@ ${ components.menubar() }
     self.isLoading = ko.observable(false);
 
     self.sortDesc = ko.observable(true);
-    self.filters = ko.observableArray([])
+    self.filters = ko.observableArray([]);
 
     self.keys = ko.observableArray(partition_keys_json);
     self.values = ko.observableArray(partition_values_json);
 
+    var partition_specs = [];
+    $.each(partition_values_json, function (index, partition) {
+        partition_specs.push(partition.partitionSpec);
+    });
+    self.availablePartitions = ko.observableArray(partition_specs);
+    self.chosenPartitions = ko.observableArray([]);
+
     self.typeaheadValues = function (column) {
       var _vals = [];
       self.values().forEach(function (row) {
@@ -154,7 +191,52 @@ ${ components.menubar() }
 
   $(document).ready(function () {
     $("a[data-row-selector='true']").jHueRowSelector();
+
+    $(".selectAll").click(function () {
+      if ($(this).attr("checked")) {
+        $(this).removeAttr("checked").removeClass("fa-check");
+        $("." + $(this).data("selectables")).removeClass("fa-check").removeAttr("checked");
+      }
+      else {
+        $(this).attr("checked", "checked").addClass("fa-check");
+        $("." + $(this).data("selectables")).addClass("fa-check").attr("checked", "checked");
+      }
+      toggleActions();
+    });
+
+    $(".partitionCheck").click(function () {
+      if ($(this).attr("checked")) {
+        $(this).removeClass("fa-check").removeAttr("checked");
+      }
+      else {
+        $(this).addClass("fa-check").attr("checked", "checked");
+      }
+      $(".selectAll").removeAttr("checked").removeClass("fa-check");
+      toggleActions();
+    });
+
+    function toggleActions() {
+      $(".toolbarBtn").attr("disabled", "disabled");
+      var selector = $(".hueCheckbox[checked='checked']");
+      if (selector.length >= 1) {
+        $("#dropBtn").removeAttr("disabled");
+      }
+    }
+
+    $("#dropBtn").click(function () {
+      $.getJSON("${ url('metastore:drop_partition', database=database, table=table.name) }", function (data) {
+        $("#dropPartitionMessage").text(data.title);
+      });
+      var _tempList = [];
+      $(".hueCheckbox[checked='checked']").each(function (index) {
+        _tempList.push($(this).data("drop-name"));
+      });
+      viewModel.chosenPartitions.removeAll();
+      viewModel.chosenPartitions(_tempList);
+      $("#dropPartition").modal("show");
+    });
   });
+
 </script>
 
 ${ commonfooter(messages) | n,unicode }

+ 8 - 0
apps/metastore/src/metastore/tests.py

@@ -204,6 +204,14 @@ class TestMetastoreWithHadoop(BeeswaxSampleProvider):
     filebrowser_path = urllib.unquote(reverse("filebrowser.views.view", kwargs={'path': path}))
     assert_equal(response.request['PATH_INFO'], filebrowser_path)
 
+  def test_drop_partition(self):
+    partition_spec = "baz='baz_one',boom='boom_two'"
+    self.client.post("/metastore/table/%s/test_partitions/partitions/drop" % self.db_name, {'partition_selection': [partition_spec]}, follow=True)
+    query = QueryHistory.objects.latest('id')
+    assert_equal_mod_whitespace("ALTER TABLE `%s`.`test_partitions` DROP IF EXISTS PARTITION (%s) PURGE" % (self.db_name, partition_spec), query.query)
+    response = self.client.get("/metastore/table/%s/test_partitions/partitions" % self.db_name)
+    assert_false("baz_one" in response.content)
+
   def test_drop_multi_tables(self):
     hql = """
       CREATE TABLE test_drop_1 (a int);

+ 1 - 0
apps/metastore/src/metastore/urls.py

@@ -33,4 +33,5 @@ urlpatterns = patterns('metastore.views',
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/partitions$', 'describe_partitions', name='describe_partitions'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/partitions/(?P<partition_spec>.+?)/read$', 'read_partition', name='read_partition'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)/partitions/(?P<partition_spec>.+?)/browse$', 'browse_partition', name='browse_partition'),
+  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/partitions/drop$', 'drop_partition', name='drop_partition'),
 )

+ 27 - 3
apps/metastore/src/metastore/views.py

@@ -19,10 +19,11 @@ import json
 import logging
 import urllib
 
+from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.utils.functional import wraps
 from django.utils.translation import ugettext as _
-from django.core.urlresolvers import reverse
+from django.views.decorators.http import require_http_methods
 
 from desktop.context_processors import get_app_name
 from desktop.lib.django_util import JsonResponse, render
@@ -336,6 +337,7 @@ def describe_partitions(request, database, table):
   for partition in partitions:
     massaged_partitions.append({
       'columns': partition.values,
+      'partitionSpec': partition.partition_spec,
       'readUrl': reverse('metastore:read_partition', kwargs={'database': database, 'table': table_obj.name,
                                                              'partition_spec': urllib.quote(partition.partition_spec)}),
       'browseUrl': reverse('metastore:browse_partition', kwargs={'database': database, 'table': table_obj.name,
@@ -365,11 +367,11 @@ def describe_partitions(request, database, table):
         'partitions': partitions,
         'partition_keys_json': json.dumps([partition.name for partition in table_obj.partition_keys]),
         'partition_values_json': json.dumps(massaged_partitions),
-        'request': request
+        'request': request,
+        'has_write_access': has_write_access(request.user)
     })
 
 
-
 def browse_partition(request, database, table, partition_spec):
   db = dbms.get(request.user)
   try:
@@ -391,6 +393,28 @@ def read_partition(request, database, table, partition_spec):
   except Exception, e:
     raise PopupException(_('Cannot read partition'), detail=e.message)
 
+@require_http_methods(["GET", "POST"])
+@check_has_write_access_permission
+def drop_partition(request, database, table):
+  db = dbms.get(request.user)
+
+  if request.method == 'POST':
+    partition_specs = request.POST.getlist('partition_selection')
+    partition_specs = [spec for spec in partition_specs]
+    try:
+      design = SavedQuery.create_empty(app_name='beeswax', owner=request.user, data=hql_query('').dumps())
+      query_history = db.drop_partitions(database, table, partition_specs, design)
+      url = reverse('beeswax:watch_query_history', kwargs={'query_history_id': query_history.id}) + '?on_success_url=' + \
+            reverse('metastore:describe_partitions', kwargs={'database': database, 'table': table})
+      return redirect(url)
+    except Exception, ex:
+      error_message, log = dbms.expand_exception(ex, db)
+      error = _("Failed to remove %(partition)s.  Error: %(error)s") % {'partition': '\n'.join(partition_specs), 'error': error_message}
+      raise PopupException(error, title=_("Hive Error"), detail=log)
+  else:
+    title = _("Do you really want to delete the partition(s)?")
+    return render('confirm.mako', request, {'url': request.path, 'title': title})
+
 
 def has_write_access(user):
   return user.is_superuser or user.has_hue_permission(action="write", app=DJANGO_APPS[0])