Browse Source

HUE-159 [beeswax] Support multiple statements per query

Queries can now be composed of multiple statements separated by some semicolon.
As in the Hive shell, '\059' can be use in case of conflicts.
When the statement does not create a result set (e.g. CREATE, LOAD, DROP, ALTER)
the watch query page will just move automatically to the next statement when
the current one is finished. Moving to the next one loses the log of the previous one.
If the query has a dataset like with a SELECT, the result page will be showned at the
end and a 'continue' button in a note will invite the user to continue the execution
of the following statements.
If a statement has an error, similarly to a statement with a dataset, the result page
is showned and the user can also skip.
3 tests added for checking multi-statement queries with datasets, no datasets or a mix.
Query box is higher now and with a note about this new functionality.
Now pressing 'enter' on execute button will execute the query.
statement_number is a new field are added in QueryHistory in order to persist which
statement is currently running.
Romain Rigaux 13 years ago
parent
commit
5a945b0dae

+ 13 - 0
apps/beeswax/src/beeswax/design.py

@@ -152,6 +152,19 @@ class HQLdesign(object):
   def get_query(self):
     return self._data_dict["query"]
 
+  @property
+  def statement_count(self):
+    return len(self.statements)
+
+  def get_query_statement(self, n=0):
+    return self.statements[n]
+
+  @property
+  def statements(self):
+    hql_query = _strip_trailing_semicolon(self.hql_query)
+    return [statement.strip() for statement in hql_query.split(';')]
+
+
 def normalize_form_dict(form, attr_list):
   """
   normalize_form_dict(form, attr_list) -> A dictionary of (attr, value)

+ 0 - 4
apps/beeswax/src/beeswax/forms.py

@@ -25,7 +25,6 @@ from desktop.lib.django_forms import ChoiceOrOtherField, MultiForm, SubmitButton
 from filebrowser.forms import PathField
 
 from beeswax import common
-from beeswax.design import _strip_trailing_semicolon
 from beeswax.server.dbms import NoSuchObjectException
 from beeswax.models import SavedQuery
 
@@ -146,9 +145,6 @@ class HQLForm(forms.Form):
                            initial=0,
                            widget=forms.widgets.Select(attrs={'class': 'span6'}))
 
-  def clean_query(self):
-    return _strip_trailing_semicolon(self.cleaned_data['query'])
-
 
 class FunctionForm(forms.Form):
   name = forms.CharField(required=True)

+ 107 - 0
apps/beeswax/src/beeswax/migrations/0005_auto__add_field_queryhistory_statement_number.py

@@ -0,0 +1,107 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+
+        # Adding field 'QueryHistory.statement_number'
+        db.add_column('beeswax_queryhistory', 'statement_number', self.gf('django.db.models.fields.SmallIntegerField')(default=0), keep_default=False)
+
+
+    def backwards(self, orm):
+
+        # Deleting field 'QueryHistory.statement_number'
+        db.delete_column('beeswax_queryhistory', 'statement_number')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'beeswax.metainstall': {
+            'Meta': {'object_name': 'MetaInstall'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'installed_example': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'})
+        },
+        'beeswax.queryhistory': {
+            'Meta': {'object_name': 'QueryHistory'},
+            'design': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['beeswax.SavedQuery']", 'null': 'True'}),
+            'has_results': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_state': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'log_context': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+            'modified_row_count': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'notify': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'operation_type': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'query': ('django.db.models.fields.TextField', [], {}),
+            'server_guid': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '1024', 'null': 'True'}),
+            'server_host': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}),
+            'server_id': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+            'server_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}),
+            'server_port': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+            'server_type': ('django.db.models.fields.CharField', [], {'default': "'beeswax'", 'max_length': '128'}),
+            'statement_number': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+            'submission_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+        },
+        'beeswax.savedquery': {
+            'Meta': {'object_name': 'SavedQuery'},
+            'data': ('django.db.models.fields.TextField', [], {'max_length': '65536'}),
+            'desc': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_auto': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True', 'blank': 'True'}),
+            'mtime': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'type': ('django.db.models.fields.IntegerField', [], {})
+        },
+        'beeswax.session': {
+            'Meta': {'object_name': 'Session'},
+            'guid': ('django.db.models.fields.TextField', [], {'max_length': "'100'"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_used': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'secret': ('django.db.models.fields.TextField', [], {'max_length': "'100'"}),
+            'server_protocol_version': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+            'status_code': ('django.db.models.fields.PositiveSmallIntegerField', [], {})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['beeswax']

+ 41 - 0
apps/beeswax/src/beeswax/models.py

@@ -30,11 +30,13 @@ from enum import Enum
 from desktop.lib.exceptions_renderable import PopupException
 
 from beeswax.conf import SERVER_INTERFACE
+from beeswax.design import HQLdesign
 from beeswaxd.ttypes import QueryHandle as BeeswaxdQueryHandle, QueryState
 from cli_service.ttypes import TSessionHandle, THandleIdentifier,\
   TOperationState, TOperationHandle, TOperationType
 
 
+
 LOG = logging.getLogger(__name__)
 
 QUERY_SUBMISSION_TIMEOUT = datetime.timedelta(0, 60 * 60)               # 1 hour
@@ -57,8 +59,10 @@ class QueryHistory(models.Model):
   last_state = models.IntegerField(db_index=True)
   has_results = models.BooleanField(default=False)          # If true, this query will eventually return tabular results.
   submission_date = models.DateTimeField(auto_now_add=True)
+  # In case of multi statements in a query, these are the id of the currently running statement
   server_id = models.CharField(max_length=1024, null=True)  # Aka secret, only query in the "submitted" state is allowed to have no server_id
   server_guid = models.CharField(max_length=1024, null=True, default=None)
+  statement_number = models.SmallIntegerField(default=0)    # The index of the currently running statement
   operation_type = models.SmallIntegerField(null=True)
   modified_row_count = models.FloatField(null=True)
   log_context = models.CharField(max_length=1024, null=True)
@@ -101,6 +105,20 @@ class QueryHistory(models.Model):
                     [self.server_name, self.server_host, self.server_port, self.server_type]))
 
 
+  def get_current_statement(self):
+    if self.design is not None:
+      design = self.design.get_design()
+      return design.get_query_statement(self.statement_number)
+    else:
+      return self.query
+
+  def is_finished(self):
+    if self.design is not None:
+      design = self.design.get_design()
+      return self.is_success() and self.statement_number + 1 == design.statement_count
+    else:
+      return self.is_success()
+
   def is_running(self):
     return self.last_state in (QueryHistory.STATE.running.index, QueryHistory.STATE.submitted.index)
 
@@ -120,6 +138,20 @@ class QueryHistory(models.Model):
     self.last_state = QueryHistory.STATE.available.index
 
 
+def make_query_context(type, info):
+  """
+  ``type`` is one of "table" and "design", and ``info`` is the table name or design id.
+  Returns a value suitable for GET param.
+  """
+  if type == 'table':
+    return "%s:%s" % (type, info)
+  elif type == 'design':
+    # Use int() to validate that info is a number
+    return "%s:%s" % (type, int(info))
+  LOG.error("Invalid query context type: %s" % (type,))
+  return ''                                     # Empty string is safer than None
+
+
 class HiveServerQueryHistory(QueryHistory):
   # Map from (thrift) server state
   STATE_MAP = {
@@ -235,6 +267,9 @@ class SavedQuery(models.Model):
   class Meta:
     ordering = ['-mtime']
 
+  def get_design(self):
+    return HQLdesign.loads(self.data)
+
   def clone(self):
     """clone() -> A new SavedQuery with a deep copy of the same data"""
     design = SavedQuery(type=self.type, owner=self.owner)
@@ -275,6 +310,12 @@ class SavedQuery(models.Model):
   def __str__(self):
     return '%s %s' % (self.name, self.owner)
 
+  def get_query_context(self):
+    try:
+      return make_query_context('design', self.id)
+    except:
+      return ""
+
 
 class SessionManager(models.Manager):
   def get_session(self, user):

+ 10 - 5
apps/beeswax/src/beeswax/server/beeswax_lib.py

@@ -16,6 +16,7 @@
 # limitations under the License.
 
 import logging
+import re
 import thrift
 
 from django.utils.encoding import smart_str, force_unicode
@@ -100,6 +101,7 @@ class BeeswaxDataTable(DataTable):
 
 
 class BeeswaxClient:
+  NO_RESULT_SET_RE = re.compile('DROP|CREATE|ALTER|LOAD', re.IGNORECASE)
 
   def __init__(self, query_server, user):
     self.user = user
@@ -107,7 +109,7 @@ class BeeswaxClient:
     self.db_client = self.db_client(query_server)
     self.meta_client = self.meta_client()
 
-  def make_query(self, hql_query):
+  def make_query(self, hql_query, statement=0):
     # HUE-535 without having to modify Beeswaxd, add 'use database' as first option
     if self.query_server['server_name'] == 'impala':
       configuration = [','.join(['%(key)s=%(value)s' % setting for setting in hql_query.settings])]
@@ -115,7 +117,8 @@ class BeeswaxClient:
       configuration = ['use ' + hql_query.query.get('database', 'default')]
       configuration.extend(hql_query.get_configuration())
 
-    thrift_query = BeeswaxService.Query(query=hql_query.query['query'], configuration=configuration)
+    query_statement = hql_query.get_query_statement(statement)
+    thrift_query = BeeswaxService.Query(query=query_statement, configuration=configuration)
     thrift_query.hadoop_user = self.user.username
     return thrift_query
 
@@ -136,10 +139,12 @@ class BeeswaxClient:
     return BeeswaxTable(table)
 
 
-  def query(self, query):
-    thrift_query = self.make_query(query)
+  def query(self, query, statement=0):
+    thrift_query = self.make_query(query, statement)
     handle = self.db_client.query(thrift_query)
-    return BeeswaxQueryHandle(secret=handle.id, has_result_set=True, log_context=handle.log_context)
+    # Fake has_result_set
+    has_result_set = not BeeswaxClient.NO_RESULT_SET_RE.match(thrift_query.query) is not None
+    return BeeswaxQueryHandle(secret=handle.id, has_result_set=has_result_set, log_context=handle.log_context)
 
 
   def fetch(self, handle, start_over=True, rows=-1):

+ 27 - 17
apps/beeswax/src/beeswax/server/dbms.py

@@ -174,31 +174,39 @@ class Dbms:
       curr = time.time()
     return None
 
+  def execute_next_statement(self, query_history):
+    query_history.statement_number += 1
+    query_history.last_state = QueryHistory.STATE.submitted.index
+    query_history.save()
+    query = query_history.design.get_design()
+    return self.execute_and_watch(query, query_history=query_history)
 
-  def execute_and_watch(self, query, design=None):
+  def execute_and_watch(self, query, design=None, query_history=None):
     """
     Run query and return a QueryHistory object in order to see its progress on a Web page.
     """
-    query_statement = query.query['query']
-    query_history = QueryHistory.build(
-                                owner=self.client.user,
-                                query=query_statement,
-                                server_host='%(server_host)s' % self.client.query_server,
-                                server_port='%(server_port)d' % self.client.query_server,
-                                server_name='%(server_name)s' % self.client.query_server,
-                                server_type=self.server_type,
-                                last_state=QueryHistory.STATE.submitted.index,
-                                design=design,
-                                notify=query.query.get('email_notify', False))
-    query_history.save()
-
-    LOG.debug("Made new QueryHistory id %s user %s query: %s..." % (query_history.id, self.client.user, query_history.query[:25]))
+    hql_query = query.hql_query
+    if query_history is None:
+      query_history = QueryHistory.build(
+                                  owner=self.client.user,
+                                  query=hql_query,
+                                  server_host='%(server_host)s' % self.client.query_server,
+                                  server_port='%(server_port)d' % self.client.query_server,
+                                  server_name='%(server_name)s' % self.client.query_server,
+                                  server_type=self.server_type,
+                                  last_state=QueryHistory.STATE.submitted.index,
+                                  design=design,
+                                  notify=query.query.get('email_notify', False),
+                                  statement_number=0)
+      query_history.save()
+
+      LOG.debug("Made new QueryHistory id %s user %s query: %s..." % (query_history.id, self.client.user, query_history.query[:25]))
 
     try:
-      handle = self.client.query(query)
+      handle = self.client.query(query, query_history.statement_number)
       if not handle.is_valid():
         msg = _("Server returning invalid handle for query id %(id)d [%(query)s]...") % \
-              {'id': query_history.id, 'query': query_statement[:40]}
+              {'id': query_history.id, 'query': query[:40]}
         raise BeeswaxException(msg)
     except BeeswaxException, ex: # TODO HS2
       LOG.exception(ex)
@@ -219,6 +227,8 @@ class Dbms:
     query_history.set_to_running()
     query_history.save()
 
+    LOG.debug("Updated QueryHistory id %s user %s statement_number: %s" % (query_history.id, self.client.user, query_history.statement_number))
+
     return query_history
 
 

+ 5 - 4
apps/beeswax/src/beeswax/server/hive_server2_lib.py

@@ -315,8 +315,9 @@ class HiveServerClient:
     return HiveServerDataTable(results, schema)
 
 
-  def execute_async_query(self, query):
-    return self.execute_async_statement(statement=query.query['query'], conf_overlay={})
+  def execute_async_query(self, query, statement=0):
+    query_statement = query.get_query_statement(statement)
+    return self.execute_async_statement(statement=query_statement, conf_overlay={})
 
 
   def execute_statement(self, statement, conf_overlay=None, max_rows=100):
@@ -395,8 +396,8 @@ class HiveServerClientCompatible:
     self.query_server = client.query_server
 
 
-  def query(self, query):
-    return self._client.execute_async_query(query)
+  def query(self, query, statement=0):
+    return self._client.execute_async_query(query, statement)
 
 
   def get_state(self, handle):

+ 1 - 1
apps/beeswax/src/beeswax/templates/create_table_manually.mako

@@ -131,7 +131,7 @@ ${layout.menubar(section='tables')}
                       ${comps.field(table_form["field_terminator"], render_default=True)}
                       <span  class="help-inline error-inline hide">${_('This field is required. Spaces are not allowed. Terminator must be exactly one character.')}</span>
                       <span class="help-block">
-                          ${_('Enter the column delimiter.  Must be a single character.  Use syntax like "\001" or "\t" for special characters.')}
+                          ${_('Enter the column delimiter. Must be a single character. Use syntax like "\\001" or "\\t" for special characters.')}
                       </span>
                   </div>
               </div>

+ 18 - 5
apps/beeswax/src/beeswax/templates/execute.mako

@@ -33,7 +33,7 @@
         </div>
     % endif
 
-    <textarea class="span9" rows="18" placeholder="${_('Example: SELECT * FROM tablename')}" name="${form.query["query"].html_name}" id="queryField">${extract_field_data(form.query["query"]) or ''}</textarea>
+    <textarea class="span9" rows="35" placeholder="${_('Example: SELECT * FROM tablename')}" name="${form.query["query"].html_name}" id="queryField">${extract_field_data(form.query["query"]) or ''}</textarea>
 
     <div id="validationResults">
     % if len(form.query["query"].errors):
@@ -216,9 +216,9 @@ ${layout.menubar(section='query')}
                             % endif
                           ">${_('Email Notification')}</li>
                           <li
-	                        % if app_name == 'impala':
-	                            class="hide"
-	                        % endif
+                            % if app_name == 'impala':
+                                class="hide"
+                            % endif
                           >
                             <label class="checkbox" rel="tooltip" data-original-title="${_("If checked, you will receive an email notification when the query completes.")}">
                                 <input type="checkbox" id="id_${form.query["email_notify"].html_name | n}" name="${form.query["email_notify"].html_name | n}" ${extract_field_data(form.query["email_notify"]) and "CHECKED" or ""}/>
@@ -230,6 +230,12 @@ ${layout.menubar(section='query')}
                     <input type="hidden" name="${form.query["query"].html_name | n}" class="query" value="" />
                 </form>
             </div>
+            <div id="multiStatementsQuery" class="alert">
+              <button type="button" class="close" data-dismiss="alert">&times;</button>
+              <strong>${_('Did you know?')}</strong>
+              ${ _("You can execute queries with multiple SQL statements delimited by a semicolon ';'.") }
+              ${ _("Use '\\059' instead of ';' if you have some conflicts.") }
+            </div>
         </div>
         <div class="span9">
             % if on_success_url:
@@ -461,9 +467,16 @@ ${layout.menubar(section='query')}
             }
         % endif
 
-        $("#executeQuery").click(function(){
+        var executeQuery = function(){
             $("<input>").attr("type","hidden").attr("name","button-submit").attr("value","Execute").appendTo($("#advancedSettingsForm"));
             checkAndSubmit();
+        }
+
+        $("#executeQuery").click(executeQuery);
+        $("#executeQuery").keyup(function(event){
+            if(event.keyCode == 13){
+                executeQuery();
+            }
         });
 
         % if app_name == 'impala':

+ 2 - 7
apps/beeswax/src/beeswax/templates/list_history.mako

@@ -24,7 +24,7 @@ from beeswax.views import collapse_whitespace
 <%namespace name="comps" file="beeswax_components.mako" />
 
 ${ commonheader(_('History'), app_name, user, '100px') | n,unicode }
-${layout.menubar(section='history')}
+${ layout.menubar(section='history') }
 
 <%def name="show_saved_query(design, history)">
   % if design:
@@ -105,12 +105,7 @@ ${layout.menubar(section='history')}
             %>
             % for query in page.object_list:
               <%
-                qcontext = ""
-                try:
-                  design = query.design
-                  qcontext = views.make_query_context('design', design.id)
-                except:
-                  pass
+                qcontext = query.design.get_query_context()
               %>
               <tr class="histRow">
                 <td data-sort-value="${time.mktime(query.submission_date.timetuple())}">${query.submission_date.strftime("%x %X")}</td>

+ 20 - 5
apps/beeswax/src/beeswax/templates/watch_results.mako

@@ -96,10 +96,25 @@ ${layout.menubar(section='query')}
                 </ul>
             </div>
 
-      <div id="jumpToColumnAlert" class="alert hide">
-        <button type="button" class="close" data-dismiss="alert">&times;</button>
-        <strong>${_('Did you know?')}</strong> ${_('You can click on a row to select a column you want to jump to.')}
-      </div>
+          % if not query.is_finished():
+            <div id="multiStatementsQuery" class="alert">
+              <button type="button" class="close" data-dismiss="alert">&times;</button>
+              <strong>${_('Multi-statement query')}</strong></br>
+              ${_('Hue stopped as one of your query contains some results.') }
+              ${_('Click on') }
+              <form action="${ url(app_name + ':watch_query', query.id) }?context=${ query.design.get_query_context() }" method="POST">
+                <input type="submit" value="${ _("next") }"/ class="btn btn-danger">
+              </form>
+              ${_('for continuing the execution of the remaining statements.') }
+            </div>
+          % endif
+
+          <div id="jumpToColumnAlert" class="alert hide">
+            <button type="button" class="close" data-dismiss="alert">&times;</button>
+            <strong>${_('Did you know?')}</strong>
+            ${_('If the result contains a large number of columns, click a row to select a column to jump to.') }
+            ${ _('As you type into the field, a drop-down list displays column names that match the string.')}
+          </div>
         </div>
 
         <div class="span9">
@@ -171,7 +186,7 @@ ${layout.menubar(section='query')}
         </div>
 
         <div class="tab-pane" id="query">
-          <pre>${ query.query }</pre>
+          <pre>${ query.get_current_statement() }</pre>
         </div>
 
         <div class="tab-pane" id="log">

+ 1 - 1
apps/beeswax/src/beeswax/templates/watch_wait.mako

@@ -64,7 +64,7 @@ ${layout.menubar(section='query')}
 					<pre>${ log }</pre>
 				</div>
 				<div class="tab-pane" id="query">
-					<pre>${ query.query }</pre>
+					<pre>${ query.get_current_statement() }</pre>
 				</div>
 			</div>
 		</div>

+ 50 - 1
apps/beeswax/src/beeswax/tests.py

@@ -27,14 +27,17 @@ import threading
 
 from nose.tools import assert_true, assert_equal, assert_false
 from nose.plugins.skip import SkipTest
+
 from django.utils.encoding import smart_str
 from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
 
-from beeswaxd import ttypes
 from desktop.lib.django_test_util import make_logged_in_client, assert_equal_mod_whitespace
 from desktop.lib.django_test_util import assert_similar_pages
 from desktop.lib.test_utils import grant_access
 
+from beeswaxd import ttypes
+
 import beeswax.create_table
 import beeswax.forms
 import beeswax.hive_site
@@ -466,6 +469,52 @@ for x in sys.stdin:
     except:
       LOG.exception("Saw exception in child thread.")
 
+  def test_multiple_statements_no_result_set(self):
+    hql = """
+      CREATE TABLE test_multiple_statements_1 (a int);
+      CREATE TABLE test_multiple_statements_2 (a int);
+      DROP TABLE test_multiple_statements_1;
+      DROP TABLE test_multiple_statements_2;
+    """
+
+    resp = _make_query(self.client, hql)
+    resp = wait_for_query_to_finish(self.client, resp, max=30.0)
+
+    assert_true('DROP TABLE test_multiple_statements_2' in resp.content, resp.content)
+
+  def test_multiple_statements_with_result_set(self):
+    hql = """
+      SELECT foo FROM test;
+      SELECT count(*) FROM test;
+    """
+
+    resp = _make_query(self.client, hql)
+    query = hql_query(hql)
+
+    handle = self.db.execute_and_wait(query)
+    resp = wait_for_query_to_finish(self.client, resp, max=30.0)
+
+    assert_true('multiStatementsQuery' in resp.content, resp.content)
+
+    resp = self.client.post(reverse('beeswax:watch_query', args=[resp.context['query'].id]))
+    assert_true('Waiting for query' in resp.content, resp.content)
+    assert_true('SELECT count(*) FROM test' in resp.content, resp.content)
+
+  def test_multiple_statements_various_queries(self):
+    hql = """
+      CREATE TABLE test_multiple_statements_2 (a int);
+      DROP TABLE test_multiple_statements_1;
+      SELECT foo FROM test;
+    """
+
+    resp = _make_query(self.client, hql)
+    query = hql_query(hql)
+
+    handle = self.db.execute_and_wait(query)
+    resp = wait_for_query_to_finish(self.client, resp, max=30.0)
+
+    assert_true('SELECT foo FROM test' in resp.content, resp.content)
+
   def test_parallel_queries(self):
     """
     Test that we can issue two queries to the BeeswaxServer in parallel.

