浏览代码

HUE-535 [beeswax] Support multiple databases

Dropdown for listing and selecting databases from the metastore
Store the DB of a query in the query object
Loading a query select back the previous DB
Beeswaxd was not modified as 'use db' can be sent as configuration
HS2 requires a 'use db' for each execute query
All the DDL SQL contain 'db_name.table_name' notation
Use a cookie for remembering previous select DB and just navigating on the pages
when no design are selected for consistency
Adding tests for testing loading of a query, selecting a different DB, listing tables
Romain Rigaux 13 年之前
父节点
当前提交
9f284af
共有 25 个文件被更改,包括 425 次插入302 次删除
  1. 48 46
      apps/beeswax/src/beeswax/create_table.py
  2. 15 11
      apps/beeswax/src/beeswax/design.py
  3. 22 3
      apps/beeswax/src/beeswax/forms.py
  4. 9 1
      apps/beeswax/src/beeswax/server/beeswax_lib.py
  5. 18 7
      apps/beeswax/src/beeswax/server/dbms.py
  6. 14 2
      apps/beeswax/src/beeswax/server/hive_server2_lib.py
  7. 2 2
      apps/beeswax/src/beeswax/templates/choose_delimiter.mako
  8. 3 3
      apps/beeswax/src/beeswax/templates/choose_file.mako
  9. 0 38
      apps/beeswax/src/beeswax/templates/create_table_index.mako
  10. 10 3
      apps/beeswax/src/beeswax/templates/create_table_manually.mako
  11. 1 1
      apps/beeswax/src/beeswax/templates/create_table_statement.mako
  12. 2 2
      apps/beeswax/src/beeswax/templates/define_columns.mako
  13. 5 5
      apps/beeswax/src/beeswax/templates/describe_table.mako
  14. 38 18
      apps/beeswax/src/beeswax/templates/execute.mako
  15. 1 0
      apps/beeswax/src/beeswax/templates/list_designs.mako
  16. 1 3
      apps/beeswax/src/beeswax/templates/list_history.mako
  17. 11 3
      apps/beeswax/src/beeswax/templates/load_table.mako
  18. 2 0
      apps/beeswax/src/beeswax/templates/my_queries.mako
  19. 79 66
      apps/beeswax/src/beeswax/templates/show_tables.mako
  20. 2 2
      apps/beeswax/src/beeswax/templates/util.mako
  21. 5 2
      apps/beeswax/src/beeswax/test_base.py
  22. 67 45
      apps/beeswax/src/beeswax/tests.py
  23. 9 10
      apps/beeswax/src/beeswax/urls.py
  24. 60 28
      apps/beeswax/src/beeswax/views.py
  25. 1 1
      apps/filebrowser/src/filebrowser/templates/listdir_components.mako

+ 48 - 46
apps/beeswax/src/beeswax/create_table.py

@@ -44,12 +44,7 @@ from beeswax.views import execute_directly
 LOG = logging.getLogger(__name__)
 
 
-def index(request):
-  """Main create table entry point, to divert into manual creation vs create from file."""
-  return render("create_table_index.mako", request, {})
-
-
-def create_table(request):
+def create_table(request, database='default'):
   """Create a table by specifying its attributes manually"""
   db = dbms.get(request.user)
 
@@ -68,6 +63,7 @@ def create_table(request):
         partition_columns = [ f.cleaned_data for f in form.partitions.forms ]
         proposed_query = django_mako.render_to_string("create_table_statement.mako",
           {
+            'database': database,
             'table': form.table.cleaned_data,
             'columns': columns,
             'partition_columns': partition_columns
@@ -76,17 +72,18 @@ def create_table(request):
         # Mako outputs bytestring in utf8
         proposed_query = proposed_query.decode('utf-8')
         table_name = form.table.cleaned_data['name']
-        return _submit_create_and_load(request, proposed_query, table_name, None, False)
+        return _submit_create_and_load(request, proposed_query, table_name, None, False, database=database)
   else:
     form.bind()
 
-  return render("create_table_manually.mako", request, dict(
-    action="#",
-    table_form=form.table,
-    columns_form=form.columns,
-    partitions_form=form.partitions,
-    has_tables=len(dbms.get(request.user).get_tables()) > 0
-  ))
+  return render("create_table_manually.mako", request, {
+    'action': "#",
+    'table_form': form.table,
+    'columns_form': form.columns,
+    'partitions_form': form.partitions,
+    'has_tables': len(dbms.get(request.user).get_tables()) > 0,
+    'database': database,
+  })
 
 
 IMPORT_PEEK_SIZE = 8192
@@ -100,7 +97,7 @@ DELIMITER_READABLE = {'\\001' : _('ctrl-As'),
                       ' '     : _('spaces')}
 FILE_READERS = [ ]
 
-def import_wizard(request):
+def import_wizard(request, database='default'):
   """
   Help users define table and based on a file they want to import to Hive.
   Limitations:
@@ -194,16 +191,17 @@ def import_wizard(request):
                                               (s2_delim_form.cleaned_data['delimiter'],))
 
       if do_s2_auto_delim or do_s2_user_delim or cancel_s3_column_def:
-        return render('choose_delimiter.mako', request, dict(
-          action=reverse(app_name + ':import_wizard'),
-          delim_readable=DELIMITER_READABLE.get(s2_delim_form['delimiter'].data[0], s2_delim_form['delimiter'].data[1]),
-          initial=delim_is_auto,
-          file_form=s1_file_form,
-          delim_form=s2_delim_form,
-          fields_list=fields_list,
-          delimiter_choices=TERMINATOR_CHOICES,
-          n_cols=n_cols,
-        ))
+        return render('choose_delimiter.mako', request, {
+          'action': reverse(app_name + ':import_wizard', kwargs={'database': database}),
+          'delim_readable': DELIMITER_READABLE.get(s2_delim_form['delimiter'].data[0], s2_delim_form['delimiter'].data[1]),
+          'initial': delim_is_auto,
+          'file_form': s1_file_form,
+          'delim_form': s2_delim_form,
+          'fields_list': fields_list,
+          'delimiter_choices': TERMINATOR_CHOICES,
+          'n_cols': n_cols,
+          'database': database,
+        })
 
       #
       # Go to step 3: Define column.
@@ -217,14 +215,15 @@ def import_wizard(request):
                 column_type='string',
             ))
           s3_col_formset = ColumnTypeFormSet(prefix='cols', initial=columns)
-        return render('define_columns.mako', request, dict(
-          action=reverse(app_name + ':import_wizard'),
-          file_form=s1_file_form,
-          delim_form=s2_delim_form,
-          column_formset=s3_col_formset,
-          fields_list=fields_list,
-          n_cols=n_cols,
-        ))
+        return render('define_columns.mako', request, {
+          'action': reverse(app_name + ':import_wizard', kwargs={'database': database}),
+          'file_form': s1_file_form,
+          'delim_form': s2_delim_form,
+          'column_formset': s3_col_formset,
+          'fields_list': fields_list,
+          'n_cols': n_cols,
+           'database': database,
+        })
 
       #
       # Finale: Execute
@@ -239,23 +238,25 @@ def import_wizard(request):
                           row_format='Delimited',
                           field_terminator=delim),
             'columns': [ f.cleaned_data for f in s3_col_formset.forms ],
-            'partition_columns': []
+            'partition_columns': [],
+            'database': database,
           }
         )
 
         do_load_data = s1_file_form.cleaned_data.get('do_import')
         path = s1_file_form.cleaned_data['path']
-        return _submit_create_and_load(request, proposed_query, table_name, path, do_load_data)
+        return _submit_create_and_load(request, proposed_query, table_name, path, do_load_data, database=database)
   else:
     s1_file_form = CreateByImportFileForm()
 
-  return render('choose_file.mako', request, dict(
-    action=reverse(app_name + ':import_wizard'),
-    file_form=s1_file_form,
-  ))
+  return render('choose_file.mako', request, {
+    'action': reverse(app_name + ':import_wizard', kwargs={'database': database}),
+    'file_form': s1_file_form,
+    'database': database,
+  })
 
 
-def _submit_create_and_load(request, create_hql, table_name, path, do_load):
+def _submit_create_and_load(request, create_hql, table_name, path, do_load, database):
   """
   Submit the table creation, and setup the load to happen (if ``do_load``).
   """
@@ -265,11 +266,11 @@ def _submit_create_and_load(request, create_hql, table_name, path, do_load):
   if do_load:
     on_success_params['table'] = table_name
     on_success_params['path'] = path
-    on_success_url = reverse(app_name + ':load_after_create')
+    on_success_url = reverse(app_name + ':load_after_create', kwargs={'database': database})
   else:
-    on_success_url = reverse(app_name + ':describe_table', kwargs={'table': table_name})
+    on_success_url = reverse(app_name + ':describe_table', kwargs={'database': database, 'table': table_name})
 
-  query = hql_query(create_hql)
+  query = hql_query(create_hql, database=database)
   return execute_directly(request, query,
                           on_success_url=on_success_url,
                           on_success_params=on_success_params)
@@ -440,7 +441,7 @@ class TextFileReader(object):
 FILE_READERS.append(TextFileReader)
 
 
-def load_after_create(request):
+def load_after_create(request, database):
   """
   Automatically load data into a newly created table.
 
@@ -449,15 +450,16 @@ def load_after_create(request):
   """
   tablename = request.REQUEST.get('table')
   path = request.REQUEST.get('path')
+
   if not tablename or not path:
     msg = _('Internal error: Missing needed parameter to load data into table')
     LOG.error(msg)
     raise PopupException(msg)
 
   LOG.debug("Auto loading data from %s into table %s" % (path, tablename))
-  hql = "LOAD DATA INPATH '%s' INTO TABLE `%s`" % (path, tablename)
+  hql = "LOAD DATA INPATH '%s' INTO TABLE `%s.%s`" % (path, database, tablename)
   query = hql_query(hql)
   app_name = get_app_name(request)
-  on_success_url = reverse(app_name + ':describe_table', kwargs={'table': tablename})
+  on_success_url = reverse(app_name + ':describe_table', kwargs={'database': database, 'table': tablename})
 
   return execute_directly(request, query, on_success_url=on_success_url)

+ 15 - 11
apps/beeswax/src/beeswax/design.py

@@ -32,16 +32,17 @@ from desktop.lib.django_mako import render_to_string
 
 LOG = logging.getLogger(__name__)
 
-SERIALIZATION_VERSION = '0.4.0'
+SERIALIZATION_VERSION = '0.4.1'
 
 
-def hql_query(hql):
-  data_dict = simplejson.loads('{"query": {"email_notify": null, "query": null, "type": 0, "is_parameterized": true}, '
-                               '"functions": [], "VERSION": "0.4.0", "file_resources": [], "settings": []}')
+def hql_query(hql, database='default'):
+  data_dict = simplejson.loads('{"query": {"email_notify": null, "query": null, "type": 0, "is_parameterized": true, "database": "default"}, '
+                               '"functions": [], "VERSION": "0.4.1", "file_resources": [], "settings": []}')
   if not (isinstance(hql, str) or isinstance(hql, unicode)):
     raise Exception('Requires a SQL text query of type <str>, <unicode> and not %s' % type(hql))
 
   data_dict['query']['query'] = _strip_trailing_semicolon(hql)
+  data_dict['query']['database'] = database
   hql_design = HQLdesign()
   hql_design._data_dict = data_dict
 
@@ -57,7 +58,7 @@ class HQLdesign(object):
   want to use "$" natively, but we leave that as an advanced
   option to turn off.
   """
-  _QUERY_ATTRS = [ 'query', 'type', 'is_parameterized', 'email_notify' ]
+  _QUERY_ATTRS = [ 'query', 'type', 'is_parameterized', 'email_notify', 'database' ]
   _SETTINGS_ATTRS = [ 'key', 'value' ]
   _FILE_RES_ATTRS = [ 'type', 'path' ]
   _FUNCTIONS_ATTRS = [ 'name', 'class_name' ]
@@ -114,8 +115,7 @@ class HQLdesign(object):
 
   def get_query_dict(self):
     """get_query_dict() -> QueryDict"""
-    # We construct the mform to use its structure and prefix. We don't actually bind
-    # data to the forms.
+    # We construct the mform to use its structure and prefix. We don't actually bind data to the forms.
     from beeswax.forms import QueryForm
     mform = QueryForm()
     mform.bind()
@@ -136,12 +136,16 @@ class HQLdesign(object):
     """Returns an HQLdesign from the serialized form"""
     dic = simplejson.loads(data)
     if dic['VERSION'] != SERIALIZATION_VERSION:
-      LOG.error('Design version mismatch. Found %s; expect %s' %
-                (dic['VERSION'], SERIALIZATION_VERSION))
-      return None
+      LOG.error('Design version mismatch. Found %s; expect %s' % (dic['VERSION'], SERIALIZATION_VERSION))
+
+    # Convert to latest version
     del dic['VERSION']
+    if 'type' not in dic['query'] or dic['query']['type'] is None:
+      dic['query']['type'] = 0
+    if 'database' not in dic['query']:
+      dic['query']['database'] = 'default'
 
-    design = HQLdesign.__new__(HQLdesign)
+    design = HQLdesign()
     design._data_dict = dic
     return design
 

+ 22 - 3
apps/beeswax/src/beeswax/forms.py

@@ -41,12 +41,26 @@ class QueryForm(MultiForm):
       saveform=SaveForm)
 
 
+class DbForm(forms.Form):
+  """For 'show tables'"""
+  database = forms.ChoiceField(required=False,
+                           label='',
+                           choices=(('default', 'default'),),
+                           initial=0,
+                           widget=forms.widgets.Select(attrs={'class': 'span6'}))
+
+  def __init__(self, *args, **kwargs):
+    databases = kwargs.pop('databases')
+    super(DbForm, self).__init__(*args, **kwargs)
+    self.fields['database'].choices = ((db, db) for db in databases)
+
+
 class SaveForm(forms.Form):
   """Used for saving query design."""
   name = forms.CharField(required=False,
-                        max_length=64,
-                        initial=SavedQuery.DEFAULT_NEW_DESIGN_NAME,
-                        help_text=_t('Change the name to save as a new design.'))
+                         max_length=64,
+                         initial=SavedQuery.DEFAULT_NEW_DESIGN_NAME,
+                         help_text=_t('Change the name to save as a new design.'))
   desc = forms.CharField(required=False, max_length=1024, label=_t("Description"))
   save = forms.BooleanField(widget=SubmitButton, required=False)
   saveas = forms.BooleanField(widget=SubmitButton, required=False)