+ 26 - 23
apps/beeswax/src/beeswax/views.py

@@ -47,7 +47,7 @@ import beeswax.management.commands.beeswax_install_examples
 from beeswax import common, data_export, models, conf
 from beeswax.forms import LoadDataForm, QueryForm, DbForm
 from beeswax.design import HQLdesign, hql_query
-from beeswax.models import SavedQuery
+from beeswax.models import SavedQuery, make_query_context
 from beeswax.server import dbms
 from beeswax.server.dbms import expand_exception, get_query_server_config
 
@@ -122,7 +122,7 @@ def save_design(request, form, type, design, explicit_save):
 
   design.save()
 
-  LOG.info('Saved %sdesign "%s" (id %s) for %s' %
+  LOG.info('Saved %s design "%s" (id %s) for %s' %
            (explicit_save and '' or 'auto ', design.name, design.id, design.owner))
   if explicit_save:
     messages.info(request, _('Saved design "%(name)s"') % {'name': design.name})
@@ -530,6 +530,7 @@ def watch_query(request, id):
   """
   # Coerce types; manage arguments
   query_history = authorized_get_history(request, id, must_exist=True)
+  db = dbms.get(request.user, query_history.get_query_server_config())
 
   # GET param: context.
   context_param = request.GET.get('context', '')
@@ -542,19 +543,26 @@ def watch_query(request, id):
   if not on_success_url:
     on_success_url = results_url
 
+  # Go to next statement if asked to continue or when a statement with no dataset finished.
+  if request.method == 'POST' or (not query_history.is_finished() and query_history.is_success() and not query_history.has_results):
+    try:
+      query_history = db.execute_next_statement(query_history)
+    except BeeswaxException, ex:
+      pass
+
   # Check query state
   handle, state = _get_query_handle_and_state(query_history)
   query_history.save_state(state)
 
-  if query_history.is_success():
-    return format_preserving_redirect(request, on_success_url, request.GET)
-  elif query_history.is_failure():
+  if query_history.is_failure():
     # When we fetch, Beeswax server will throw us a BeeswaxException, which has the
     # log we want to display.
     return format_preserving_redirect(request, results_url, request.GET)
+  elif query_history.is_finished() or (query_history.is_success() and query_history.has_results):
+    return format_preserving_redirect(request, on_success_url, request.GET)
 
   # Still running
-  log = dbms.get(request.user, query_history.get_query_server_config()).get_log(handle)
+  log = db.get_log(handle)
 
   # Keep waiting
   # - Translate context into something more meaningful (type, data)
@@ -570,10 +578,18 @@ def watch_query(request, id):
 
 def watch_query_refresh_json(request, id):
   query_history = authorized_get_history(request, id, must_exist=True)
+  db = dbms.get(request.user, query_history.get_query_server_config())
   handle, state = _get_query_handle_and_state(query_history)
   query_history.save_state(state)
 
-  log = dbms.get(request.user, query_history.get_query_server_config()).get_log(handle)
+  try:
+    if not query_history.is_finished() and query_history.is_success() and not query_history.has_results:
+      db.execute_next_statement(query_history)
+      handle, state = _get_query_handle_and_state(query_history)
+
+    log = db.get_log(handle)
+  except BeeswaxException, ex:
+    handle, state = _get_query_handle_and_state(query_history)
 
   jobs = _parse_out_hadoop_jobs(log)
   job_urls = dict([(job, reverse('jobbrowser.views.single_job', kwargs=dict(job=job))) for job in jobs])
@@ -582,12 +598,13 @@ def watch_query_refresh_json(request, id):
     'log': log,
     'jobs': jobs,
     'jobUrls': job_urls,
-    'isSuccess': query_history.is_success(),
+    'isSuccess': query_history.is_finished() or (query_history.is_success() and query_history.has_results),
     'isFailure': query_history.is_failure()
   }
 
   return HttpResponse(json.dumps(result), mimetype="application/json")
 
+
 def view_results(request, id, first_row=0):
   """
   Returns the view for the results of the QueryHistory with the given id.
@@ -1124,11 +1141,11 @@ def execute_directly(request, query, query_server=None, design=None, tablename=N
     authorized_get_design(request, design.id)
 
   db = dbms.get(request.user, query_server)
-
   database = query.query.get('database', 'default')
   db.use(database)
 
   history_obj = db.execute_query(query, design)
+
   watch_url = reverse(get_app_name(request) + ':watch_query', kwargs={'id': history_obj.id})
   if 'download' in kwargs and kwargs['download']:
     watch_url += '?download=true'
@@ -1219,20 +1236,6 @@ def _list_designs(querydict, page_size, prefix="", user=None):
   return page, filter_params
 
 
-def make_query_context(type, info):
-  """
-  ``type`` is one of "table" and "design", and ``info`` is the table name or design id.
-  Returns a value suitable for GET param.
-  """
-  if type == 'table':
-    return "%s:%s" % (type, info)
-  elif type == 'design':
-    # Use int() to validate that info is a number
-    return "%s:%s" % (type, int(info))
-  LOG.error("Invalid query context type: %s" % (type,))
-  return ''                                     # Empty string is safer than None
-
-
 def _get_query_handle_and_state(query_history):
   """
   Front-end wrapper to handle exceptions. Expects the query to be submitted.