@@ -121,6 +135,11 @@ class HQLForm(forms.Form):
   is_parameterized = forms.BooleanField(required=False, initial=True)
   email_notify = forms.BooleanField(required=False, initial=False)
   type = forms.IntegerField(required=False, initial=0)
+  database = forms.ChoiceField(required=False,
+                           label='',
+                           choices=(('default', 'default'),),
+                           initial=0,
+                           widget=forms.widgets.Select(attrs={'class': 'span6'}))
 
   def clean_query(self):
     return _strip_trailing_semicolon(self.cleaned_data['query'])

+ 9 - 1
apps/beeswax/src/beeswax/server/beeswax_lib.py

@@ -109,11 +109,19 @@ class BeeswaxClient:
     self.meta_client = self.meta_client()
 
   def make_query(self, hql_query):
-    thrift_query = BeeswaxService.Query(query=hql_query.query['query'], configuration=hql_query.get_configuration())
+    # HUE-535 without having to modify Beeswaxd, add 'use database' as first option
+    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)
     thrift_query.hadoop_user = self.user.username
     return thrift_query
 
 
+  def get_databases(self, *args, **kwargs):
+    return self.meta_client.get_all_databases()
+
+
   def get_tables(self, *args, **kwargs):
     return self.meta_client.get_tables(*args, **kwargs)
 

+ 18 - 7
apps/beeswax/src/beeswax/server/dbms.py

@@ -92,12 +92,16 @@ class Dbms:
     return self.client.get_tables(database, table_names)
 
 
+  def get_databases(self):
+    return self.client.get_databases()
+
+
   def execute_query(self, query, design):
     return self.execute_and_watch(query, design=design)
 
 
-  def select_star_from(self, table):
-    hql = "SELECT * FROM `%s` %s" % (table.name, self._get_browse_limit_clause(table))
+  def select_star_from(self, database, table):
+    hql = "SELECT * FROM `%s.%s` %s" % (database, table.name, self._get_browse_limit_clause(table))
     return self.execute_statement(hql)
 
 
@@ -116,11 +120,11 @@ class Dbms:
     return self.client.fetch(query_handle, start_over, rows)
 
 
-  def get_sample(self, table):
+  def get_sample(self, database, table):
     """No samples if it's a view (HUE-526)"""
     if not table.is_view:
       limit = min(100, BROWSE_PARTITIONED_TABLE_LIMIT.get())
-      hql = "SELECT * FROM `%s` LIMIT %s" % (table.name, limit)
+      hql = "SELECT * FROM `%s.%s` LIMIT %s" % (database, table.name, limit)
       query = hql_query(hql)
       handle = self.execute_and_wait(query, timeout_sec=5.0)
 
@@ -128,15 +132,22 @@ class Dbms:
         return self.fetch(handle)
 
 
-  def drop_table(self, table):
+  def drop_table(self, database, table):
     if table.is_view:
-      hql = "DROP VIEW `%s`" % (table.name,)
+      hql = "DROP VIEW `%s.%s`" % (database, table.name,)
     else:
-      hql = "DROP TABLE `%s`" % (table.name,)
+      hql = "DROP TABLE `%s.%s`" % (database, table.name,)
 
     return self.execute_statement(hql)
 
 
+  def use(self, database):
+    """Beeswax does not support use directly."""
+    if SERVER_INTERFACE.get() == HIVE_SERVER2:
+      query = hql_query('USE %s' % database)
+      return self.execute_and_wait(query)
+
+
   def get_log(self, query_handle):
     return self.client.get_log(query_handle)
 

+ 14 - 2
apps/beeswax/src/beeswax/server/hive_server2_lib.py

@@ -24,7 +24,7 @@ from cli_service import TCLIService
 from cli_service.ttypes import TOpenSessionReq, TGetTablesReq, TFetchResultsReq,\
   TStatusCode, TGetResultSetMetadataReq, TGetColumnsReq, TType,\
   TExecuteStatementReq, TGetOperationStatusReq, TFetchOrientation,\
-  TCloseSessionReq
+  TCloseSessionReq, TGetCatalogsReq
 
 from beeswax import conf
 from beeswax.models import Session, HiveServerQueryHandle, HiveServerQueryHistory
@@ -281,6 +281,15 @@ class HiveServerClient:
     return self._client.CloseSession(req)
 
 
+  def get_databases(self):
+    req = TGetCatalogsReq()
+    res = self.call(self._client.GetCatalogs, req)
+
+    results, schema = self.fetch_result(res.operationHandle)
+
+    return HiveServerTRowSet(results.results, schema.schema).cols(('TABLE_CATALOG',))
+
+
   def get_tables(self, database, table_names):
     req = TGetTablesReq(schemaName=database, tableName=table_names)
     res = self.call(self._client.GetTables, req)
@@ -394,7 +403,6 @@ class HiveServerClientCompatible:
     operationHandle = handle.get_rpc_handle()
 
     res = self._client.get_operation_status(operationHandle)
-    print res
     return HiveServerQueryHistory.STATE_MAP[res.operationState]
 
 
@@ -431,6 +439,10 @@ class HiveServerClientCompatible:
     return 'No logs retrieval implemented'
 
 
+  def get_databases(self, database, table_names):
+    return [table['TABLE_CAT'] for table in self._client.get_databases()]
+
+
   def get_tables(self, database, table_names):
     return [table['TABLE_NAME'] for table in self._client.get_tables(database, table_names)]
 

+ 2 - 2
apps/beeswax/src/beeswax/templates/choose_delimiter.mako

@@ -32,8 +32,8 @@ ${layout.menubar(section='tables')}
             <div class="well sidebar-nav">
                 <ul class="nav nav-list">
                     <li class="nav-header">${_('Actions')}</li>
-                    <li><a href="${ url(app_name + ':import_wizard')}">${_('Create a new table from a file')}</a></li>
-                    <li><a href="${ url(app_name + ':create_table')}">${_('Create a new table manually')}</a></li>
+                    <li><a href="${ url(app_name + ':import_wizard', database=database)}">${_('Create a new table from a file')}</a></li>
+                    <li><a href="${ url(app_name + ':create_table', database=database)}">${_('Create a new table manually')}</a></li>
                 </ul>
             </div>
         </div>

+ 3 - 3
apps/beeswax/src/beeswax/templates/choose_file.mako

@@ -32,14 +32,14 @@ ${layout.menubar(section='tables')}
             <div class="well sidebar-nav">
                 <ul class="nav nav-list">
                     <li class="nav-header">${_('Actions')}</li>
-                    <li><a href="${ url(app_name + ':import_wizard')}">${_('Create a new table from a file')}</a></li>
-                    <li><a href="${ url(app_name + ':create_table')}">${_('Create a new table manually')}</a></li>
+                    <li><a href="${ url(app_name + ':import_wizard', database=database)}">${_('Create a new table from a file')}</a></li>
+                    <li><a href="${ url(app_name + ':create_table', database=database)}">${_('Create a new table manually')}</a></li>
                 </ul>
             </div>
         </div>
         <div class="span9">
             <ul class="nav nav-pills">
-                <li class="active"><a href="${ url(app_name + ':import_wizard') }">${_('Step 1: Choose File')}</a></li>
+                <li class="active"><a href="${ url(app_name + ':import_wizard', database=database) }">${_('Step 1: Choose File')}</a></li>
                 <li><a id="step2" href="#">${_('Step 2: Choose Delimiter')}</a></li>
                 <li><a href="#">${_('Step 3: Define Columns')}</a></li>
             </ul>

+ 0 - 38
apps/beeswax/src/beeswax/templates/create_table_index.mako

@@ -1,38 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. licenses this file
-## to you under the Apache License, Version 2.0 (the
-## "License"); you may not use this file except in compliance
-## with the License.  You may obtain a copy of the License at
-##
-##     http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-<%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="layout" file="layout.mako" />
-
-${commonheader(_('Create Table'), app_name, user, '100px')}
-
-<div class="container-fluid">
-  <div class="toolbar"></div>
-
-  <div id="create_table" class="view">
-    <div class="bw-create_table_choice">
-      <h1>${_('How Do You Want to Create Your Table?')}</h1>
-      <ul class="creation_choices">
-         <li class="clearfix"><a href="${ url(app_name + ':import_wizard')}">${_('Create From File')}</a></li>
-         <li class="clearfix"><a href="${ url(app_name + ':create_table')}">${_('Create Manually')}</a></li>
-      </ul>
-    </div>
-  </div>
-</div>
-${commonfooter(messages)}

+ 10 - 3
apps/beeswax/src/beeswax/templates/create_table_manually.mako

@@ -31,11 +31,12 @@ ${layout.menubar(section='tables')}
     <div class="well sidebar-nav">
         <ul class="nav nav-list">
             <li class="nav-header">${_('Actions')}</li>
-            <li><a href="${ url(app_name + ':import_wizard')}">${_('Create a new table from a file')}</a></li>
-            <li><a href="${ url(app_name + ':create_table')}">${_('Create a new table manually')}</a></li>
+            <li><a href="${ url(app_name + ':import_wizard', database=database)}">${_('Create a new table from a file')}</a></li>
+            <li><a href="${ url(app_name + ':create_table', database=database)}">${_('Create a new table manually')}</a></li>
         </ul>
     </div>
 </div>
+
 <div class="span9">
 <ul class="nav nav-pills">
     <li class="active"><a href="#step1" class="step">${_('Step 1: Name')}</a></li>
@@ -45,6 +46,7 @@ ${layout.menubar(section='tables')}
     <li><a href="#step5" class="step">${_('Step 5: Location')}</a></li>
     <li><a href="#step6" class="step">${_('Step 6: Columns')}</a></li>
 </ul>
+
 <form action="#" method="POST" id="mainForm" class="form-horizontal">
 <div class="steps">
 
@@ -478,18 +480,22 @@ $(document).ready(function () {
       $(this).parent().addClass("active");
     }
   });
+
   $("#nextBtn").click(function () {
     $("ul.nav-pills li.active").next().find("a").click();
   });
+
   $("#backBtn").click(function () {
     $("ul.nav-pills li.active").prev().find("a").click();
   });
+
   $("#submit").click(function (event) {
     // validate step 6
-    if (!validateStep6()) {
+    if (! validateStep6()) {
       event.preventDefault();
     }
   });
+
   var _url = location.href;
   if (_url.indexOf("#") > -1) {
     $(".step[href='" + _url.substring(_url.indexOf("#"), _url.length) + "']").click();
@@ -726,4 +732,5 @@ $(document).ready(function () {
   }
 });
 </script>
+
 ${commonfooter(messages)}

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

@@ -50,7 +50,7 @@ CREATE \
 % if not table.get("use_default_location", True):
 EXTERNAL \
 % endif
-TABLE `${ table["name"] | n }`
+TABLE `${ '%s.%s' % (database, table["name"]) | n }`
 ${column_list(columns)|n}
 % if table["comment"]:
 COMMENT "${table["comment"] | n}"

+ 2 - 2
apps/beeswax/src/beeswax/templates/define_columns.mako

@@ -32,8 +32,8 @@ ${layout.menubar(section='tables')}
             <div class="well sidebar-nav">
                 <ul class="nav nav-list">
                     <li class="nav-header">${_('Actions')}</li>
-                    <li><a href="${ url(app_name + ':import_wizard')}">${_('Create a new table from a file')}</a></li>
-                    <li><a href="${ url(app_name + ':create_table')}">${_('Create a new table manually')}</a></li>
+                    <li><a href="${ url(app_name + ':import_wizard', database=database)}">${_('Create a new table from a file')}</a></li>
+                    <li><a href="${ url(app_name + ':create_table', database=database)}">${_('Create a new table manually')}</a></li>
                 </ul>
             </div>
         </div>

+ 5 - 5
apps/beeswax/src/beeswax/templates/describe_table.mako

@@ -60,7 +60,7 @@ ${layout.menubar(section='tables')}
                 <ul class="nav nav-list">
                     <li class="nav-header">${_('Actions')}</li>
                     <li><a href="#importData" data-toggle="modal">${_('Import Data')}</a></li>
-                    <li><a href="${ url("beeswax:read_table", table=table.name) }">${_('Browse Data')}</a></li>
+                    <li><a href="${ url("beeswax:read_table", database=database, table=table.name) }">${_('Browse Data')}</a></li>
                     <li><a href="#dropTable" data-toggle="modal">${_('Drop')} ${view_or_table_noun}</a></li>
                     <li><a href="${ table.hdfs_link }" rel="${ table.path_location }">${_('View File Location')}</a></li>
                 </ul>
@@ -89,7 +89,7 @@ ${layout.menubar(section='tables')}
                 % if table.partition_keys:
                   <div class="tab-pane" id="partitionColumns">
                     ${column_table(table.partition_keys)}
-                    <a href="${ url("beeswax:describe_partitions", table=table.name) }">${_('Show Partitions')}</a>
+                    <a href="${ url("beeswax:describe_partitions", database=database, table=table.name) }">${_('Show Partitions')}</a>
                   </div>
                 % endif
 
@@ -131,7 +131,7 @@ ${layout.menubar(section='tables')}
 
 
 <div id="dropTable" class="modal hide fade">
-    <form id="dropTableForm" method="POST" action="${ url("beeswax:drop_table", table=table.name) }">
+    <form id="dropTableForm" method="POST" action="${ url("beeswax:drop_table", database=database, table=table.name) }">
     <div class="modal-header">
         <a href="#" class="close" data-dismiss="modal">&times;</a>
         <h3>${_('Drop Table')}</h3>
@@ -150,7 +150,7 @@ ${layout.menubar(section='tables')}
 
 
 <div id="importData" class="modal hide fade">
-    <form method="POST" action="${ url("beeswax:load_table", table=table.name) }" class="form-horizontal">
+    <form method="POST" action="${ url("beeswax:load_table", database=database, table=table.name) }" class="form-horizontal">
         <div class="modal-header">
             <a href="#" class="close" data-dismiss="modal">&times;</a>
             <h3>${_('Import data')}</h3>
@@ -246,7 +246,7 @@ ${layout.menubar(section='tables')}
        "bFilter":false
      });
 
-     $.getJSON("${ url("beeswax:drop_table", table=table.name) }", function (data) {
+     $.getJSON("${ url("beeswax:drop_table", database=database, table=table.name) }", function (data) {
        $("#dropTableMessage").text(data.title);
      });
 

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

@@ -23,14 +23,17 @@
 <%namespace name="layout" file="layout.mako" />
 
 <%def name="query()">
-    % if design and not design.is_auto and design.name:
-      <h1>${_('Query Editor')} : ${design.name}</h1>
-      % if design.desc:
-        <p>${design.desc}</p>
-      % endif
-    % else:
-      <h1>${_('Query Editor')}</h1>
-    % endif
+
+
+            % if error_message:
+                <div class="alert alert-error">
+                    <p><strong>${_('Your query has the following error(s):')}</strong></p>
+                    <p class="queryErrorMessage">${error_message}</p>
+                    % if log:
+                        <small>${_('click the')} <b>${_('Error Log')}</b> ${_('tab below for details')}</small>
+                    % endif
+                </div>
+            % endif
 
     <textarea class="span9" rows="18" placeholder="${_('Example: SELECT * FROM tablename')}" name="${form.query["query"].html_name | n}" id="queryField">${extract_field_data(form.query["query"]) or ''}</textarea>
     <div id="validationResults">
@@ -62,6 +65,10 @@ ${layout.menubar(section='query')}
             <div class="well sidebar-nav">
                 <form id="advancedSettingsForm" action="${action}" method="POST" class="form form-horizontal noPadding">
                     <ul class="nav nav-list">
+                        <li class="nav-header">${_('database')}</li>
+                        <li>
+                          ${ form.query['database'] }
+                        </li>
                         <li class="nav-header">${_('settings')}</li>
                         <li>
                             % for i, f in enumerate(form.settings.forms):
@@ -207,20 +214,19 @@ ${layout.menubar(section='query')}
             </div>
         </div>
         <div class="span9">
-            % if error_message:
-                <div class="alert alert-error">
-                    <p><strong>${_('Your query has the following error(s):')}</strong></p>
-                    <p class="queryErrorMessage">${error_message}</p>
-                    % if log:
-                        <small>${_('click the')} <b>${_('Error Log')}</b> ${_('tab below for details')}</small>
-                    % endif
-                </div>
-            % endif
-
             % if on_success_url:
               <input type="hidden" name="on_success_url" value="${on_success_url}"/>
             % endif
 
+            % if design and not design.is_auto and design.name:
+              <h1>${_('Query Editor')} : ${design.name}</h1>
+              % if design.desc:
+                <p>${design.desc}</p>
+              % endif
+            % else:
+              <h1>${_('Query Editor')}</h1>
+            % endif
+
             % if error_messages or log:
                 <ul class="nav nav-tabs">
                     <li class="active">
@@ -376,6 +382,9 @@ ${layout.menubar(section='query')}
 
 </style>
 
+<script src="/static/ext/js/jquery/plugins/jquery.cookie.js"></script>
+
+
 <script type="text/javascript" charset="utf-8">
     $(document).ready(function(){
         $("*[rel=tooltip]").tooltip({
@@ -423,6 +432,17 @@ ${layout.menubar(section='query')}
             })
         }
 
+        $("#id_query-database").change(function(){
+             $.cookie("hueBeeswaxLastDatabase", $(this).val(), {expires: 90});
+        });
+
+        ## If no particular query is loaded
+        % if design is None or design.id is None:
+            if ($.cookie("hueBeeswaxLastDatabase") != null) {
+                $("#id_query-database").val($.cookie("hueBeeswaxLastDatabase"));
+            }
+        % endif
+
         $("#executeQuery").click(function(){
             $("<input>").attr("type","hidden").attr("name","button-submit").attr("value","Execute").appendTo($("#advancedSettingsForm"));
             checkAndSubmit();

+ 1 - 0
apps/beeswax/src/beeswax/templates/list_designs.mako

@@ -108,6 +108,7 @@ ${layout.menubar(section='saved queries')}
             "bLengthChange": false,
             "bInfo": false,
             "bFilter": false,
+            "aaSorting": [[0, 'asc']],
             "aoColumns": [
                 null,
                 null,

+ 1 - 3
apps/beeswax/src/beeswax/templates/list_history.mako

@@ -92,7 +92,6 @@ ${layout.menubar(section='history')}
             <thead>
               <tr>
                 <th width="10%">${_('Time')}</th>
-                <th width="10%">${_('Query Server')}</th>
                 <th width="15%">${_('Name')}</th>
                 <th width="45%">${_('Query')}</th>
                 <th width="10%">${_('User')}</th>
@@ -115,7 +114,6 @@ ${layout.menubar(section='history')}
               %>
               <tr class="histRow">
                 <td data-sort-value="${time.mktime(query.submission_date.timetuple())}">${query.submission_date.strftime("%x %X")}</td>
-                <td>${query.server_name}</td>
                 <td>${show_saved_query(design, query)}</td>
                 <td>
                   <p>
@@ -157,9 +155,9 @@ ${layout.menubar(section='history')}
                 null,
                 null,
                 null,
-                null,
                 { "bSortable": false }
             ],
+            "aaSorting": [[0, 'desc']],
             "oLanguage": {
                 "sEmptyTable":     "${_('No data available in table')}",
                 "sInfo":           "${_('Showing _START_ to _END_ of _TOTAL_ entries')}",

+ 11 - 3
apps/beeswax/src/beeswax/templates/load_table.mako

@@ -17,17 +17,24 @@
 from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 %>
+
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="util" file="util.mako" />
-${commonheader(_('Load Data into %(table)s') % dict(table=table), app_name, user, '100px')}
+
+${commonheader(_('Load Data into %(table)s') % {'table': table}, app_name, user, '100px')}
+
 ${layout.menubar()}
+
 <div class="container-fluid">
-  <h1>${_('Load Data into %(table)s') % dict(table=table)}</h1>
+  <h1>${_('Load Data into %(table)s') % {'table': table}}</h1>
+
   <div class="prompt_popup">
     <form action="${action}" method="POST">
       <dl>
         ${util.render_field(form["path"])}
-        <a class="hue-chooseFile" data-filters="ArtButton" data-icon-styles="{'width': 16, 'height': 16, 'top': 6, 'left': 6 }" data-chooseFor="path">${_('Open File Chooser')} </a>
+        <a class="hue-chooseFile" data-filters="ArtButton" data-icon-styles="{'width': 16, 'height': 16, 'top': 6, 'left': 6 }" data-chooseFor="path">
+          ${_('Open File Chooser')}
+        </a>
         ## Path (on HDFS) of files to load.
         ${util.render_field(form["overwrite"])}
         ## Any existing data will be erased!
@@ -45,4 +52,5 @@ ${layout.menubar()}
     </form>
   </div>
 </div>
+
 ${commonfooter(messages)}

+ 2 - 0
apps/beeswax/src/beeswax/templates/my_queries.mako

@@ -176,6 +176,7 @@ ${layout.menubar(section='my queries')}
             "bPaginate": false,
             "bLengthChange": false,
             "bInfo": false,
+            "aaSorting": [[3, 'desc']],
             "aoColumns": [
                 null,
                 null,
@@ -190,6 +191,7 @@ ${layout.menubar(section='my queries')}
             "bPaginate": false,
             "bLengthChange": false,
             "bInfo": false,
+            "aaSorting": [[0, 'desc']],
             "aoColumns": [
                 { "sSortDataType": "dom-sort-value", "sType": "numeric" },
                 null,

+ 79 - 66
apps/beeswax/src/beeswax/templates/show_tables.mako

@@ -25,87 +25,100 @@ ${layout.menubar(section='tables')}
 
 
 <div class="container-fluid">
-	<h1>${_('Table List')}</h1>
-	<div class="row-fluid">
-		<div class="span3">
-			<div class="well sidebar-nav">
-				<ul class="nav nav-list">
-					<li class="nav-header">${_('Actions')}</li>
-					% if not examples_installed:
-		        	<li><a href="#installSamples" data-toggle="modal">${_('Install samples')}</a></li>
-		      		% endif
-		      		<li><a href="${ url(app_name + ':import_wizard') }">${_('Create a new table from a file')}</a></li>
-					<li><a href="${ url(app_name + ':create_table') }">${_('Create a new table manually')}</a></li>
-				</ul>
-			</div>
-		</div>
-		<div class="span9">
-			<table class="table table-condensed table-striped datatables">
-				<thead>
-					<tr>
-						<th>${_('Table Name')}</th>
-						<th>&nbsp;</th>
-					</tr>
-				</thead>
-				<tbody>
-				% for table in tables:
-					<tr>
-						<td>
-							<a href="${ url(app_name + ':describe_table', table=table) }" data-row-selector="true">${ table }</a>
-						</td>
-						<td><a href="${ url(app_name + ':read_table', table=table) }" class="btn">${_('Browse Data')}</a></td>
-					</tr>
-				% endfor
-				</tbody>
-			</table>
-		</div>
-	</div>
+    <h1>${_('Table List')}</h1>
+    <div class="row-fluid">
+        <div class="span3">
+            <div class="well sidebar-nav">
+                <ul class="nav nav-list">
+                    <li class="nav-header">${_('database')}</li>
+                    <li>
+                       <form action="${ url(app_name + ':show_tables') }" id="db_form" method="POST">
+                         ${ db_form }
+                       </form>
+                    </li>
+                    <li class="nav-header">${_('Actions')}</li>
+                    % if not examples_installed:
+                    <li><a href="#installSamples" data-toggle="modal">${_('Install samples')}</a></li>
+                      % endif
+                      <li><a href="${ url(app_name + ':import_wizard', database=database) }">${_('Create a new table from a file')}</a></li>
+                    <li><a href="${ url(app_name + ':create_table', database=database) }">${_('Create a new table manually')}</a></li>
+                </ul>
+            </div>
+        </div>
+        <div class="span9">
+            <table class="table table-condensed table-striped datatables">
+                <thead>
+                    <tr>
+                        <th>${_('Table Name')}</th>
+                        <th>&nbsp;</th>
+                    </tr>
+                </thead>
+                <tbody>
+                % for table in tables:
+                    <tr>
+                        <td>
+                            <a href="${ url(app_name + ':describe_table', database=database, table=table) }" data-row-selector="true">${ table }</a>
+                        </td>
+                        <td><a href="${ url(app_name + ':read_table', database=database, table=table) }" class="btn">${_('Browse Data')}</a></td>
+                    </tr>
+                % endfor
+                </tbody>
+            </table>
+        </div>
+    </div>
 </div>
 
 
 
 % if not examples_installed:
 <div id="installSamples" class="modal hide fade">
-	<div class="modal-header">
-		<a href="#" class="close" data-dismiss="modal">&times;</a>
-		<h3>${_('Install samples')}</h3>
-	</div>
-	<div class="modal-body">
-	  <div id="installSamplesMessage">
+    <div class="modal-header">
+        <a href="#" class="close" data-dismiss="modal">&times;</a>
+        <h3>${_('Install samples')}</h3>
+    </div>
+    <div class="modal-body">
+      <div id="installSamplesMessage">
 
-	  </div>
-	</div>
-	<div class="modal-footer">
-		<a href="#" class="btn" data-dismiss="modal">${_('Cancel')}</a>
-		<a href="#" id="installSamplesBtn" class="btn btn-primary">${_('Yes, install samples')}</a>
-	</div>
+      </div>
+    </div>
+    <div class="modal-footer">
+        <a href="#" class="btn" data-dismiss="modal">${_('Cancel')}</a>
+        <a href="#" id="installSamplesBtn" class="btn btn-primary">${_('Yes, install samples')}</a>
+    </div>
 </div>
 % endif
 
+<script src="/static/ext/js/jquery/plugins/jquery.cookie.js"></script>
+
 <script type="text/javascript" charset="utf-8">
-	$(document).ready(function(){
-		$(".datatables").dataTable({
-			"bPaginate": false,
-		    "bLengthChange": false,
-			"bInfo": false,
-			"bFilter": false,
-			"aoColumns": [
-				null,
-				{ "sWidth": "130px", "bSortable" : false }
-			 ]
-		});
+    $(document).ready(function(){
+        $(".datatables").dataTable({
+            "bPaginate": false,
+            "bLengthChange": false,
+            "bInfo": false,
+            "bFilter": false,
+            "aoColumns": [
+                null,
+                { "sWidth": "130px", "bSortable" : false }
+             ]
+        });
 
-		$("a[data-row-selector='true']").jHueRowSelector();
+        $("a[data-row-selector='true']").jHueRowSelector();
 
-		% if not examples_installed:
-		$.getJSON("${ url(app_name + ':install_examples') }",function(data){
-			$("#installSamplesMessage").text(data.title);
-		});
+        $("#id_database").change(function(){
+             $.cookie("hueBeeswaxLastDatabase", $(this).val(), {expires: 90});
+             $('#db_form').submit();
+        });
+
+        % if not examples_installed:
+        $.getJSON("${ url(app_name + ':install_examples') }", function(data){
+            $("#installSamplesMessage").text(data.title);
+        });
 
         $("#installSamplesBtn").click(function(){
             $.post(
                 "${ url(app_name + ':install_examples') }",
-                { submit:"Submit" },
+                { submit: "Submit" },
                 function(result){
                     if (result.creationSucceeded){
                         window.location.href = "${ url(app_name + ':show_tables') }";
@@ -117,8 +130,8 @@ ${layout.menubar(section='tables')}
                 }
             );
         });
-		% endif
-	});
+        % endif
+    });
 </script>
 
 ${commonfooter(messages)}

+ 2 - 2
apps/beeswax/src/beeswax/templates/util.mako

@@ -58,8 +58,8 @@ from django.utils.translation import ugettext as _
 <%def name="render_query_context(query_context)">
   % if query_context:
     % if query_context[0] == 'table':
-      <% tablename = query_context[1] %>
-      <a href="${ url(app_name + ':describe_table', tablename) }">${tablename}</a>
+      <% tablename, database = query_context[1].split(':') %>
+      <a href="${ url(app_name + ':describe_table', database, tablename) }">${tablename}</a>
     % elif query_context[0] == 'design':
       <% design = query_context[1] %>
       % if design.is_auto:

+ 5 - 2
apps/beeswax/src/beeswax/test_base.py

@@ -201,7 +201,7 @@ def wait_for_query_to_finish(client, response, max=30.0):
 def make_query(client, query, submission_type="Execute",
                udfs=None, settings=None, resources=None,
                wait=False, name=None, desc=None, local=True,
-               is_parameterized=True, max=30.0, **kwargs):
+               is_parameterized=True, max=30.0, database='default', **kwargs):
   """
   Prepares arguments for the execute view.
 
@@ -217,7 +217,8 @@ def make_query(client, query, submission_type="Execute",
   # Prepares arguments for the execute view.
   parameters = {
     'query-query': query,
-    'query-is_parameterized': is_parameterized and "on"
+    'query-is_parameterized': is_parameterized and "on",
+    'query-database': database,
   }
 
   if submission_type == 'Execute':
@@ -306,6 +307,8 @@ class BeeswaxSampleProvider(object):
     if _INITIALIZED:
       return
 
+    make_query(cls.client, 'CREATE DATABASE other_db', wait=True)
+
     data_file = u'/tmp/beeswax/sample_data_échantillon_%d.tsv'
 
     # Create a "test_partitions" table.

+ 67 - 45
apps/beeswax/src/beeswax/tests.py

@@ -61,11 +61,11 @@ CSV_LINK_PAT = re.compile('/beeswax/download/\d+/csv')
 def _make_query(client, query, submission_type="Execute",
                 udfs=None, settings=None, resources=[],
                 wait=False, name=None, desc=None, local=True,
-                is_parameterized=True, max=30.0, **kwargs):
+                is_parameterized=True, max=30.0, database='default', **kwargs):
   """Wrapper around the real make_query"""
   res = make_query(client, query, submission_type,
                    udfs, settings, resources,
-                   wait, name, desc, local, is_parameterized, max, **kwargs)
+                   wait, name, desc, local, is_parameterized, max, database, **kwargs)
 
   # Should be in the history if it's submitted.
   if submission_type == 'Execute':
@@ -123,17 +123,17 @@ class TestBeeswaxWithHadoop(BeeswaxSampleProvider):
     assert_true("A base for other temporary directories" in response_verbose.content)
 
   def test_describe_partitions(self):
-    response = self.client.get("/beeswax/table/test_partitions/partitions")
+    response = self.client.get("/beeswax/table/default/test_partitions/partitions")
     assert_true("baz_one" in response.content)
     assert_true("boom_two" in response.content)
-    response = self.client.get("/beeswax/table/test/partitions")
+    response = self.client.get("/beeswax/table/default/test/partitions")
     assert_true("is not partitioned." in response.content)
 
   def test_browse_partitions_with_limit(self):
     # Limit to 90
     finish = beeswax.conf.BROWSE_PARTITIONED_TABLE_LIMIT.set_for_testing("90")
     try:
-      response = self.client.get("/beeswax/table/test_partitions")
+      response = self.client.get("/beeswax/table/default/test_partitions")
       assert_true("0x%x" % 89 in response.content, response.content)
       assert_false("0x%x" % 90 in response.content, response.content)
     finally:
@@ -181,18 +181,25 @@ for x in sys.stdin:
     assert_equal("echo", self.db.echo("echo"))
 
     # Table should have been created
-    response = self.client.get("/beeswax/tables")
+    response = self.client.get("/beeswax/tables/")
     assert_true("test" in response.context["tables"])
 
+    # Switch databases
+    response = self.client.get("/beeswax/tables/default")
+    assert_true("test" in response.context["tables"])
+
+    response = self.client.get("/beeswax/tables/not_there")
+    assert_false("test" in response.context["tables"])
+
     # And have detail
-    response = self.client.get("/beeswax/table/test")
+    response = self.client.get("/beeswax/table/default/test")
     assert_true("foo" in response.content)
 
     # Remember the number of history items. Use a generic fragment 'test' to pass verification.
     history_cnt = verify_history(self.client, fragment='test')
 
     # Show table data.
-    response = self.client.get("/beeswax/table/test/read", follow=True)
+    response = self.client.get("/beeswax/table/default/test/read", follow=True)
     response = wait_for_query_to_finish(self.client, response, max=30.0)
     # Note that it may not return all rows at once. But we expect at least 10.
     assert_true(len(response.context['results']) > 10)
@@ -203,7 +210,7 @@ for x in sys.stdin:
     assert_equal(verify_history(self.client, fragment='test'), history_cnt,
                  'Implicit queries should not be saved in the history')
     assert_equal(str(response.context['query_context'][0]), 'table')
-    assert_equal(str(response.context['query_context'][1]), 'test')
+    assert_equal(str(response.context['query_context'][1]), 'test:default')
 
     # Query the data
     # We use a semicolon here for kicks; the code strips it out.
@@ -383,12 +390,12 @@ for x in sys.stdin:
     design_id = response.context["design"].id
 
     # Don't fill out the form
-    response = self.client.post("/beeswax/execute_parameterized/%d" % design_id, { 'query_servers-server': 'default' })
+    response = self.client.post("/beeswax/execute_parameterized/%d" % design_id)
     assert_true("parameterization.mako", response.template)
 
     # Now fill it out
     response = self.client.post("/beeswax/execute_parameterized/%d" % design_id,
-      { "parameterization-x": str(1), "parameterization-y": str(2), 'query_servers-server': 'default' }, follow=True)
+      { "parameterization-x": str(1), "parameterization-y": str(2)}, follow=True)
 
     assert_true("watch_wait.mako" in response.template)
     # Check that substitution happened!
@@ -397,7 +404,7 @@ for x in sys.stdin:
 
     # Check that error handling is reasonable
     response = self.client.post("/beeswax/execute_parameterized/%d" % design_id,
-      { "parameterization-x": "'_this_is_not SQL ", "parameterization-y": str(2), 'query_servers-server': 'default' },
+      { "parameterization-x": "'_this_is_not SQL ", "parameterization-y": str(2) },
       follow=True)
     assert_true("execute.mako" in response.template)
     log = response.context["log"]
@@ -440,7 +447,7 @@ for x in sys.stdin:
                 "selecting from latin1 table should not blow up")
 
     # Describe table should be fine with non-ascii comment
-    response = self.client.get('/beeswax/table/test_utf8')
+    response = self.client.get('/beeswax/table/default/test_utf8')
     assert_equal(response.context['table'].parameters['comment'], self.get_i18n_table_comment())
 
   def _parallel_query_helper(self, i, result_holder, lock, num_tasks):
@@ -536,18 +543,19 @@ for x in sys.stdin:
     nplus_designs = len(resp.context['page'].object_list)
     assert_true(nplus_designs == n_designs, 'Auto design should not show up in list_designs')
 
-    # Test explicit save
+    # Test explicit save and use another DB
     query = 'MORE BOGUS JUNKS FROM test'
-    exe_resp = _make_query(self.client, query, name='rubbish', submission_type='Save')
+    exe_resp = _make_query(self.client, query, name='rubbish', submission_type='Save', database='other_db')
     assert_true("error_message" not in exe_resp.context)
     resp = cli.get('/beeswax/list_designs')
-    assert_true('rubbish' in resp.content)
+    assert_true('rubbish' in resp.content, resp.content)
     nplusplus_designs = len(resp.context['page'].object_list)
     assert_true(nplusplus_designs > nplus_designs)
 
-    # Retrieve that design
+    # Retrieve that design and check correct DB is selected
     design = beeswax.models.SavedQuery.objects.filter(name='rubbish')[0]
     resp = cli.get('/beeswax/execute/%s' % (design.id,))
+    assert_true('selected="selected">other_db</option>' in resp.content)
     assert_similar_pages(resp.content, exe_resp.content)
 
     # Clone the rubbish design
@@ -673,20 +681,20 @@ for x in sys.stdin:
     about whether a table is partitioned.
     """
     # Check that view works
-    resp = self.client.get("/beeswax/table/test/load")
+    resp = self.client.get("/beeswax/table/default/test/load")
     assert_true(resp.context["form"])
 
     # Try the submission
-    resp = self.client.post("/beeswax/table/test/load", dict(path="/tmp/foo", overwrite=True))
-    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' OVERWRITE INTO TABLE `test`",
+    resp = self.client.post("/beeswax/table/default/test/load", dict(path="/tmp/foo", overwrite=True))
+    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' OVERWRITE INTO TABLE `default.test`",
         resp.context["form"].query.initial["query"])
-    resp = self.client.post("/beeswax/table/test/load", dict(path="/tmp/foo", overwrite=False))
-    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `test`",
+    resp = self.client.post("/beeswax/table/default/test/load", dict(path="/tmp/foo", overwrite=False))
+    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `default.test`",
         resp.context["form"].query.initial["query"])
 
     # Try it with partitions
-    resp = self.client.post("/beeswax/table/test_partitions/load", dict(path="/tmp/foo", partition_0="alpha", partition_1="beta"))
-    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `test_partitions` PARTITION (baz='alpha', boom='beta')",
+    resp = self.client.post("/beeswax/table/default/test_partitions/load", dict(path="/tmp/foo", partition_0="alpha", partition_1="beta"))
+    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `default.test_partitions` PARTITION (baz='alpha', boom='beta')",
         resp.context["form"].query.initial["query"])
 
 
@@ -788,7 +796,7 @@ for x in sys.stdin:
     self.client.post('/beeswax/install_examples')
 
     # New tables exists
-    resp = self.client.get('/beeswax/tables')
+    resp = self.client.get('/beeswax/tables/')
     assert_true('sample_08' in resp.content)
     assert_true('sample_07' in resp.content)
 
@@ -822,10 +830,10 @@ for x in sys.stdin:
     NOT TESTED/DONE: Validation checks for the inputs.
     """
     # Make sure we get a form
-    resp = self.client.get("/beeswax/create/create_table")
+    resp = self.client.get("/beeswax/create/create_table/default")
     assert_true("Field terminator" in resp.content)
     # Make a submission
-    resp = self.client.post("/beeswax/create/create_table", {
+    resp = self.client.post("/beeswax/create/create_table/default", {
       'table-name': 'my_table',
       'table-comment': 'Yo>>>>dude',  # Make sure escaping is sort of ok.
       'table-row_format': 'Delimited',
@@ -844,7 +852,7 @@ for x in sys.stdin:
     }, follow=True)
 
     assert_equal_mod_whitespace("""
-        CREATE EXTERNAL TABLE `my_table`
+        CREATE EXTERNAL TABLE `default.my_table`
         (
          `my_col` string
         )
@@ -856,11 +864,11 @@ for x in sys.stdin:
           STORED AS TextFile LOCATION "/tmp/foo"
     """, resp.context['query'].query)
 
-    assert_true('on_success_url=%2Fbeeswax%2Ftable%2Fmy_table' in resp.context['fwd_params'])
+    assert_true('on_success_url=%2Fbeeswax%2Ftable%2Fdefault%2Fmy_table' in resp.context['fwd_params'], resp.context['fwd_params'])
 
   def test_create_table_timestamp(self):
     # Check form
-    response = self.client.get('/beeswax/create/create_table')
+    response = self.client.get('/beeswax/create/create_table/default')
     assert_true('<option value="timestamp">timestamp</option>' in response.content, response.content)
 
     # Check creation
@@ -870,14 +878,14 @@ for x in sys.stdin:
     self._make_custom_data_file(filename, [0, 0, 0])
     self._make_table('timestamp_invalid_data', 'CREATE TABLE timestamp_invalid_data (timestamp1 TIMESTAMP)', filename)
 
-    response = self.client.get("/beeswax/table/timestamp_invalid_data")
+    response = self.client.get("/beeswax/table/default/timestamp_invalid_data")
     assert_true('Error!' in response.content, response.content)
 
     # Good format
     self._make_custom_data_file(filename, ['2012-01-01 10:11:30', '2012-01-01 10:11:31'])
     self._make_table('timestamp_valid_data', 'CREATE TABLE timestamp_valid_data (timestamp1 TIMESTAMP)', filename)
 
-    response = self.client.get("/beeswax/table/timestamp_valid_data")
+    response = self.client.get("/beeswax/table/default/timestamp_valid_data")
     assert_true('2012-01-01 10:11:30' in response.content, response.content)
 
   def test_partitioned_create_table(self):
@@ -885,10 +893,10 @@ for x in sys.stdin:
     Test HQL generation of create table with partition columns
     """
     # Make sure we get a form
-    resp = self.client.get("/beeswax/create/create_table")
+    resp = self.client.get("/beeswax/create/create_table/default")
     assert_true("Field terminator" in resp.content)
     # Make a submission
-    resp = self.client.post("/beeswax/create/create_table", {
+    resp = self.client.post("/beeswax/create/create_table/default", {
       'table-name': 'my_table2',
       'table-row_format': 'Delimited',
       'table-field_terminator_0': r'\001',
@@ -908,7 +916,7 @@ for x in sys.stdin:
     }, follow=True)
 
     assert_equal_mod_whitespace("""
-        CREATE TABLE `my_table2`
+        CREATE TABLE `default.my_table2`
         (
          `my_col` string
         )
@@ -928,7 +936,7 @@ for x in sys.stdin:
     """
     Test field dependency in the create table form
     """
-    resp = self.client.post("/beeswax/create/create_table", {
+    resp = self.client.post("/beeswax/create/create_table/default", {
       'table-name': 'my_table',
       'table-row_format': 'SerDe',
       # Missing SerDe fields!
@@ -985,7 +993,7 @@ for x in sys.stdin:
     write_file('/tmp/comma.dat.gz', RAW_FIELDS, ',', do_gzip=True)
 
     # Test auto delim selection
-    resp = self.client.post('/beeswax/create/import_wizard', {
+    resp = self.client.post('/beeswax/create/import_wizard/default', {
       'submit_file': 'on',
       'path': '/tmp/comma.dat',
       'name': 'test_create_import',
@@ -993,7 +1001,7 @@ for x in sys.stdin:
     assert_equal(resp.context['fields_list'], RAW_FIELDS)
 
     # Test same with gzip
-    resp = self.client.post('/beeswax/create/import_wizard', {
+    resp = self.client.post('/beeswax/create/import_wizard/default', {
       'submit_file': 'on',
       'path': '/tmp/comma.dat.gz',
       'name': 'test_create_import',
@@ -1001,7 +1009,7 @@ for x in sys.stdin:
     assert_equal(resp.context['fields_list'], RAW_FIELDS)
 
     # Make sure space works
-    resp = self.client.post('/beeswax/create/import_wizard', {
+    resp = self.client.post('/beeswax/create/import_wizard/default', {
       'submit_preview': 'on',
       'path': '/tmp/spacé.dat',
       'name': 'test_create_import',
@@ -1012,7 +1020,7 @@ for x in sys.stdin:
     assert_equal(len(resp.context['fields_list'][0]), 4)
 
     # Make sure custom delimiters work
-    resp = self.client.post('/beeswax/create/import_wizard', {
+    resp = self.client.post('/beeswax/create/import_wizard/default', {
       'submit_preview': 'on',
       'path': '/tmp/pipes.dat',
       'name': 'test_create_import',
@@ -1023,7 +1031,7 @@ for x in sys.stdin:
     assert_equal(len(resp.context['fields_list'][0]), 3)
 
     # Test column definition
-    resp = self.client.post('/beeswax/create/import_wizard', {
+    resp = self.client.post('/beeswax/create/import_wizard/default', {
       'submit_delim': 'on',
       'path': '/tmp/comma.dat.gz',
       'name': 'test_create_import',
@@ -1035,7 +1043,7 @@ for x in sys.stdin:
     assert_equal(len(resp.context['column_formset'].forms), 3)
 
     # Test table creation and data loading
-    resp = self.client.post('/beeswax/create/import_wizard', {
+    resp = self.client.post('/beeswax/create/import_wizard/default', {
       'submit_create': 'on',
       'path': '/tmp/comma.dat.gz',
       'name': 'test_create_import',
@@ -1058,7 +1066,7 @@ for x in sys.stdin:
     resp = wait_for_query_to_finish(self.client, resp, max=180.0)
 
     # Check data is in the table (by describing it)
-    resp = self.client.get('/beeswax/table/test_create_import')
+    resp = self.client.get('/beeswax/table/default/test_create_import')
     cols = resp.context['table'].cols
     assert_equal(len(cols), 3)
     assert_equal([ col.name for col in cols ], [ 'col_a', 'col_b', 'col_c' ])
@@ -1066,7 +1074,7 @@ for x in sys.stdin:
     assert_true("sp ace</td>" in resp.content)
 
   def test_describe_view(self):
-    resp = self.client.get('/beeswax/table/myview')
+    resp = self.client.get('/beeswax/table/default/myview')
     assert_equal(None, resp.context['sample'])
     assert_true(resp.context['table'].is_view)
     assert_true("View Metadata" in resp.content)
@@ -1075,7 +1083,7 @@ for x in sys.stdin:
 
   def test_select_query_server(self):
     c = make_logged_in_client()
-    _make_query(c, 'SELECT bogus FROM test', server='beeswax') # Improvement: mock another server
+    _make_query(c, 'SELECT bogus FROM test') # Improvement: mock another server
 
     history = beeswax.models.QueryHistory.objects.latest('id')
     assert_equal('beeswax', history.server_name)
@@ -1083,6 +1091,20 @@ for x in sys.stdin:
     assert_equal(BEESWAXD_TEST_PORT, history.server_port)
 
 
+  def test_select_multi_db(self):
+    response = _make_query(self.client, 'SELECT * FROM test LIMIT 5', local=False, database='default')
+    response = wait_for_query_to_finish(self.client, response)
+    assert_true('Query Results' in response.content, response.content)
+
+    response = _make_query(self.client, 'SHOW TABLES', local=False, database='other_db')
+    response = wait_for_query_to_finish(self.client, response)
+    assert_true('Query Results' in response.content, response.content)
+
+    response = _make_query(self.client, 'SELECT * FROM test LIMIT 5', local=False, database='not_there')
+    response = wait_for_query_to_finish(self.client, response)
+    assert_true('Error' in response.content, response.content)
+
+
 def test_import_gzip_reader():
   """Test the gzip reader in create table"""
   # Make gzipped data

+ 9 - 10
apps/beeswax/src/beeswax/urls.py

@@ -20,12 +20,12 @@ from django.conf.urls.defaults import patterns, url
 urlpatterns = patterns('beeswax.views',
   url(r'^$', 'index', name='index'),
 
-  url(r'^tables$', 'show_tables', name='show_tables'),
-  url(r'^table/(?P<table>\w+)$', 'describe_table', name='describe_table'),
-  url(r'^table/(?P<table>\w+)/partitions$', 'describe_partitions', name='describe_partitions'),
-  url(r'^table/(?P<table>\w+)/load$', 'load_table', name='load_table'),
-  url(r'^table/(?P<table>\w+)/read$', 'read_table', name='read_table'),
-  url(r'^table/(?P<table>\w+)/drop$', 'drop_table', name='drop_table'),
+  url(r'^tables/(?P<database>\w+)?$', 'show_tables', name='show_tables'),
+  url(r'^table/(?P<database>\w+)/(?P<table>\w+)$', 'describe_table', name='describe_table'),
+  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/partitions$', 'describe_partitions', name='describe_partitions'),
+  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/load$', 'load_table', name='load_table'),
+  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/read$', 'read_table', name='read_table'),
+  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/drop$', 'drop_table', name='drop_table'),
 
   url(r'^execute/(?P<design_id>\d+)?$', 'execute_query', name='execute_query'),
   url(r'^explain_parameterized/(?P<design_id>\d+)$', 'explain_parameterized_query', name='explain_parameterized_query'),
@@ -50,8 +50,7 @@ urlpatterns = patterns('beeswax.views',
 urlpatterns += patterns(
   'beeswax.create_table',
 
-  url(r'^create$', 'index'),
-  url(r'^create/create_table$', 'create_table', name='create_table'),
-  url(r'^create/import_wizard$', 'import_wizard', name='import_wizard'),
-  url(r'^create/auto_load$', 'load_after_create', name='load_after_create'),
+  url(r'^create/create_table/(?P<database>\w+)$', 'create_table', name='create_table'),
+  url(r'^create/import_wizard/(?P<database>\w+)$', 'import_wizard', name='import_wizard'),
+  url(r'^create/auto_load/(?P<database>\w+)$', 'load_after_create', name='load_after_create'),
 )

+ 60 - 28
apps/beeswax/src/beeswax/views.py

@@ -45,7 +45,7 @@ import beeswax.design
 import beeswax.management.commands.beeswax_install_examples
 
 from beeswax import common, data_export, models, conf
-from beeswax.forms import LoadDataForm, QueryForm
+from beeswax.forms import LoadDataForm, QueryForm, DbForm
 from beeswax.design import HQLdesign, hql_query
 from beeswax.models import SavedQuery
 from beeswax.server import dbms
@@ -252,7 +252,7 @@ def list_query_history(request):
                           Default to "-date".
     auto_query=<bool>   - Show auto generated actions (drop table, read data, etc). Default False
   """
-  DEFAULT_PAGE_SIZE = 10
+  DEFAULT_PAGE_SIZE = 20
 
   share_queries = conf.SHARE_SAVED_QUERIES.get() or request.user.is_superuser
 
@@ -277,22 +277,40 @@ def list_query_history(request):
 Table Views
 """
 
-def show_tables(request):
-  tables = dbms.get(request.user).get_tables()
+def show_tables(request, database=None):
+  if database is None:
+    database = request.COOKIES.get('hueBeeswaxLastDatabase', 'default') # Assume always 'default'
+  db = dbms.get(request.user)
+
+  databases = db.get_databases()
+
+  if request.method == 'POST':
+    db_form = DbForm(request.POST, databases=databases)
+    if db_form.is_valid():
+      database = db_form.cleaned_data['database']
+  else:
+    db_form = DbForm(initial={'database': database}, databases=databases)
+
+  tables = db.get_tables(database=database)
   examples_installed = beeswax.models.MetaInstall.get().installed_example
 
-  return render("show_tables.mako", request, dict(tables=tables, examples_installed=examples_installed))
+  return render("show_tables.mako", request, {
+      'tables': tables,
+      'examples_installed': examples_installed,
+      'db_form': db_form,
+      'database': database,
+  })
 
 
-def describe_table(request, table):
+def describe_table(request, database, table):
   db = dbms.get(request.user)
   error_message = ''
   table_data = ''
 
-  table = db.get_table('default', table)
+  table = db.get_table(database, table)
 
   try:
-    table_data = db.get_sample(table)
+    table_data = db.get_sample(database, table)
   except BeeswaxException, ex:
     error_message, logs = expand_exception(ex, db)
 
@@ -303,17 +321,18 @@ def describe_table(request, table):
       'sample': table_data and table_data.rows(),
       'load_form': load_form,
       'error_message': error_message,
+      'database': database,
   })
 
 
-def drop_table(request, table):
+def drop_table(request, database, table):
   db = dbms.get(request.user)
 
-  table = db.get_table('default', table)
+  table = db.get_table(database, table)
 
   if request.method == 'POST':
     try:
-      query_history = db.drop_table(table)
+      query_history = db.drop_table(database, table)
       url = reverse(get_app_name(request) + ':watch_query', args=[query_history.id]) + '?on_success_url=' + reverse(get_app_name(request) + ':show_tables')
       return redirect(url)
     except BeeswaxException, ex:
@@ -328,23 +347,23 @@ def drop_table(request, table):
     return render('confirm.html', request, dict(url=request.path, title=title))
 
 
-def read_table(request, table):
+def read_table(request, database, table):
   db = dbms.get(request.user)
 
-  table = db.get_table('default', table)
+  table = db.get_table(database, table)
 
   try:
-    history = db.select_star_from(table)
+    history = db.select_star_from(database, table)
     get = request.GET.copy()
-    get['context'] = 'table:%s' % table.name
+    get['context'] = 'table:%s:%s' % (table.name, database)
     request.GET = get
     return watch_query(request, history.id)
   except Exception, e:
     raise PopupException(_('Can read table'), detail=e)
 
 
-def load_table(request, table):
-  table_obj = dbms.get(request.user).get_table('default', table)
+def load_table(request, database, table):
+  table_obj = dbms.get(request.user).get_table(database, table)
 
   if request.method == "POST":
     form = beeswax.forms.LoadDataForm(table_obj, request.POST)
@@ -356,7 +375,7 @@ def load_table(request, table):
       if form.cleaned_data['overwrite']:
         hql += " OVERWRITE"
       hql += " INTO TABLE "
-      hql += "`%s`" % (table,)
+      hql += "`%s.%s`" % (database, table,)
       if form.partition_columns:
         hql += " PARTITION ("
         vals = []
@@ -365,21 +384,21 @@ def load_table(request, table):
         hql += ", ".join(vals)
         hql += ")"
 
-      on_success_url = reverse(get_app_name(request) + ':describe_table', kwargs={'table': table})
+      on_success_url = reverse(get_app_name(request) + ':describe_table', kwargs={'database': database, 'table': table})
       return confirm_query(request, hql, on_success_url)
   else:
     form = beeswax.forms.LoadDataForm(table_obj)
-    return render("load_table.mako", request, dict(form=form, table=table, action=request.get_full_path()))
+    return render("load_table.mako", request, {'form': form, 'table': table, 'action': request.get_full_path()})
 
 
-def describe_partitions(request, table):
+def describe_partitions(request, database, table):
   db = dbms.get(request.user)
 
-  table_obj = db.get_table('default', table)
+  table_obj = db.get_table(database, table)
   if not table_obj.partition_keys:
     raise PopupException(_("Table '%(table)s' is not partitioned.") % {'table': table})
 
-  partitions = db.get_partitions("default", table_obj, max_parts=None)
+  partitions = db.get_partitions(database, table_obj, max_parts=None)
 
   return render("describe_partitions.mako", request,
                 dict(table=table_obj, partitions=partitions, request=request))
@@ -420,8 +439,13 @@ def execute_query(request, design_id=None):
   design = safe_get_design(request, query_type, design_id)
   on_success_url = request.REQUEST.get('on_success_url')
 
+  query_server = get_query_server_config(app_name, requires_ddl=False)
+  db = dbms.get(request.user, query_server)
+  databases = ((db, db) for db in db.get_databases())
+
   if request.method == 'POST':
     form.bind(request.POST)
+    form.query.fields['database'].choices =  databases # Could not do it in the form
 
     to_explain = request.POST.has_key('button-explain')
     to_submit = request.POST.has_key('button-submit')
@@ -442,7 +466,6 @@ def execute_query(request, design_id=None):
 
       if to_explain or to_submit:
         query_str = form.query.cleaned_data["query"]
-        query_server = get_query_server_config(app_name, requires_ddl=False)
 
         # (Optional) Parameterization.
         parameterization = get_parameterization(request, query_str, form, design, to_explain)
@@ -459,7 +482,6 @@ def execute_query(request, design_id=None):
         except BeeswaxException, ex:
           print ex.errorCode
           print ex.SQLState
-          db = dbms.get(request.user, query_server)
           error_message, log = expand_exception(ex, db)
   else:
     if design.id is not None:
@@ -469,6 +491,7 @@ def execute_query(request, design_id=None):
     else:
       # New design
       form.bind()
+    form.query.fields['database'].choices = databases # Could not do it in the form
 
   return render('execute.mako', request, {
     'action': action,
@@ -809,12 +832,14 @@ def confirm_query(request, query, on_success_url=None):
   mform = QueryForm()
   mform.bind()
   mform.query.initial = dict(query=query)
+
   return render('execute.mako', request, {
     'form': mform,
     'action': reverse(get_app_name(request) + ':execute_query'),
     'error_message': None,
     'design': None,
     'on_success_url': on_success_url,
+    'design': None,
   })
 
 
@@ -954,8 +979,10 @@ def safe_get_design(request, design_type, design_id=None):
       design = models.SavedQuery.get(design_id, request.user, design_type)
     except models.SavedQuery.DoesNotExist:
       messages.error(request, _('Design does not exist'))
+
   if design is None:
     design = models.SavedQuery(owner=request.user, type=design_type)
+
   return design
 
 def get_parameterization(request, query_str, form, design, is_explain):
@@ -1043,7 +1070,7 @@ def execute_directly(request, query, query_server=None, design=None, tablename=N
   """
   execute_directly(request, query_msg, tablename, design) -> HTTP response for execution
 
-  This method wraps around dbms.execute_directly() to take care of the HTTP response
+  This method wraps around dbms.execute_query() to take care of the HTTP response
   after the execution.
 
     query
@@ -1074,7 +1101,12 @@ def execute_directly(request, query, query_server=None, design=None, tablename=N
   if design is not None:
     authorized_get_design(request, design.id)
 
-  history_obj = dbms.get(request.user, query_server).execute_query(query, design)
+  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})
 
   # Prepare the GET params for the watch_url
@@ -1083,7 +1115,7 @@ def execute_directly(request, query, query_server=None, design=None, tablename=N
   if design:
     get_dict['context'] = make_query_context('design', design.id)
   elif tablename:
-    get_dict['context'] = make_query_context('table', tablename)
+    get_dict['context'] = make_query_context('table', '%s:%s' % (tablename, database))
 
   # (2) on_success_url
   if on_success_url:

+ 1 - 1
apps/filebrowser/src/filebrowser/templates/listdir_components.mako

@@ -674,7 +674,7 @@ from django.utils.translation import ugettext as _
 
             self.page = ko.observable(new Page(page));
             self.recordsPerPageChoices = ["15", "30", "45", "60", "100", "200"],
-                    self.recordsPerPage = ko.observable("30");
+            self.recordsPerPage = ko.observable("30");
             self.targetPageNum = ko.observable(1);
             self.targetPath = ko.observable("${current_request_path}");