Эх сурвалжийг харах

HUE-1699 [rdbms] Create RDBMS app

- Create libs for mysql, postgresql
- New RDBMS
Abraham Elmahrek 12 жил өмнө
parent
commit
50cc161
47 өөрчлөгдсөн 3291 нэмэгдсэн , 57 устгасан
  1. 4 2
      apps/Makefile
  2. 2 2
      apps/beeswax/src/beeswax/create_table.py
  3. 5 9
      apps/beeswax/src/beeswax/design.py
  4. 11 6
      apps/beeswax/src/beeswax/models.py
  5. 88 9
      apps/beeswax/src/beeswax/server/dbms.py
  6. 2 3
      apps/beeswax/src/beeswax/server/hive_server2_lib.py
  7. 103 0
      apps/beeswax/src/beeswax/server/mysql_lib.py
  8. 95 0
      apps/beeswax/src/beeswax/server/postgresql_lib.py
  9. 84 0
      apps/beeswax/src/beeswax/server/rdbms_base_lib.py
  10. 3 0
      apps/beeswax/src/beeswax/templates/layout.mako
  11. 6 6
      apps/beeswax/src/beeswax/tests.py
  12. 16 15
      apps/beeswax/src/beeswax/views.py
  13. 3 3
      apps/metastore/src/metastore/views.py
  14. 24 0
      apps/rdbms/Makefile
  15. 2 0
      apps/rdbms/babel.cfg
  16. 1 0
      apps/rdbms/hueversion.py
  17. 29 0
      apps/rdbms/setup.py
  18. 15 0
      apps/rdbms/src/rdbms/__init__.py
  19. 276 0
      apps/rdbms/src/rdbms/api.py
  20. 83 0
      apps/rdbms/src/rdbms/conf.py
  21. 116 0
      apps/rdbms/src/rdbms/design.py
  22. 38 0
      apps/rdbms/src/rdbms/forms.py
  23. 44 0
      apps/rdbms/src/rdbms/locale/de/LC_MESSAGES/django.po
  24. 42 0
      apps/rdbms/src/rdbms/locale/en/LC_MESSAGES/django.po
  25. 41 0
      apps/rdbms/src/rdbms/locale/en_US.pot
  26. 44 0
      apps/rdbms/src/rdbms/locale/es/LC_MESSAGES/django.po
  27. 44 0
      apps/rdbms/src/rdbms/locale/fr/LC_MESSAGES/django.po
  28. 44 0
      apps/rdbms/src/rdbms/locale/ja/LC_MESSAGES/django.po
  29. 44 0
      apps/rdbms/src/rdbms/locale/ko/LC_MESSAGES/django.po
  30. 44 0
      apps/rdbms/src/rdbms/locale/pt/LC_MESSAGES/django.po
  31. 44 0
      apps/rdbms/src/rdbms/locale/pt_BR/LC_MESSAGES/django.po
  32. 44 0
      apps/rdbms/src/rdbms/locale/zh_CN/LC_MESSAGES/django.po
  33. 23 0
      apps/rdbms/src/rdbms/settings.py
  34. 568 0
      apps/rdbms/src/rdbms/templates/execute.mako
  35. 48 0
      apps/rdbms/src/rdbms/tests.py
  36. 52 0
      apps/rdbms/src/rdbms/urls.py
  37. 117 0
      apps/rdbms/src/rdbms/views.py
  38. BIN
      apps/rdbms/static/art/icon_rdbms_24.png
  39. BIN
      apps/rdbms/static/help/images/23888161.png
  40. 663 0
      apps/rdbms/static/help/index.html
  41. 232 0
      apps/rdbms/static/js/rdbms.vm.js
  42. 17 0
      desktop/conf.dist/hue.ini
  43. 17 0
      desktop/conf/pseudo-distributed.ini.tmpl
  44. 4 1
      desktop/core/src/desktop/settings.py
  45. 3 0
      desktop/core/src/desktop/templates/common_header.mako
  46. 1 1
      desktop/core/src/desktop/views.py
  47. 105 0
      desktop/core/static/js/codemirror-sql-hint.js

+ 4 - 2
apps/Makefile

@@ -45,7 +45,8 @@ APPS := about \
   search \
   hbase \
   sqoop \
-  zookeeper
+  zookeeper \
+  rdbms
 
 ################################################
 # Install all applications into the Desktop environment
@@ -119,7 +120,8 @@ I18N_APPS := about \
   search \
   hbase \
   sqoop \
-  zookeeper
+  zookeeper \
+  rdbms
 
 COMPILE_LOCALE_TARGETS := $(I18N_APPS:%=.recursive-compile-locales/%)
 compile-locales: $(COMPILE_LOCALE_TARGETS)

+ 2 - 2
apps/beeswax/src/beeswax/create_table.py

@@ -251,8 +251,8 @@ def _submit_create_and_load(request, create_hql, table_name, path, do_load, data
 
   query = hql_query(create_hql, database=database)
   return execute_directly(request, query,
-                          on_success_url=on_success_url,
-                          on_success_params=on_success_params)
+                                on_success_url=on_success_url,
+                                on_success_params=on_success_params)
 
 
 def _delim_preview(fs, file_form, encoding, file_types, delimiters):

+ 5 - 9
apps/beeswax/src/beeswax/design.py

@@ -18,10 +18,7 @@
 """
 The HQLdesign class can (de)serialize a design to/from a QueryDict.
 """
-try:
-  import json
-except ImportError:
-  import simplejson as json
+import json
 
 import logging
 import re
@@ -44,7 +41,7 @@ def hql_query(hql, database='default'):
   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']['query'] = strip_trailing_semicolon(hql)
   data_dict['query']['database'] = database
   hql_design = HQLdesign()
   hql_design._data_dict = data_dict
@@ -52,7 +49,6 @@ def hql_query(hql, database='default'):
   return hql_design
 
 
-
 class HQLdesign(object):
   """
   Represents an HQL design, with methods to perform (de)serialization.
@@ -163,8 +159,8 @@ class HQLdesign(object):
 
   @property
   def statements(self):
-    hql_query = _strip_trailing_semicolon(self.hql_query)
-    return [_strip_trailing_semicolon(statement.strip()) for statement in split_statements(hql_query)]
+    hql_query = strip_trailing_semicolon(self.hql_query)
+    return [strip_trailing_semicolon(statement.strip()) for statement in split_statements(hql_query)]
 
 
 def split_statements(hql):
@@ -255,7 +251,7 @@ def denormalize_formset_dict(data_dict_list, formset, attr_list):
 
 _SEMICOLON_WHITESPACE = re.compile(";\s*$")
 
-def _strip_trailing_semicolon(query):
+def strip_trailing_semicolon(query):
   """As a convenience, we remove trailing semicolons from queries."""
   s = _SEMICOLON_WHITESPACE.split(query, 2)
   if len(s) > 1:

+ 11 - 6
apps/beeswax/src/beeswax/models.py

@@ -31,10 +31,11 @@ from enum import Enum
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.models import Document
 
-from beeswax.design import HQLdesign, hql_query
 from TCLIService.ttypes import TSessionHandle, THandleIdentifier,\
   TOperationState, TOperationHandle, TOperationType
 
+from beeswax.design import HQLdesign
+
 
 LOG = logging.getLogger(__name__)
 
@@ -43,7 +44,9 @@ QUERY_SUBMISSION_TIMEOUT = datetime.timedelta(0, 60 * 60)               # 1 hour
 # Constants for DB fields, hue ini
 BEESWAX = 'beeswax'
 HIVE_SERVER2 = 'hiveserver2'
-QUERY_TYPES = (HQL, IMPALA) = range(2)
+MYSQL = 'mysql'
+POSTGRESQL = 'postgresql'
+QUERY_TYPES = (HQL, IMPALA, RDBMS) = range(3)
 
 
 class QueryHistory(models.Model):
@@ -51,7 +54,7 @@ class QueryHistory(models.Model):
   Holds metadata about all queries that have been executed.
   """
   STATE = Enum('submitted', 'running', 'available', 'failed', 'expired')
-  SERVER_TYPE = ((BEESWAX, 'Beeswax'), (HIVE_SERVER2, 'Hive Server 2'))
+  SERVER_TYPE = ((BEESWAX, 'Beeswax'), (HIVE_SERVER2, 'Hive Server 2'), (MYSQL, 'MySQL'), (POSTGRESQL, 'PostgreSQL'))
 
   owner = models.ForeignKey(User, db_index=True)
   query = models.TextField()
@@ -95,6 +98,8 @@ class QueryHistory(models.Model):
   def get_type_name(query_type):
     if query_type == IMPALA:
       return 'impala'
+    elif query_type == RDBMS:
+      return 'rdbms'
     else:
       return 'beeswax'
 
@@ -203,7 +208,7 @@ class SavedQuery(models.Model):
   DEFAULT_NEW_DESIGN_NAME = _('My saved query')
   AUTO_DESIGN_SUFFIX = _(' (new)')
   TYPES = QUERY_TYPES
-  TYPES_MAPPING = {'beeswax': HQL, 'hql': HQL, 'impala': IMPALA}
+  TYPES_MAPPING = {'beeswax': HQL, 'hql': HQL, 'impala': IMPALA, 'rdbms': RDBMS}
 
   type = models.IntegerField(null=False)
   owner = models.ForeignKey(User, db_index=True)
@@ -236,12 +241,12 @@ class SavedQuery(models.Model):
     return design
 
   @classmethod
-  def create_empty(cls, app_name, owner):
+  def create_empty(cls, app_name, owner, data):
     query_type = SavedQuery.TYPES_MAPPING[app_name]
     design = SavedQuery(owner=owner, type=query_type)
     design.name = SavedQuery.DEFAULT_NEW_DESIGN_NAME
     design.desc = ''
-    design.data = hql_query('').dumps()
+    design.data = data
     design.is_auto = True
     design.save()
     return design

+ 88 - 9
apps/beeswax/src/beeswax/server/dbms.py

@@ -39,16 +39,25 @@ LOG = logging.getLogger(__name__)
 
 
 def get(user, query_server=None):
-  # Avoid circular dependency
-  from beeswax.server.hive_server2_lib import HiveServerClientCompatible, HiveServerClient
-
   if query_server is None:
     query_server = get_query_server_config()
 
-  return Dbms(HiveServerClientCompatible(HiveServerClient(query_server, user)), QueryHistory.SERVER_TYPE[1][0])
+  if query_server['server_name'] in ('impala', 'beeswax'):
+    # Avoid circular dependency
+    from beeswax.server.hive_server2_lib import HiveServerClientCompatible, HiveServerClient
+
+    return HS2Dbms(HiveServerClientCompatible(HiveServerClient(query_server, user)), QueryHistory.SERVER_TYPE[1][0])
+  elif query_server['server_name'] == 'mysql':
+    from beeswax.server.mysql_lib import MySQLClient
+
+    return Rdbms(MySQLClient(query_server, user), QueryHistory.SERVER_TYPE[2][0])
+  elif query_server['server_name'] in ('postgresql', 'postgresql_psycopg2'):
+    from beeswax.server.postgresql_lib import PostgreSQLClient
 
+    return Rdbms(PostgreSQLClient(query_server, user), QueryHistory.SERVER_TYPE[2][0])
 
-def get_query_server_config(name='beeswax'):
+
+def get_query_server_config(name='beeswax', server=None):
   if name == 'impala':
     from impala.conf import SERVER_HOST as IMPALA_SERVER_HOST, SERVER_PORT as IMPALA_SERVER_PORT, \
         IMPALA_PRINCIPAL, IMPERSONATION_ENABLED
@@ -60,6 +69,28 @@ def get_query_server_config(name='beeswax'):
         'principal': IMPALA_PRINCIPAL.get(),
         'impersonation_enabled': IMPERSONATION_ENABLED.get()
     }
+  elif name == 'rdbms':
+    from rdbms.conf import RDBMS
+
+    if not server or server not in RDBMS:
+      keys = RDBMS.keys()
+      name = keys and keys[0] or None
+    else:
+      name = server
+
+    if name:
+      query_server = {
+        'server_name': RDBMS[name].ENGINE.get().split('.')[-1],
+        'server_host': RDBMS[name].HOST.get(),
+        'server_port': RDBMS[name].PORT.get(),
+        'username': RDBMS[name].USER.get(),
+        'password': RDBMS[name].PASSWORD.get(),
+        'password': RDBMS[name].PASSWORD.get(),
+        'alias': name
+      }
+    else:
+      query_server = {}
+
   else:
     kerberos_principal = hive_site.get_hiveserver2_kerberos_principal(HIVE_SERVER_HOST.get())
 
@@ -69,7 +100,8 @@ def get_query_server_config(name='beeswax'):
         'server_port': HIVE_SERVER_PORT.get(),
         'principal': kerberos_principal
     }
-    LOG.debug("Query Server: %s" % query_server)
+
+  LOG.debug("Query Server: %s" % query_server)
 
   return query_server
 
@@ -86,13 +118,14 @@ class NoSuchObjectException: pass
 
 
 class Dbms:
-  """SQL"""
-
   def __init__(self, client, server_type):
     self.client = client
     self.server_type = server_type
 
 
+class HS2Dbms(Dbms):
+  """SQL"""
+
   def get_table(self, database, table_name):
     # DB name not supported in SHOW PARTITIONS required in Table
     self.use(database)
@@ -315,13 +348,14 @@ class Dbms:
   def execute_and_wait(self, query, timeout_sec=30.0):
     """
     Run query and check status until it finishes or timeouts.
+
+    Check status until it finishes or timeouts.
     """
     SLEEP_INTERVAL = 0.5
 
     handle = self.client.query(query)
     curr = time.time()
     end = curr + timeout_sec
-
     while curr <= end:
       state = self.client.get_state(handle)
       if state not in (QueryHistory.STATE.running, QueryHistory.STATE.submitted):
@@ -443,6 +477,51 @@ class Dbms:
     return ""
 
 
+class Rdbms(Dbms):
+  def get_table(self, database, table_name):
+    return self.client.get_table(database, table_name)
+
+  def get_databases(self):
+    return self.client.get_databases()
+
+  def execute_query(self, query, design):
+    sql_query = query.sql_query
+    query_history = QueryHistory.build(
+      owner=self.client.user,
+      query=sql_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.available.index,
+      design=design,
+      notify=False,
+      query_type=query.query['type'],
+      statement_number=0
+    )
+    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
+
+  def explain(self, statement):
+    return self.client.explain(statement)
+
+  def use(self, database):
+    self.client.use(database)
+
+  def execute_and_wait(self, query, timeout_sec=30.0):
+    """
+    Run query
+
+    Simply run query irrespective of timeout.
+    Timeout exists to comply with interface.
+    """
+
+    return self.client.query(query)
+
+
 class Table:
   """
   Represents the metadata of a Hive Table.

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

@@ -17,7 +17,6 @@
 
 import logging
 import re
-import thrift
 
 from operator import itemgetter
 
@@ -35,7 +34,7 @@ from beeswax import conf
 from beeswax import hive_site
 from beeswax.models import Session, HiveServerQueryHandle, HiveServerQueryHistory
 from beeswax.server.dbms import Table, NoSuchObjectException, DataTable,\
-  QueryServerException
+                                QueryServerException
 
 
 LOG = logging.getLogger(__name__)
@@ -612,7 +611,7 @@ class ResultMetaCompatible:
     self.in_tablename = True
 
 
-class HiveServerClientCompatible:
+class HiveServerClientCompatible(object):
   """Same API as Beeswax"""
 
   def __init__(self, client):

+ 103 - 0
apps/beeswax/src/beeswax/server/mysql_lib.py

@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+# 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.
+
+import logging
+
+try:
+    import MySQLdb as Database
+except ImportError, e:
+    from django.core.exceptions import ImproperlyConfigured
+    raise ImproperlyConfigured("Error loading MySQLdb module: %s" % e)
+
+# We want version (1, 2, 1, 'final', 2) or later. We can't just use
+# lexicographic ordering in this check because then (1, 2, 1, 'gamma')
+# inadvertently passes the version test.
+version = Database.version_info
+if (version < (1,2,1) or (version[:3] == (1, 2, 1) and
+        (len(version) < 5 or version[3] != 'final' or version[4] < 2))):
+    from django.core.exceptions import ImproperlyConfigured
+    raise ImproperlyConfigured("MySQLdb-1.2.1p2 or newer is required; you have %s" % Database.__version__)
+
+from beeswax.server.rdbms_base_lib import BaseRDBMSDataTable, BaseRDBMSResult, BaseRDMSClient
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DataTable(BaseRDBMSDataTable): pass
+
+
+class Result(BaseRDBMSResult): pass
+
+
+class MySQLClient(BaseRDMSClient):
+  """Same API as Beeswax"""
+
+  data_table_cls = DataTable
+  result_cls = Result
+
+  def __init__(self, *args, **kwargs):
+    super(MySQLClient, self).__init__(*args, **kwargs)
+    self.connection = Database.connect(**self._conn_params)
+
+
+  @property
+  def _conn_params(self):
+    return {
+      'user': self.query_server['username'],
+      'passwd': self.query_server['password'],
+      'host': self.query_server['server_host'],
+      'port': self.query_server['server_port']
+    }
+
+
+  def use(self, database):
+    cursor = self.connection.cursor()
+    cursor.execute("USE %s" % database)
+    self.connection.commit()
+
+
+  def execute_statement(self, statement):
+    cursor = self.connection.cursor()
+    cursor.execute(statement)
+    self.connection.commit()
+    if cursor.description:
+      columns = [column[0] for column in cursor.description]
+    else:
+      columns = []
+    return self.data_table_cls(cursor, columns)
+
+
+  def get_databases(self):
+    cursor = self.connection.cursor()
+    cursor.execute("SHOW DATABASES")
+    self.connection.commit()
+    return [row[0] for row in cursor.fetchall()]
+
+
+  def get_tables(self, database, table_names):
+    cursor = self.connection.cursor()
+    cursor.execute("SHOW TABLES")
+    self.connection.commit()
+    return [row[0] for row in cursor.fetchall()]
+
+
+  def get_columns(self, database, table):
+    cursor = self.connection.cursor()
+    cursor.execute("SHOW COLUMNS %s.%s" % (database, table))
+    self.connection.commit()
+    return [row[0] for row in cursor.fetchall()]

+ 95 - 0
apps/beeswax/src/beeswax/server/postgresql_lib.py

@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+# 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.
+
+import logging
+
+try:
+    import psycopg2 as Database
+except ImportError, e:
+    from django.core.exceptions import ImproperlyConfigured
+    raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e)
+
+from beeswax.server.rdbms_base_lib import BaseRDBMSDataTable, BaseRDBMSResult, BaseRDMSClient
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DataTable(BaseRDBMSDataTable): pass
+
+
+class Result(BaseRDBMSResult): pass
+
+
+class PostgreSQLClient(BaseRDMSClient):
+  """Same API as Beeswax"""
+
+  data_table_cls = DataTable
+  result_cls = Result
+
+  def __init__(self, *args, **kwargs):
+    super(PostgreSQLClient, self).__init__(*args, **kwargs)
+    self.connection = Database.connect(**self._conn_params)
+
+
+  @property
+  def _conn_params(self):
+    return {
+      'user': self.query_server['username'],
+      'password': self.query_server['password'],
+      'host': self.query_server['server_host'],
+      'port': self.query_server['server_port'] == 0 and 5432 or self.query_server['server_port']
+    }
+
+
+  def use(self, database):
+    conn_params = self._conn_params
+    conn_params['database'] = database
+    self.connection = Database.connect(**conn_params)
+
+
+  def execute_statement(self, statement):
+    cursor = self.connection.cursor()
+    cursor.execute(statement)
+    self.connection.commit()
+    if cursor.description:
+      columns = [column[0] for column in cursor.description]
+    else:
+      columns = []
+    return self.data_table_cls(cursor, columns)
+
+
+  def get_databases(self):
+    cursor = self.connection.cursor()
+    cursor.execute("SELECT datname FROM pg_database")
+    self.connection.commit()
+    return [row[0] for row in cursor.fetchall()]
+
+
+  def get_tables(self, database, table_names):
+    # Doesn't use database and only retrieves tables for database currently in use.
+    cursor = self.connection.cursor()
+    cursor.execute("SELECT table_schema,table_name FROM information_schema.tables")
+    self.connection.commit()
+    return [row[0] for row in cursor.fetchall()]
+
+
+  def get_columns(self, database, table):
+    cursor = self.connection.cursor()
+    cursor.execute("SHOW COLUMNS %s.%s" % (database, table))
+    self.connection.commit()
+    return [row[0] for row in cursor.fetchall()]

+ 84 - 0
apps/beeswax/src/beeswax/server/rdbms_base_lib.py

@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# 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.
+
+import logging
+
+from beeswax.server.dbms import DataTable
+
+
+LOG = logging.getLogger(__name__)
+
+
+class BaseRDBMSDataTable(DataTable):
+  def __init__(self, cursor, columns, fetch_size=1000):
+    self.cursor = cursor
+    self.columns = columns
+    self.next = None
+    self.startRowOffset = 0
+    self.fetchSize = 1000
+
+  @property
+  def ready(self):
+    return True
+
+  @property
+  def has_more(self):
+    if not self.next:
+      self.next = list(self.cursor.fetchmany(self.fetchSize))
+    return bool(self.next)
+
+  def rows(self):
+    while self.has_more:
+      yield self.next.pop(0)
+
+
+
+class BaseRDBMSResult(object):
+  def __init__(self, data_table):
+    self.data_table = data_table
+    self.rows = data_table.rows
+    self.has_more = data_table.has_more
+    self.start_row = data_table.startRowOffset
+    self.columns = data_table.columns
+    self.ready = True
+
+
+class BaseRDMSClient(object):
+  """Same API as Beeswax"""
+
+  data_table_cls = None
+  result_cls = None
+
+  def __init__(self, query_server, user):
+    self.user = user
+    self.query_server = query_server
+
+
+  def create_result(self, datatable):
+    return self.result_cls(datatable)
+
+
+  def query(self, query, statement=0):
+    return self.execute_statement(query.get_query_statement(statement))
+
+
+  def explain(self, query):
+    q = query.get_query_statement(0)
+    if q[:8].upper().startswith('EXPLAIN'):
+      return self.execute_statement(q)
+    else:
+      return self.execute_statement('EXPLAIN ' + q)

+ 3 - 0
apps/beeswax/src/beeswax/templates/layout.mako

@@ -38,6 +38,9 @@ def is_selected(section, matcher):
                 % if app_name == 'impala':
                   <img src="/impala/static/art/icon_impala_24.png" />
                   ${ _('Impala') }
+                % elif app_name == 'rdbms':
+                  <img src="/rdbms/static/art/icon_rdbms_24.png" />
+                  ${ _('DB Query') }
                 % else:
                   <img src="/beeswax/static/art/icon_beeswax_24.png" />
                   ${ _('Beeswax') }

+ 6 - 6
apps/beeswax/src/beeswax/tests.py

@@ -52,7 +52,7 @@ from beeswax.conf import HIVE_SERVER_HOST
 from beeswax.views import collapse_whitespace
 from beeswax.test_base import make_query, wait_for_query_to_finish, verify_history, get_query_server_config,\
   HIVE_SERVER_TEST_PORT
-from beeswax.design import hql_query, _strip_trailing_semicolon
+from beeswax.design import hql_query, strip_trailing_semicolon
 from beeswax.data_export import download
 from beeswax.models import SavedQuery, QueryHistory, HQL
 from beeswax.server import dbms
@@ -1269,17 +1269,17 @@ def test_history_page():
   do_view('q-user=test_who', 0)
   do_view('q-user=:all')
 
-def test_strip_trailing_semicolon():
+def teststrip_trailing_semicolon():
   # Note that there are two queries (both an execute and an explain) scattered
   # in this file that use semicolons all the way through.
 
   # Single semicolon
-  assert_equal("foo", _strip_trailing_semicolon("foo;\n"))
-  assert_equal("foo\n", _strip_trailing_semicolon("foo\n;\n\n\n"))
+  assert_equal("foo", strip_trailing_semicolon("foo;\n"))
+  assert_equal("foo\n", strip_trailing_semicolon("foo\n;\n\n\n"))
   # Multiple semicolons: strip only last one
-  assert_equal("fo;o;", _strip_trailing_semicolon("fo;o;;     "))
+  assert_equal("fo;o;", strip_trailing_semicolon("fo;o;;     "))
   # No semicolons
-  assert_equal("foo", _strip_trailing_semicolon("foo"))
+  assert_equal("foo", strip_trailing_semicolon("foo"))
 
 def test_hadoop_extraction():
   sample_log = """

+ 16 - 15
apps/beeswax/src/beeswax/views.py

@@ -48,6 +48,8 @@ from beeswax.models import SavedQuery, make_query_context, QueryHistory
 from beeswax.server import dbms
 from beeswax.server.dbms import expand_exception, get_query_server_config, QueryServerException
 
+import rdbms.design
+
 from thrift.transport.TTransport import TTransportException
 
 
@@ -61,9 +63,9 @@ def index(request):
 Design views
 """
 
-def save_design(request, form, type, design, explicit_save):
+def save_design(request, form, type_, design, explicit_save):
   """
-  save_design(request, form, type, design, explicit_save) -> SavedQuery
+  save_design(request, form, type_, design, explicit_save) -> SavedQuery
 
   A helper method to save the design:
     * If ``explicit_save``, then we save the data in the current design.
@@ -77,12 +79,12 @@ def save_design(request, form, type, design, explicit_save):
   """
   assert form.saveform.is_valid()
 
-  if type == models.HQL:
+  if type_ == models.HQL:
     design_cls = beeswax.design.HQLdesign
-  elif type == models.IMPALA:
+  elif type_ == models.IMPALA:
     design_cls = beeswax.design.HQLdesign
   else:
-    raise ValueError(_('Invalid design type %(type)s') % {'type': type})
+    raise ValueError(_('Invalid design type %(type)s') % {'type': type_})
 
   old_design = design
   design_obj = design_cls(form)
@@ -105,7 +107,7 @@ def save_design(request, form, type, design, explicit_save):
       design.name = models.SavedQuery.DEFAULT_NEW_DESIGN_NAME
     design.is_auto = True
 
-  design.type = type
+  design.type = type_
   design.data = new_data
 
   design.save()
@@ -384,7 +386,7 @@ def execute_query(request, design_id=None):
 
   query_server = get_query_server_config(app_name)
   db = dbms.get(request.user, query_server)
-  databases = _get_db_choices(request)
+  databases = get_db_choices(request)
 
   if request.method == 'POST':
     form.bind(request.POST)
@@ -514,7 +516,7 @@ def watch_query(request, id):
 
   # Keep waiting
   # - Translate context into something more meaningful (type, data)
-  query_context = _parse_query_context(context_param)
+  query_context = parse_query_context(context_param)
 
   return render('watch_wait.mako', request, {
                 'query': query_history,
@@ -627,7 +629,7 @@ def view_results(request, id, first_row=0):
 
   handle, state = _get_query_handle_and_state(query_history)
   context_param = request.GET.get('context', '')
-  query_context = _parse_query_context(context_param)
+  query_context = parse_query_context(context_param)
 
   # To remove when Impala has start_over support
   download  = request.GET.get('download', '')
@@ -1024,7 +1026,7 @@ def _run_parameterized_query(request, design_id, explain):
   params = design_obj.get_query_dict()
   params.update(request.POST)
 
-  databases = _get_db_choices(request)
+  databases = get_db_choices(request)
   query_form.bind(params)
   query_form.query.fields['database'].choices = databases # Could not do it in the form
 
@@ -1067,7 +1069,7 @@ def _run_parameterized_query(request, design_id, explain):
 
 
 def execute_directly(request, query, query_server=None, design=None, tablename=None,
-                     on_success_url=None, on_success_params=None, **kwargs):
+                           on_success_url=None, on_success_params=None, **kwargs):
   """
   execute_directly(request, query_msg, tablename, design) -> HTTP response for execution
 
@@ -1227,9 +1229,9 @@ def _get_query_handle_and_state(query_history):
   return (handle, state)
 
 
-def _parse_query_context(context):
+def parse_query_context(context):
   """
-  _parse_query_context(context) -> ('table', <table_name>) -or- ('design', <design_obj>)
+  parse_query_context(context) -> ('table', <table_name>) -or- ('design', <design_obj>)
   """
   if not context:
     return None
@@ -1351,7 +1353,6 @@ def _list_query_history(user, querydict, page_size, prefix=""):
   if pagenum < 1:
     pagenum = 1
   db_queryset = db_queryset[ page_size * (pagenum - 1) : page_size * pagenum ]
-
   paginator = Paginator(db_queryset, page_size, total=total_count)
   page = paginator.page(pagenum)
 
@@ -1387,7 +1388,7 @@ def _update_query_state(query_history):
     query_history.save_state(state_enum)
   return True
 
-def _get_db_choices(request):
+def get_db_choices(request):
   app_name = get_app_name(request)
   query_server = get_query_server_config(app_name)
   db = dbms.get(request.user, query_server)

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

@@ -78,7 +78,7 @@ def drop_database(request):
 
     try:
       # Can't be simpler without an important refactoring
-      design = SavedQuery.create_empty(app_name='beeswax', owner=request.user)
+      design = SavedQuery.create_empty(app_name='beeswax', owner=request.user, data=hql_query('').dumps())
       query_history = db.drop_databases(databases, design)
       url = reverse('beeswax:watch_query', args=[query_history.id]) + '?on_success_url=' + reverse('metastore:databases')
       return redirect(url)
@@ -178,7 +178,7 @@ def drop_table(request, database):
     tables_objects = [db.get_table(database, table) for table in tables]
     try:
       # Can't be simpler without an important refactoring
-      design = SavedQuery.create_empty(app_name='beeswax', owner=request.user)
+      design = SavedQuery.create_empty(app_name='beeswax', owner=request.user, data=hql_query('').dumps())
       query_history = db.drop_tables(database, tables_objects, design)
       url = reverse('beeswax:watch_query', args=[query_history.id]) + '?on_success_url=' + reverse('metastore:show_tables')
       return redirect(url)
@@ -226,7 +226,7 @@ def load_table(request, database, table):
     if load_form.is_valid():
       on_success_url = reverse('metastore:describe_table', kwargs={'database': database, 'table': table.name})
       try:
-        design = SavedQuery.create_empty(app_name='beeswax', owner=request.user)
+        design = SavedQuery.create_empty(app_name='beeswax', owner=request.user, data=hql_query('').dumps())
         query_history = db.load_data(database, table, load_form, design)
         url = reverse('beeswax:watch_query', args=[query_history.id]) + '?on_success_url=' + on_success_url
         response['status'] = 0

+ 24 - 0
apps/rdbms/Makefile

@@ -0,0 +1,24 @@
+#
+# 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.
+#
+
+ifeq ($(ROOT),)
+  $(error "Error: Expect the environment variable $$ROOT to point to the Desktop installation")
+endif
+
+APP_NAME = rdbms
+include $(ROOT)/Makefile.sdk

+ 2 - 0
apps/rdbms/babel.cfg

@@ -0,0 +1,2 @@
+[python: src/rdbms/**.py]
+[mako: src/rdbms/templates/**.mako]

+ 1 - 0
apps/rdbms/hueversion.py

@@ -0,0 +1 @@
+../../VERSION

+ 29 - 0
apps/rdbms/setup.py

@@ -0,0 +1,29 @@
+# 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 setuptools import setup, find_packages
+from hueversion import VERSION
+
+setup(
+      name = "rdbms",
+      version = VERSION,
+      author = "Hue",
+      url = 'http://github.com/cloudera/hue',
+      description = "Queries against an RDBMS.",
+      packages = find_packages('src'),
+      package_dir = {'': 'src'},
+      install_requires = ['setuptools', 'desktop'],
+      entry_points = { 'desktop.sdk.application': 'rdbms=rdbms' },
+)

+ 15 - 0
apps/rdbms/src/rdbms/__init__.py

@@ -0,0 +1,15 @@
+# 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.

+ 276 - 0
apps/rdbms/src/rdbms/api.py

@@ -0,0 +1,276 @@
+#!/usr/bin/env python
+# 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.
+
+import json
+import logging
+
+from django.http import HttpResponse, Http404
+from django.utils.translation import ugettext as _
+
+from desktop.context_processors import get_app_name
+
+from beeswax import models as beeswax_models
+from beeswax.forms import SaveForm
+from beeswax.server import dbms
+from beeswax.views import authorized_get_history, safe_get_design
+
+from rdbms import conf
+from rdbms.forms import SQLForm
+from rdbms.design import SQLdesign
+from rdbms.views import save_design
+
+
+LOG = logging.getLogger(__name__)
+
+
+def servers(request):
+  servers = conf.get_server_choices()
+  servers_dict = dict(servers)
+  response = {
+    'servers': servers_dict
+  }
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def databases(request, server):
+  app_name = get_app_name(request)
+  query_server = dbms.get_query_server_config(app_name, server)
+
+  if not query_server:
+    raise Http404
+  
+  db = dbms.get(request.user, query_server)
+
+  response = {
+    'databases': db.get_databases()
+  }
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def execute_query(request, design_id=None):
+  response = {'status': -1, 'message': ''}
+
+  if request.method != 'POST':
+    response['message'] = _('A POST request is required.')
+  
+  app_name = get_app_name(request)
+  query_type = beeswax_models.SavedQuery.TYPES_MAPPING[app_name]
+  design = safe_get_design(request, query_type, design_id)
+
+  try:
+    form = get_query_form(request, design_id)
+
+    if form.is_valid():
+      design = save_design(request, SaveForm(), form, query_type, design)
+
+      query = SQLdesign(form, query_type=query_type)
+      query_server = dbms.get_query_server_config(app_name)
+      db = dbms.get(request.user, query_server)
+      query_history = db.execute_query(query, design)
+      query_history.last_state = beeswax_models.QueryHistory.STATE.expired.index
+      query_history.save()
+
+      try:
+        db.use(form.cleaned_data['database'])
+        datatable = db.execute_and_wait(query)
+        results = db.client.create_result(datatable)
+
+        response['status'] = 0
+        response['results'] = results_to_dict(results)
+        response['design'] = design.id
+      except Exception, e:
+        response['status'] = -1
+        response['message'] = str(e)
+
+    else:
+      response['message'] = _('There was an error with your query.')
+      response['errors'] = form.errors
+  except RuntimeError, e:
+    response['message']= str(e)
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def explain_query(request):
+  response = {'status': -1, 'message': ''}
+
+  if request.method != 'POST':
+    response['message'] = _('A POST request is required.')
+  
+  app_name = get_app_name(request)
+  query_type = beeswax_models.SavedQuery.TYPES_MAPPING[app_name]
+
+  try:
+    form = get_query_form(request)
+
+    if form.is_valid():
+      query = SQLdesign(form, query_type=query_type)
+      query_server = dbms.get_query_server_config(app_name)
+      db = dbms.get(request.user, query_server)
+
+      try:
+        db.use(form.cleaned_data['database'])
+        datatable = db.explain(query)
+        results = db.client.create_result(datatable)
+
+        response['status'] = 0
+        response['results'] = results_to_dict(results)
+      except Exception, e:
+        response['status'] = -1
+        response['message'] = str(e)
+
+    else:
+      response['message'] = _('There was an error with your query.')
+      response['errors'] = form.errors
+  except RuntimeError, e:
+    response['message']= str(e)
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def fetch_results(request, id, first_row=0):
+  """
+  Returns the results of the QueryHistory with the given id.
+
+  The query results MUST be ready.
+
+  If ``first_row`` is 0, restarts (if necessary) the query read.  Otherwise, just
+  spits out a warning if first_row doesn't match the servers conception.
+  Multiple readers will produce a confusing interaction here, and that's known.
+  """
+  first_row = long(first_row)
+  results = type('Result', (object,), {
+                'rows': 0,
+                'columns': [],
+                'has_more': False,
+                'start_row': 0,
+            })
+  fetch_error = False
+  error_message = ''
+
+  query_history = authorized_get_history(request, id, must_exist=True)
+  query_server = query_history.get_query_server_config()
+  design = SQLdesign.loads(query_history.design.data)
+  db = dbms.get(request.user, query_server)
+
+  try:
+    database = design.query.get('database', 'default')
+    db.use(database)
+    datatable = db.execute_and_wait(design)
+    results = db.client.create_result(datatable)
+    status = 0
+  except Exception, e:
+    fetch_error = True
+    error_message = str(e)
+    status = -1
+
+  response = {
+    'status': status,
+    'message': fetch_error and error_message or '',
+    'results': results_to_dict(results)
+  }
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def save_query(request, design_id=None):
+  response = {'status': -1, 'message': ''}
+
+  if request.method != 'POST':
+    response['message'] = _('A POST request is required.')
+
+  app_name = get_app_name(request)
+  query_type = beeswax_models.SavedQuery.TYPES_MAPPING[app_name]
+  design = safe_get_design(request, query_type, design_id)
+
+  try:
+    save_form = SaveForm(request.POST.copy())
+    query_form = get_query_form(request, design_id)
+
+    if query_form.is_valid() and save_form.is_valid():
+      design = save_design(request, save_form, query_form, query_type, design, True)
+      response['design_id'] = design.id
+      response['status'] = 0
+    else:
+      response['errors'] = query_form.errors
+  except RuntimeError, e:
+    response['message'] = str(e)
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def fetch_saved_query(request, design_id):
+  response = {'status': -1, 'message': ''}
+
+  if request.method != 'GET':
+    response['message'] = _('A GET request is required.')
+
+  app_name = get_app_name(request)
+  query_type = beeswax_models.SavedQuery.TYPES_MAPPING[app_name]
+  design = safe_get_design(request, query_type, design_id)
+
+  response['design'] = design_to_dict(design)
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def results_to_dict(results):
+  data = {}
+  rows = []
+  for row in results.rows():
+    rows.append(dict(zip(results.columns, row)))
+  data['rows'] = rows
+  data['start_row'] = results.start_row
+  data['has_more'] = results.has_more
+  data['columns'] = results.columns
+  return data
+
+
+def design_to_dict(design):
+  sql_design = SQLdesign.loads(design.data)
+  return {
+    'id': design.id,
+    'query': sql_design.sql_query,
+    'name': design.name,
+    'desc': design.desc,
+    'server': sql_design.server,
+    'database': sql_design.database
+  }
+
+
+def get_query_form(request, design_id=None):
+  app_name = get_app_name(request)
+  servers = conf.get_server_choices()
+
+  # Get database choices
+  app_name = get_app_name(request)
+  query_server = dbms.get_query_server_config(app_name, request.POST.get('server', None))
+
+  if not query_server:
+    raise RuntimeError(_("Server specified doesn't exist."))
+  
+  db = dbms.get(request.user, query_server)
+  databases = [(database, database) for database in db.get_databases()]
+
+  if not databases:
+    raise RuntimeError(_("No databases are available. Permissions could be missing."))
+
+  form = SQLForm(request.POST)
+  form.fields['server'].choices = servers # Could not do it in the form
+  form.fields['database'].choices = databases # Could not do it in the form
+
+  return form

+ 83 - 0
apps/rdbms/src/rdbms/conf.py

@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+# 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 django.utils.translation import ugettext_lazy as _t
+from desktop.lib.conf import Config, UnspecifiedConfigSection,\
+                             ConfigSection, coerce_json_dict
+from desktop.conf import coerce_database
+
+
+RDBMS = UnspecifiedConfigSection(
+  key="databases",
+  help=_t("RDBMS server configurations."),
+  each=ConfigSection(
+    help=_t("RDBMS server configuration."),
+    members=dict(
+      NICE_NAME=Config(
+        key='nice_name',
+        help=_t('Nice name of server.'),
+        type=str,
+        default=None,
+      ),
+      ENGINE=Config(
+        key='engine',
+        help=_t('Database engine, such as postgresql_psycopg2, mysql, or sqlite3.'),
+        type=coerce_database,
+        default='django.db.backends.sqlite3',
+      ),
+      USER=Config(
+        key='user',
+        help=_t('Database username.'),
+        type=str,
+        default='',
+      ),
+      PASSWORD=Config(
+        key='password',
+        help=_t('Database password.'),
+        type=str,
+        default='',
+      ),
+      HOST=Config(
+        key='host',
+        help=_t('Database host.'),
+        type=str,
+        default='',
+      ),
+      PORT=Config(
+        key='port',
+        help=_t('Database port.'),
+        type=int,
+        default=0,
+      ),
+      OPTIONS=Config(
+        key='options',
+        help=_t('Database options to send to the server when connecting.'),
+        type=coerce_json_dict,
+        default='{}'
+      )
+    )
+  )
+)
+
+
+def config_validator(user):
+  res = []
+  return res
+
+
+def get_server_choices():
+  return [(alias, RDBMS[alias].NICE_NAME.get() or alias) for alias in RDBMS]

+ 116 - 0
apps/rdbms/src/rdbms/design.py

@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# 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.
+
+"""
+The HQLdesign class can (de)serialize a design to/from a QueryDict.
+"""
+import json
+import logging
+
+import django.http
+from django.utils.translation import ugettext as _
+
+from beeswax.design import normalize_form_dict, denormalize_form_dict, strip_trailing_semicolon,\
+                           split_statements
+
+
+LOG = logging.getLogger(__name__)
+
+SERIALIZATION_VERSION = "0.0.1"
+
+
+class SQLdesign(object):
+  """
+  Represents an SQL design, with methods to perform (de)serialization.
+  """
+  _QUERY_ATTRS = [ 'query', 'type', 'database', 'server' ]
+
+  def __init__(self, form=None, query_type=None):
+    """Initialize the design from a valid form data."""
+    if form is not None:
+      self._data_dict = dict(query = normalize_form_dict(form, SQLdesign._QUERY_ATTRS))
+      if query_type is not None:
+        self._data_dict['query']['type'] = query_type
+
+  def dumps(self):
+    """Returns the serialized form of the design in a string"""
+    dic = self._data_dict.copy()
+    dic['VERSION'] = SERIALIZATION_VERSION
+    return json.dumps(dic)
+
+  @property
+  def sql_query(self):
+    return self._data_dict['query']['query']
+
+  @property
+  def query(self):
+    return self._data_dict['query'].copy()
+
+  @property
+  def server(self):
+    return self._data_dict['query']['server']
+
+  @property
+  def database(self):
+    return self._data_dict['query']['database']
+
+  def get_query_dict(self):
+    # 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()
+
+    res = django.http.QueryDict('', mutable=True)
+    res.update(denormalize_form_dict(
+                self._data_dict['query'], mform.query, SQLdesign._QUERY_ATTRS))
+    return res
+
+  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):
+    sql_query = strip_trailing_semicolon(self.sql_query)
+    return [strip_trailing_semicolon(statement.strip()) for statement in split_statements(sql_query)]
+
+  @staticmethod
+  def loads(data):
+    """Returns SQLdesign from the serialized form"""
+    dic = json.loads(data)
+    dic = dict(map(lambda k: (str(k), dic.get(k)), dic.keys()))
+    if dic['VERSION'] != SERIALIZATION_VERSION:
+      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 'server' not in dic['query']:
+      raise RuntimeError(_('No server!'))
+    if 'database' not in dic['query']:
+      raise RuntimeError(_('No database!'))
+
+    design = SQLdesign()
+    design._data_dict = dic
+    return design

+ 38 - 0
apps/rdbms/src/rdbms/forms.py

@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# 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 django import forms
+from django.utils.translation import ugettext_lazy as _t
+
+
+class SQLForm(forms.Form):
+  query = forms.CharField(label=_t("Query Editor"),
+                          required=True,
+                          widget=forms.Textarea(attrs={'class': 'beeswax_query'}))
+  is_parameterized = forms.BooleanField(required=False, initial=True)
+  email_notify = forms.BooleanField(required=False, initial=False)
+  type = forms.IntegerField(required=False, initial=0)
+  server = forms.ChoiceField(required=False,
+                             label='',
+                             choices=(('default', 'default'),),
+                             initial=0,
+                             widget=forms.widgets.Select(attrs={'class': 'input-medium'}))
+  database = forms.ChoiceField(required=False,
+                           label='',
+                           choices=(('default', 'default'),),
+                           initial=0,
+                           widget=forms.widgets.Select(attrs={'class': 'input-medium'}))

+ 44 - 0
apps/rdbms/src/rdbms/locale/de/LC_MESSAGES/django.po

@@ -0,0 +1,44 @@
+# German translations for Hue.
+# Copyright (C) 2012 Cloudera
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-08-02 20:43-0700\n"
+"PO-Revision-Date: 2012-07-30 18:50-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: de <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:29
+msgid "Host of the Impala Server."
+msgstr "Host des Impala-Servers."
+
+#: src/impala/conf.py:34
+msgid "Port of the Impala Server."
+msgstr "Port des Impala-Servers."
+
+#: src/impala/conf.py:40
+#, fuzzy
+msgid "Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or 'hiveserver2'."
+msgstr "Beeswax- oder Hive Server 2-Thrift-API verwendet. Zur Auswahl stehen: \"beeswax\" oder \"hiveserver2\"."
+
+#: src/impala/conf.py:45
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr "Kerberos-Hauptname für Impala. Typischerweise 'impala/hostname.foo.com'."
+
+#: src/impala/conf.py:63
+msgid "No available Impalad to send queries to."
+msgstr "Kein Impalad zum Senden von Abfragen verfügbar."
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr "Eine POST-Anforderung ist erforderlich."
+

+ 42 - 0
apps/rdbms/src/rdbms/locale/en/LC_MESSAGES/django.po

@@ -0,0 +1,42 @@
+# English translations for Hue.
+# Copyright (C) 2013 Cloudera, Inc
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2013.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Hue VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-05-10 11:59-0700\n"
+"PO-Revision-Date: 2013-10-28 10:31-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: en <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:26
+msgid "Host of the Impala Server."
+msgstr ""
+
+#: src/impala/conf.py:31
+msgid "Port of the Impala Server."
+msgstr ""
+
+#: src/impala/conf.py:37
+msgid ""
+"Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or "
+"'hiveserver2'."
+msgstr ""
+
+#: src/impala/conf.py:42
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr ""
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr ""
+

+ 41 - 0
apps/rdbms/src/rdbms/locale/en_US.pot

@@ -0,0 +1,41 @@
+# Translations template for Hue.
+# Copyright (C) 2013 Cloudera, Inc
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2013.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Hue VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-05-10 11:59-0700\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:26
+msgid "Host of the Impala Server."
+msgstr ""
+
+#: src/impala/conf.py:31
+msgid "Port of the Impala Server."
+msgstr ""
+
+#: src/impala/conf.py:37
+msgid ""
+"Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or "
+"'hiveserver2'."
+msgstr ""
+
+#: src/impala/conf.py:42
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr ""
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr ""
+

+ 44 - 0
apps/rdbms/src/rdbms/locale/es/LC_MESSAGES/django.po

@@ -0,0 +1,44 @@
+# Spanish translations for Hue.
+# Copyright (C) 2012 Cloudera
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-08-02 20:43-0700\n"
+"PO-Revision-Date: 2012-07-30 18:50-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: es <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:29
+msgid "Host of the Impala Server."
+msgstr "Host del servidor Impala."
+
+#: src/impala/conf.py:34
+msgid "Port of the Impala Server."
+msgstr "Puerto del servidor Impala."
+
+#: src/impala/conf.py:40
+#, fuzzy
+msgid "Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or 'hiveserver2'."
+msgstr "Se utiliza la API Thrift de Beeswax o de Hive Server 2. Las opciones son: 'beeswax' o 'hiveserver2'."
+
+#: src/impala/conf.py:45
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr "Nombre de la entidad de seguridad de Kerberos para Hue. Normalmente 'hue/hostname.foo.com'."
+
+#: src/impala/conf.py:63
+msgid "No available Impalad to send queries to."
+msgstr "No hay ningún Impalad disponible al que enviar las consultas."
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr "Se necesita una solicitud POST."
+

+ 44 - 0
apps/rdbms/src/rdbms/locale/fr/LC_MESSAGES/django.po

@@ -0,0 +1,44 @@
+# French translations for Hue.
+# Copyright (C) 2012 Cloudera
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-08-02 20:43-0700\n"
+"PO-Revision-Date: 2012-07-30 18:50-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: fr <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:29
+msgid "Host of the Impala Server."
+msgstr "Hôte du serveur Impala."
+
+#: src/impala/conf.py:34
+msgid "Port of the Impala Server."
+msgstr "Port du serveur Impala."
+
+#: src/impala/conf.py:40
+#, fuzzy
+msgid "Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or 'hiveserver2'."
+msgstr "API Thrift de Beeswax ou Hive Server 2 utilisée. Les choix possibles sont : \"beeswax\" ou \"hiveserver2\"."
+
+#: src/impala/conf.py:45
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr "Nom Kerberos principal pour Impala. En général 'impala/nomhôte.foo.com'."
+
+#: src/impala/conf.py:63
+msgid "No available Impalad to send queries to."
+msgstr "Aucun Impalad disponible pour l'envoi de requêtes."
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr "Requête POST requise."
+

+ 44 - 0
apps/rdbms/src/rdbms/locale/ja/LC_MESSAGES/django.po

@@ -0,0 +1,44 @@
+# Japanese translations for Hue.
+# Copyright (C) 2012 Cloudera
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-08-02 20:43-0700\n"
+"PO-Revision-Date: 2012-07-30 18:50-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: ja <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:29
+msgid "Host of the Impala Server."
+msgstr "Impala Server のホストです。"
+
+#: src/impala/conf.py:34
+msgid "Port of the Impala Server."
+msgstr "Impala Server のポートです。"
+
+#: src/impala/conf.py:40
+#, fuzzy
+msgid "Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or 'hiveserver2'."
+msgstr "Beeswax または Hive Server 2 Thrift API を使用します。選択肢には 'beeswax' と 'hiveserver2' があります。"
+
+#: src/impala/conf.py:45
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr "Impala の Kerberos プリンシパル名。普通は 'impala/hostname.foo.com' です。"
+
+#: src/impala/conf.py:63
+msgid "No available Impalad to send queries to."
+msgstr "クエリ送信先に利用できる Impalad はありません。"
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr "POST 要求が必要です。"
+

+ 44 - 0
apps/rdbms/src/rdbms/locale/ko/LC_MESSAGES/django.po

@@ -0,0 +1,44 @@
+# Korean translations for Hue.
+# Copyright (C) 2012 Cloudera
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-08-02 20:43-0700\n"
+"PO-Revision-Date: 2012-07-30 18:50-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: ko <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:29
+msgid "Host of the Impala Server."
+msgstr "Impala 서버의 호스트입니다."
+
+#: src/impala/conf.py:34
+msgid "Port of the Impala Server."
+msgstr "Impala 서버의 포트입니다."
+
+#: src/impala/conf.py:40
+#, fuzzy
+msgid "Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or 'hiveserver2'."
+msgstr "Beeswax 또는 Hive Server 2 Thrift API를 사용 중입니다. 'beeswax' 또는 'hiveserver2' 중에서 선택할 수 있습니다."
+
+#: src/impala/conf.py:45
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr "Impala의 Kerberos 원칙 이름입니다. 일반적으로 'impala/hostname.foo.com'입니다."
+
+#: src/impala/conf.py:63
+msgid "No available Impalad to send queries to."
+msgstr "쿼리를 전송할 사용 가능한 Impalad가 없습니다."
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr "POST 요청이 필요합니다."
+

+ 44 - 0
apps/rdbms/src/rdbms/locale/pt/LC_MESSAGES/django.po

@@ -0,0 +1,44 @@
+# Portuguese translations for Hue.
+# Copyright (C) 2012 Cloudera
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-08-02 20:43-0700\n"
+"PO-Revision-Date: 2012-07-30 18:50-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: pt <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:29
+msgid "Host of the Impala Server."
+msgstr "Anfitrião do Impala Server."
+
+#: src/impala/conf.py:34
+msgid "Port of the Impala Server."
+msgstr "Porta do servidor Impala."
+
+#: src/impala/conf.py:40
+#, fuzzy
+msgid "Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or 'hiveserver2'."
+msgstr "Beeswax ou Hive Server 2 Thrift API utilizado. As opções são: \"beeswax\" ou \"hiveserver2\"."
+
+#: src/impala/conf.py:45
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr "Nome principal do Kerberos para o Impala. Normalmente \"impala/hostname.foo.com\"."
+
+#: src/impala/conf.py:63
+msgid "No available Impalad to send queries to."
+msgstr "Sem Impala disponível para enviar consultas."
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr "É necessário um pedido POST."
+

+ 44 - 0
apps/rdbms/src/rdbms/locale/pt_BR/LC_MESSAGES/django.po

@@ -0,0 +1,44 @@
+# Portuguese (Brazil) translations for Hue.
+# Copyright (C) 2012 Cloudera
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-08-02 20:43-0700\n"
+"PO-Revision-Date: 2012-07-30 18:50-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: pt_BR <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:29
+msgid "Host of the Impala Server."
+msgstr "Host do servidor Impala."
+
+#: src/impala/conf.py:34
+msgid "Port of the Impala Server."
+msgstr "Porta do servidor Impala."
+
+#: src/impala/conf.py:40
+#, fuzzy
+msgid "Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or 'hiveserver2'."
+msgstr "API Thrift do Beeswax ou Hive Server 2 usada. As opções são: 'beeswax' ou 'hiveserver2'."
+
+#: src/impala/conf.py:45
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr "Nome principal do Kerberos para o Impala. Normalmente, 'hue/hostname.foo.com'."
+
+#: src/impala/conf.py:63
+msgid "No available Impalad to send queries to."
+msgstr "Nenhum Impala disponível para o qual enviar consultas."
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr "É necessária uma solicitação POST."
+

+ 44 - 0
apps/rdbms/src/rdbms/locale/zh_CN/LC_MESSAGES/django.po

@@ -0,0 +1,44 @@
+# Chinese (China) translations for Hue.
+# Copyright (C) 2012 Cloudera
+# This file is distributed under the same license as the Hue project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2013-08-02 20:43-0700\n"
+"PO-Revision-Date: 2012-07-30 18:50-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: zh_CN <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: src/impala/conf.py:29
+msgid "Host of the Impala Server."
+msgstr "Impala 服务器主机。"
+
+#: src/impala/conf.py:34
+msgid "Port of the Impala Server."
+msgstr "Impala 服务器端口。"
+
+#: src/impala/conf.py:40
+#, fuzzy
+msgid "Beeswax or Hive Server 2 Thrift API used. Choices are: 'beeswax' or 'hiveserver2'."
+msgstr "已使用 Beeswax 或 Hive Server 2 Thrift API。选择是:'beeswax' 或 'hiveserver2'。"
+
+#: src/impala/conf.py:45
+msgid "Kerberos principal name for Impala. Typically 'impala/hostname.foo.com'."
+msgstr "Impala 的 Kerberos 主体名称。通常为“impala/hostname.foo.com”。"
+
+#: src/impala/conf.py:63
+msgid "No available Impalad to send queries to."
+msgstr "没有可向其发送查询的 Impalad。"
+
+#: src/impala/views.py:35
+msgid "A POST request is required."
+msgstr "需要 POST 请求。"
+

+ 23 - 0
apps/rdbms/src/rdbms/settings.py

@@ -0,0 +1,23 @@
+# 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.
+
+DJANGO_APPS = ['rdbms']
+NICE_NAME = 'RDBMS UI'
+MENU_INDEX = 11
+ICON = '/rdbms/static/art/icon_rdbms_24.png'
+
+REQUIRES_HADOOP = False
+IS_URL_NAMESPACED = True

+ 568 - 0
apps/rdbms/src/rdbms/templates/execute.mako

@@ -0,0 +1,568 @@
+## 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 _
+%>
+
+${ commonheader(_('Query'), app_name, user) | n,unicode }
+
+<div class="navbar navbar-inverse navbar-fixed-top">
+  <div class="navbar-inner">
+    <div class="container-fluid">
+      <div class="nav-collapse">
+        <ul class="nav">
+          <li class="currentApp">
+            <a href="/rdbms">
+              <img src="/rdbms/static/art/icon_rdbms_24.png" />
+              ${ _('DB Query') }
+            </a>
+          </li>
+          <li class="active"><a href="${ url('rdbms:execute_query') }">${_('Query Editor')}</a></li>
+          <li><a href="${ url('rdbms:my_queries') }">${_('My Queries')}</a></li>
+          <li><a href="${ url('rdbms:list_designs') }">${_('Saved Queries')}</a></li>
+          <li><a href="${ url('rdbms:list_query_history') }">${_('History')}</a></li>
+        </ul>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="container-fluid">
+  <div class="row-fluid">
+    <div class="card card-small">
+      <ul class="nav nav-pills hueBreadcrumbBar" id="breadcrumbs">
+        <li>
+          <div style="display: inline" class="dropdown">
+            ${_('Server')}&nbsp;<a data-toggle="dropdown" href="#"><strong data-bind="text: $root.server"></strong> <i class="fa fa-caret-down"></i></a>
+            <ul data-bind="foreach: Object.keys($root.servers())" class="dropdown-menu">
+              <li data-bind="click: $root.chooseServer, text: $root.servers()[$data]" class="selectable"></li>
+            </ul>
+          </div>
+        </li>
+        <li>&nbsp;&nbsp;&nbsp;&nbsp;</li>
+        <li>
+          <div style="display: inline" class="dropdown">
+            ${_('Database')}&nbsp;<a data-toggle="dropdown" href="#"><strong data-bind="text: $root.database"></strong> <i class="fa fa-caret-down"></i></a>
+            <ul data-bind="foreach: $root.databases" class="dropdown-menu">
+              <li data-bind="click: $root.chooseDatabase, text: $data" class="selectable"></li>
+            </ul>
+          </div>
+        </li>
+      </ul>
+    </div>
+  </div>
+
+  <div id="query" class="row-fluid">
+    <div class="row-fluid">
+      <div class="card card-small">
+        <div style="margin-bottom: 30px">
+          <h1 class="card-heading simple">
+            ${ _('Query Editor') }
+            % if can_edit_name:
+              :
+              <a href="javascript:void(0);"
+                 id="query-name"
+                 data-type="text"
+                 data-name="name"
+                 data-value="${design.name}"
+                 data-original-title="${ _('Query name') }"
+                 data-placement="right">
+              </a>
+            %endif
+          </h1>
+          % if can_edit_name:
+            <p style="margin-left: 20px">
+              <a href="javascript:void(0);"
+                 id="query-description"
+                 data-type="textarea"
+                 data-name="description"
+                 data-value="${design.desc}"
+                 data-original-title="${ _('Query description') }"
+                 data-placement="right">
+              </a>
+            </p>
+          % endif
+        </div>
+        <div class="card-body">
+          <div class="tab-content">
+            <div id="queryPane">
+
+              <div data-bind="css: {'hide': query.errors().length == 0}" class="hide alert alert-error">
+                <p><strong>${_('Your query has the following error(s):')}</strong></p>
+                <div data-bind="foreach: query.errors">
+                  <p data-bind="text: $data" class="queryErrorMessage"></p>
+                </div>
+              </div>
+
+              <textarea class="hide" tabindex="1" name="query" id="queryField"></textarea>
+
+              <div class="actions">
+                <button data-bind="click: tryExecuteQuery" type="button" id="executeQuery" class="btn btn-primary" tabindex="2">${_('Execute')}</button>
+                <button data-bind="click: trySaveQuery, css: {'hide': !$root.query.id() || $root.query.id() == -1}" type="button" class="btn hide">${_('Save')}</button>
+                <button data-bind="click: trySaveAsQuery" type="button" class="btn">${_('Save as...')}</button>
+                <button data-bind="click: tryExplainQuery" type="button" id="explainQuery" class="btn">${_('Explain')}</button>
+                &nbsp; ${_('or create a')} &nbsp;<a type="button" class="btn" href="${ url('rdbms:execute_query') }">${_('New query')}</a>
+                <br /><br />
+            </div>
+
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div data-bind="css: {'hide': rows().length == 0}" class="row-fluid hide">
+      <div class="card card-small scrollable">
+        <table class="table table-striped table-condensed resultTable" cellpadding="0" cellspacing="0" data-tablescroller-min-height-disable="true" data-tablescroller-enforce-height="true">
+          <thead>
+            <tr data-bind="foreach: columns">
+              <th data-bind="text: $data"></th>
+            </tr>
+          </thead>
+          <tbody data-bind="foreach: rows">
+            <tr data-bind="foreach: $data">
+              <td data-bind="text: $data"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+
+</div>
+
+
+<div id="saveAsQueryModal" class="modal hide fade">
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${_('Save your query')}</h3>
+  </div>
+  <div class="modal-body">
+    <form class="form-horizontal">
+      <div class="control-group" id="saveas-query-name">
+        <label class="control-label">${_('Name')}</label>
+        <div class="controls">
+          <input data-bind="value: $root.query.name" type="text" class="input-xlarge">
+        </div>
+      </div>
+      <div class="control-group">
+        <label class="control-label">${_('Description')}</label>
+        <div class="controls">
+          <input data-bind="value: $root.query.description" type="text" class="input-xlarge">
+        </div>
+      </div>
+    </form>
+  </div>
+  <div class="modal-footer">
+    <button class="btn" data-dismiss="modal">${_('Cancel')}</button>
+    <button data-bind="click: modalSaveAsQuery" class="btn btn-primary">${_('Save')}</button>
+  </div>
+</div>
+
+
+<style type="text/css">
+  h1 {
+    margin-bottom: 5px;
+  }
+
+  #filechooser {
+    min-height: 100px;
+    overflow-y: auto;
+  }
+
+  .control-group {
+    margin-bottom: 3px!important;
+  }
+
+  .control-group label {
+    float: left;
+    padding-top: 5px;
+    text-align: left;
+    width: 40px;
+  }
+
+  .hueBreadcrumb {
+    padding: 12px 14px;
+  }
+
+  .hueBreadcrumbBar {
+    padding: 0;
+    margin: 12px;
+  }
+
+  .hueBreadcrumbBar a {
+    color: #338BB8 !important;
+    display: inline !important;
+  }
+
+  .divider {
+    color: #CCC;
+  }
+
+  .param {
+    padding: 8px 8px 1px 8px;
+    margin-bottom: 5px;
+    border-bottom: 1px solid #EEE;
+  }
+
+  .remove {
+    float: right;
+  }
+
+  .selectable {
+    display: block;
+    list-style: none;
+    padding: 5px;
+    background: white;
+    cursor: pointer;
+  }
+
+  .selected, .selectable:hover {
+    background: #DDDDDD;
+  }
+
+  .CodeMirror {
+    border: 1px solid #eee;
+    margin-bottom: 20px;
+  }
+
+  .editorError {
+    color: #B94A48;
+    background-color: #F2DEDE;
+    padding: 4px;
+    font-size: 11px;
+  }
+
+  .editable-empty, .editable-empty:hover {
+    color: #666;
+    font-style: normal;
+  }
+
+  .tooltip.left {
+    margin-left: -13px;
+  }
+
+  .scrollable {
+    overflow-x: auto;
+  }
+
+  .resultTable td, .resultTable th {
+    white-space: nowrap;
+  }
+</style>
+
+<script src="/static/ext/js/knockout-min.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
+<script src="/rdbms/static/js/rdbms.vm.js"></script>
+<script src="/static/ext/js/codemirror-3.11.js"></script>
+<link rel="stylesheet" href="/static/ext/css/codemirror.css">
+<script src="/static/ext/js/codemirror-sql.js"></script>
+<script src="/static/js/codemirror-sql-hint.js"></script>
+<script src="/static/js/codemirror-show-hint.js"></script>
+
+<link href="/static/ext/css/bootstrap-editable.css" rel="stylesheet">
+<script src="/static/ext/js/bootstrap-editable.min.js"></script>
+<script src="/static/ext/js/bootstrap-editable.min.js"></script>
+
+<script src="/static/ext/js/jquery/plugins/jquery-fieldselection.js" type="text/javascript"></script>
+<script src="/beeswax/static/js/autocomplete.utils.js" type="text/javascript" charset="utf-8"></script>
+
+<script type="text/javascript" charset="utf-8">
+  var codeMirror, viewModel;
+
+  $(document).ready(function(){
+
+    var queryPlaceholder = "${_('Example: SELECT * FROM tablename, or press CTRL + space')}";
+
+    $("*[rel=tooltip]").tooltip({
+      placement: 'bottom'
+    });
+
+    var resizeTimeout = -1;
+    var winWidth = $(window).width();
+    var winHeight = $(window).height();
+
+    $(window).on("resize", function () {
+      window.clearTimeout(resizeTimeout);
+      resizeTimeout = window.setTimeout(function () {
+        // prevents endless loop in IE8
+        if (winWidth != $(window).width() || winHeight != $(window).height()) {
+          codeMirror.setSize("95%", 100);
+          winWidth = $(window).width();
+          winHeight = $(window).height();
+        }
+      }, 200);
+    });
+
+    var queryEditor = $("#queryField")[0];
+
+    var AUTOCOMPLETE_SET = CodeMirror.sqlHint;
+
+    CodeMirror.onAutocomplete = function (data, from, to) {
+      if (CodeMirror.tableFieldMagic) {
+        codeMirror.replaceRange(" ", from, from);
+        codeMirror.setCursor(from);
+        CodeMirror.showHint(codeMirror, AUTOCOMPLETE_SET);
+      }
+    };
+
+    CodeMirror.fromDot = false;
+
+    codeMirror = CodeMirror(function (elt) {
+      queryEditor.parentNode.replaceChild(elt, queryEditor);
+    }, {
+      value: queryEditor.value,
+      readOnly: false,
+      lineNumbers: true,
+      mode: "text/x-sql",
+      extraKeys: {
+        "Ctrl-Space": function () {
+          CodeMirror.fromDot = false;
+          CodeMirror.showHint(codeMirror, AUTOCOMPLETE_SET);
+        },
+        Tab: function (cm) {
+          $("#executeQuery").focus();
+        }
+      },
+      onKeyEvent: function (e, s) {
+        if (s.type == "keyup") {
+          if (s.keyCode == 190) {
+            var _line = codeMirror.getLine(codeMirror.getCursor().line);
+            var _partial = _line.substring(0, codeMirror.getCursor().ch);
+            var _table = _partial.substring(_partial.lastIndexOf(" ") + 1, _partial.length - 1);
+            if (codeMirror.getValue().toUpperCase().indexOf("FROM") > -1) {
+              hac_getTableColumns($("#id_query-database").val(), _table, codeMirror.getValue(), function (columns) {
+                var _cols = columns.split(" ");
+                for (var col in _cols){
+                  _cols[col] = "." + _cols[col];
+                }
+                CodeMirror.catalogFields = _cols.join(" ");
+                CodeMirror.fromDot = true;
+                window.setTimeout(function () {
+                  codeMirror.execCommand("autocomplete");
+                }, 100);  // timeout for IE8
+              });
+            }
+          }
+        }
+      }
+    });
+
+    var selectedLine = -1;
+    var errorWidget = null;
+    if ($(".queryErrorMessage").length > 0) {
+      var err = $(".queryErrorMessage").text().toLowerCase();
+      var firstPos = err.indexOf("line");
+      selectedLine = $.trim(err.substring(err.indexOf(" ", firstPos), err.indexOf(":", firstPos))) * 1;
+      errorWidget = codeMirror.addLineWidget(selectedLine - 1, $("<div>").addClass("editorError").html("<i class='fa fa-exclamation-circle'></i> " + err)[0], {coverGutter: true, noHScroll: true})
+    }
+
+    codeMirror.setSize("95%", 100);
+
+    codeMirror.on("focus", function () {
+      if (codeMirror.getValue() == queryPlaceholder) {
+        codeMirror.setValue("");
+      }
+      if (errorWidget) {
+        errorWidget.clear();
+      }
+      $("#validationResults").empty();
+    });
+
+    codeMirror.on("blur", function () {
+      $(document.body).off("contextmenu");
+    });
+
+    codeMirror.on("change", function () {
+      $(".query").val(codeMirror.getValue());
+    });
+
+    $("#help").popover({
+      'title': "${_('Did you know?')}",
+      'content': $("#help-content").html(),
+      'trigger': 'hover',
+      'html': true
+    });
+  });
+
+  function modal(el) {
+    var el = $(el);
+    return function() {
+      el.modal('show');
+    };
+  }
+
+  function getHighlightedQuery() {
+    var selection = codeMirror.getSelection();
+    if (selection != "") {
+      return selection;
+    }
+    return null;
+  }
+
+  function tryExecuteQuery() {
+    var query = getHighlightedQuery() || codeMirror.getValue();
+    viewModel.query.query(query);
+    viewModel.executeQuery();
+  }
+
+  function tryExplainQuery() {
+    var query = getHighlightedQuery() || codeMirror.getValue();
+    viewModel.query.query(query);
+    viewModel.explainQuery();
+  }
+
+  function trySaveQuery() {
+    var query = getHighlightedQuery() || codeMirror.getValue();
+    viewModel.query.query(query);
+    if (viewModel.query.id() && viewModel.query.id() != -1) {
+      viewModel.saveQuery();
+    }
+  }
+
+  function trySaveAsQuery() {
+    var query = getHighlightedQuery() || codeMirror.getValue();
+    viewModel.query.query(query);
+    $('#saveAsQueryModal').modal('show');
+  }
+
+  function modalSaveAsQuery() {
+    if (viewModel.query.query() && viewModel.query.name()) {
+      viewModel.query.id(-1);
+      viewModel.saveQuery();
+      $('#saveas-query-name').removeClass('error');
+      $('#saveAsQueryModal').modal('hide');
+    } else if (viewModel.query.name()) {
+      $.jHueNotify.error("${_('No query provided to save.')}");
+      $('#saveAsQueryModal').modal('hide');
+    } else {
+      $('#saveas-query-name').addClass('error');
+    }
+  }
+
+
+  // Knockout
+  viewModel = new RdbmsViewModel();
+  viewModel.fetchServers();
+  viewModel.database.subscribe((function() {
+    // First call skipped to avoid reset of hueRdbmsLastDatabase
+    var counter = 0;
+    return function(value) {
+
+      % if design.id:
+        if (counter++ == 0) {
+          viewModel.fetchQuery(${design.id});
+        }
+      % endif
+
+      if (counter++ > 0) {
+        if (value != $.totalStorage("hueRdbmsLastDatabase")) {
+          $.totalStorage("hueRdbmsLastDatabase", value);
+        }
+      }
+    }
+  })());
+  viewModel.databases.subscribe(function() {
+    viewModel.database($.totalStorage("hueRdbmsLastDatabase"));
+  });
+
+  viewModel.query.query.subscribe((function() {
+    // First call skipped to avoid reset of hueRdbmsLastDatabase
+    var counter = 0;
+    return function(value) {
+      if (counter++ == 0) {
+        codeMirror.setValue(value);
+      }
+    }
+  })());
+
+  ko.applyBindings(viewModel);
+
+
+  // Editables
+  $("#query-name").editable({
+    validate: function (value) {
+      if ($.trim(value) == '') {
+        return "${ _('This field is required.') }";
+      }
+    },
+    success: function(response, newValue) {
+      viewModel.query.name(newValue);
+    },
+    emptytext: "${ _('Query name') }"
+  });
+
+  $("#query-description").editable({
+    success: function(response, newValue) {
+      viewModel.query.description(newValue);
+    },
+    emptytext: "${ _('Empty description') }"
+  });
+
+
+  // Events and datatables
+  $(document).on('saved.query', function() {
+    $.jHueNotify.info("${_('Query saved successfully!')}")
+  });
+
+  var dataTable = null;
+  var dataTableWidth = 0;
+  function cleanResultsTable() {
+    if (dataTable) {
+      dataTable.fnDestroy();
+      viewModel.columns.valueHasMutated();
+      viewModel.rows.valueHasMutated();
+      dataTable = null;
+    }
+  }
+
+  function resultsTable() {
+    if (!dataTable) {
+      dataTable = $(".resultTable").dataTable({
+        "bPaginate": false,
+        "bLengthChange": false,
+        "bInfo": false,
+        "oLanguage": {
+          "sEmptyTable": "${_('No data available')}",
+          "sZeroRecords": "${_('No matching records')}"
+        },
+        "fnDrawCallback": function( oSettings ) {
+          $(".resultTable").jHueTableExtender({
+            hintElement: "#jumpToColumnAlert",
+            fixedHeader: true,
+            firstColumnTooltip: true
+          });
+        }
+      });
+      $(".dataTables_filter").hide();
+      $(".dataTables_wrapper").jHueTableScroller();
+
+      if (dataTableWidth > 0) {
+        $(".resultTable").width(dataTableWidth);
+      } else {
+        dataTableWidth = $(".resultTable").outerWidth();
+      }
+    }
+  }
+  $(document).on('execute.query', cleanResultsTable);
+  $(document).on('explain.query', cleanResultsTable);
+  $(document).on('executed.query', resultsTable);
+  $(document).on('explained.query', resultsTable);
+
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 48 - 0
apps/rdbms/src/rdbms/tests.py

@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+# 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.
+
+# import re
+
+from nose.tools import assert_true
+
+from desktop.lib.django_test_util import make_logged_in_client
+from beeswax.server import dbms
+
+
+class MockRdbms:
+  def get_databases(self):
+    return ['db1', 'db2']
+
+  def get_tables(self, database):
+    return ['table1', 'table2']
+
+
+class TestMockedRdbms:
+  def setUp(self):
+    self.client = make_logged_in_client()
+
+    # Mock DB calls as we don't need the real ones
+    self.prev_dbms = dbms.get
+    dbms.get = lambda a, b: MockRdbms()
+
+  def tearDown(self):
+    # Remove monkey patching
+    dbms.get = self.prev_dbms
+
+  def test_basic_flow(self):
+    response = self.client.get("/rdbms/")
+    assert_true('DB Query' in response.content, response.content)

+ 52 - 0
apps/rdbms/src/rdbms/urls.py

@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+# 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 django.conf.urls.defaults import patterns, url
+
+
+# Views
+urlpatterns = patterns('rdbms.views',
+  url(r'^$', 'index', name='index'),
+  url(r'^execute/(?P<design_id>\d+)?$', 'execute_query', name='execute_query')
+)
+
+# APIs
+urlpatterns += patterns('rdbms.api',
+  url(r'^api/servers/?$', 'servers', name='api_servers'),
+  url(r'^api/servers/(?P<server>\w+)/databases/?$', 'databases', name='api_databases'),
+  url(r'^api/query/((?P<design_id>\d+)/?)?$', 'save_query', name='api_save_query'),
+  url(r'^api/query/(?P<design_id>\d+)/get$', 'fetch_saved_query', name='api_fetch_saved_query'),
+  url(r'^api/execute/(?P<design_id>\d+)?$', 'execute_query', name='api_execute_query'),
+  url(r'^api/explain/?$', 'explain_query', name='api_explain_query'),
+  url(r'^api/results/(?P<id>\d+)/(?P<first_row>\d+)$', 'fetch_results', name='api_fetch_results')
+)
+
+urlpatterns += patterns('beeswax.views',
+  url(r'^autocomplete/$', 'autocomplete', name='autocomplete'),
+  url(r'^autocomplete/(?P<database>\w+)/$', 'autocomplete', name='autocomplete'),
+  url(r'^autocomplete/(?P<database>\w+)/(?P<table>\w+)$', 'autocomplete', name='autocomplete'),
+
+  url(r'^save_design_properties$', 'save_design_properties', name='save_design_properties'), # Ajax
+
+  url(r'^my_queries$', 'my_queries', name='my_queries'),
+  url(r'^list_designs$', 'list_designs', name='list_designs'),
+  url(r'^list_trashed_designs$', 'list_trashed_designs', name='list_trashed_designs'),
+  url(r'^delete_designs$', 'delete_design', name='delete_design'),
+  url(r'^restore_designs$', 'restore_design', name='restore_design'),
+  url(r'^clone_design/(?P<design_id>\d+)$', 'clone_design', name='clone_design'),
+  url(r'^query_history$', 'list_query_history', name='list_query_history')
+)

+ 117 - 0
apps/rdbms/src/rdbms/views.py

@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# 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.
+
+import logging
+
+from django.utils.translation import ugettext as _
+from django.core.urlresolvers import reverse
+
+from desktop.context_processors import get_app_name
+from desktop.models import Document
+from desktop.lib.django_util import render
+
+from beeswax import models as beeswax_models
+from beeswax.views import safe_get_design
+
+from rdbms.design import SQLdesign
+
+
+LOG = logging.getLogger(__name__)
+
+
+def index(request):
+  return execute_query(request)
+
+
+"""
+Queries Views
+"""
+
+def execute_query(request, design_id=None):
+  """
+  View function for executing an arbitrary synchronously query.
+  """
+  action = request.path
+  app_name = get_app_name(request)
+  query_type = beeswax_models.SavedQuery.TYPES_MAPPING[app_name]
+  design = safe_get_design(request, query_type, design_id)
+
+  return render('execute.mako', request, {
+    'action': action,
+    'design': design,
+    'autocomplete_base_url': reverse('rdbms:autocomplete', kwargs={}),
+    'can_edit_name': design.id and not design.is_auto,
+  })
+
+
+def save_design(request, save_form, query_form, type_, design, explicit_save=False):
+  """
+  save_design(request, save_form, query_form, type_, design, explicit_save) -> SavedQuery
+
+  A helper method to save the design:
+    * If ``explicit_save``, then we save the data in the current design.
+    * If the user clicked the submit button, we do NOT overwrite the current
+      design. Instead, we create a new "auto" design (iff the user modified
+      the data). This new design is named after the current design, with the
+      AUTO_DESIGN_SUFFIX to signify that it's different.
+
+  Need to return a SavedQuery because we may end up with a different one.
+  Assumes that form.saveform is the SaveForm, and that it is valid.
+  """
+
+  if type_ == beeswax_models.RDBMS:
+    design_cls = SQLdesign
+  else:
+    raise ValueError(_('Invalid design type %(type)s') % {'type': type_})
+
+  old_design = design
+  design_obj = design_cls(query_form)
+  new_data = design_obj.dumps()
+
+  # Auto save if (1) the user didn't click "save", and (2) the data is different.
+  # Don't generate an auto-saved design if the user didn't change anything
+  if explicit_save:
+    design.name = save_form.cleaned_data['name']
+    design.desc = save_form.cleaned_data['desc']
+    design.is_auto = False
+  elif new_data != old_design.data:
+    # Auto save iff the data is different
+    if old_design.id is not None:
+      # Clone iff the parent design isn't a new unsaved model
+      design = old_design.clone()
+      if not old_design.is_auto:
+        design.name = old_design.name + beeswax_models.SavedQuery.AUTO_DESIGN_SUFFIX
+    else:
+      design.name = beeswax_models.SavedQuery.DEFAULT_NEW_DESIGN_NAME
+    design.is_auto = True
+
+  design.type = type_
+  design.data = new_data
+
+  design.save()
+
+  LOG.info('Saved %s design "%s" (id %s) for %s' % (design.name and '' or 'auto ', design.name, design.id, design.owner))
+
+  if design.doc.exists():
+    design.doc.update(name=design.name, description=design.desc)
+  else:
+    Document.objects.link(design, owner=design.owner, extra=design.type, name=design.name, description=design.desc)
+
+  if design.is_auto:
+    design.doc.get().add_to_history()
+
+  return design

BIN
apps/rdbms/static/art/icon_rdbms_24.png


BIN
apps/rdbms/static/help/images/23888161.png


+ 663 - 0
apps/rdbms/static/help/index.html

@@ -0,0 +1,663 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html
+  PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html lang="en-us" xml:lang="en-us">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+<link rel="stylesheet" type="text/css" href="commonltr.css"/>
+<title>Beeswax</title>
+</head>
+<body id="topic_4"><a name="topic_4"><!-- --></a>
+
+
+
+  <h1 class="title topictitle1">Beeswax </h1>
+
+
+  <div class="body conbody">
+    <p class="p">The Beeswax application enables you to perform queries on Apache Hive, a
+      data warehousing system designed to work with Hadoop. For information about Hive, see <a class="xref" href="http://archive.cloudera.com/cdh4/cdh/4/hive/" target="_blank">Hive Documentation</a>. You can create Hive databases, tables and
+      partitions, load data, create, run, and manage queries, and download the results in a
+      Microsoft Office Excel worksheet file or a comma-separated values file. </p>
+
+  </div>
+
+
+  <div class="topic concept nested1" xml:lang="en-US" lang="en-US" id="topic_4_1"><a name="topic_4_1"><!-- --></a>
+
+    <h2 class="title topictitle2">Beeswax and Hive Installation and
+      Configuration </h2>
+
+
+    <div class="body conbody">
+
+      <p class="p">Beeswax is installed and configured as part of Hue. <span class="ph">For information about installing and configuring Hue,
+        see Hue Installation in <a class="xref" href="http://www.cloudera.com/content/cloudera-content/cloudera-docs/CDH4/latest/CDH4-Installation-Guide/CDH4-Installation-Guide.html" target="_blank">http://www.cloudera.com/content/cloudera-content/cloudera-docs/CDH4/latest/CDH4-Installation-Guide/CDH4-Installation-Guide.html</a>.</span>
+      </p>
+
+
+      <p class="p">Beeswax assumes an existing Hive installation. The Hue installation
+        instructions include the configuration necessary for Beeswax to access Hive. You can view
+        the current Hive configuration from the <strong class="ph b">Settings</strong> tab in
+        the Beeswax application. </p>
+
+
+      <p class="p">By default, a Beeswax user can see the saved queries for all users –
+        both his/her own queries and those of other Beeswax users. To restrict viewing saved queries
+        to the query owner and Hue administrators, set the <samp class="ph codeph">share_saved_queries</samp> property under the <samp class="ph codeph">[beeswax]</samp> section in the Hue configuration file to <samp class="ph codeph">false</samp>. </p>
+
+
+    </div>
+
+
+  </div>
+
+
+  <div class="topic concept nested1" xml:lang="en-US" lang="en-US" id="topic_4_2"><a name="topic_4_2"><!-- --></a>
+
+    <h2 class="title topictitle2">Starting Beeswax </h2>
+
+
+    <div class="body conbody">
+
+      <p class="p">Click the <strong class="ph b">Beeswax</strong> icon (<img class="image" src="/beeswax/static/art/icon_beeswax_24.png"/>)
+        in the navigation bar at the top of the Hue browser page. </p>
+
+
+    </div>
+
+
+  </div>
+
+  <div class="topic concept nested1" id="concept_xvg_nzh_dk"><a name="concept_xvg_nzh_dk"><!-- --></a>
+    <h2 class="title topictitle2">Managing Databases, Tables, and Partitions</h2>
+
+    <div class="body conbody">
+      <p class="p" id="concept_xvg_nzh_dk__p_ygl_m13_dk"><a name="concept_xvg_nzh_dk__p_ygl_m13_dk"><!-- --></a>You can create databases, tables, partitions, and load data by executing
+          <a class="xref" href="http://archive.cloudera.com/cdh4/cdh/4/hive/language_manual/data-manipulation-statements.html" target="_blank">Hive data manipulation
+          statements</a> in the Beeswax application.</p>
+
+      <p class="p" id="concept_xvg_nzh_dk__p_pdp_m13_dk"><a name="concept_xvg_nzh_dk__p_pdp_m13_dk"><!-- --></a>You can also use the <a class="xref" href="metastoremanager.html#xd_583c10bfdbd326ba-3ca24a24-13d80143249--7f9b">Metastore Manager</a> application to manage the
+        databases, tables, and partitions and load data.</p>
+
+    </div>
+
+  </div>
+
+  <div class="topic concept nested1" xml:lang="en-US" lang="en-US" id="topic_4_2_1"><a name="topic_4_2_1"><!-- --></a>
+    <h2 class="title topictitle2">Installing Example Queries and
+      Tables</h2>
+
+    <div class="body conbody">
+      <div class="note note" id="topic_4_2_1__note_sb1_dlk_2k"><a name="topic_4_2_1__note_sb1_dlk_2k"><!-- --></a><span class="notetitle"><img src="/static/art/help/note.jpg"/> 
+      <b>Note</b>:</span> You must be a superuser to perform this task.</div>
+
+      <a name="topic_4_2_1__ol_kss_btp_wj"><!-- --></a><ol class="ol" id="topic_4_2_1__ol_kss_btp_wj">
+        <li class="li" id="topic_4_2_1__li_src_z3t_yj"><a name="topic_4_2_1__li_src_z3t_yj"><!-- --></a>Click <a name="topic_4_2_1__image_vv5_hnv_wj"><!-- --></a><img class="image" id="topic_4_2_1__image_vv5_hnv_wj" src="/static/art/hue-logo-mini.png"/>. The Quick Start Wizard opens.</li>
+
+        <li class="li" id="topic_4_2_1__li_zgj_z3t_yj"><a name="topic_4_2_1__li_zgj_z3t_yj"><!-- --></a>Click <strong class="ph b">Step 2:
+            Examples</strong>.</li>
+
+        <li class="li">Click <strong class="ph b">Beeswax (Hive UI)</strong>.</li>
+
+      </ol>
+
+    </div>
+
+  </div>
+
+
+  <div class="topic concept nested1" xml:lang="en-US" lang="en-US" id="topic_4_3"><a name="topic_4_3"><!-- --></a>
+
+    <h2 class="title topictitle2">Query Editor</h2>
+
+
+    <div class="body conbody">
+
+      <p class="p">The Query Editor view lets you create, save, and submit queries in the
+          <a class="xref" href="http://wiki.apache.org/hadoop/Hive/LanguageManual" target="_blank">Hive Query Language (HQL)</a>,
+        which is similar to Structured Query Language (SQL).  When you submit a query, the Beeswax
+        Server uses Hive to run the queries. You can either wait for the query to complete, or
+        return later to find the queries in the History view. You can also request to receive an
+        email message after the query is completed.</p>
+
+      <p class="p">In the box to the left of the Query field, you can select a database,
+        override the default Hive and Hadoop settings, specify file resources and user-defined
+        functions, enable users to enter parameters at run-time, and request email notification when
+        the job is complete. See <a class="xref" href="#topic_4_3_2">Advanced Query Settings</a> for details on using these settings. </p>
+
+
+    </div>
+
+
+    <div class="topic concept nested2" xml:lang="en-US" lang="en-US" id="topic_4_3_1"><a name="topic_4_3_1"><!-- --></a>
+
+      <h3 class="title topictitle3">Creating Queries </h3>
+
+
+      <div class="body conbody">
+
+        <ol class="ol">
+          <li class="li" id="topic_4_3_1__li_v2d_p5k_xj"><a name="topic_4_3_1__li_v2d_p5k_xj"><!-- --></a>In the Query Editor window, type a query or
+            multiple queries separated by a semicolon ";".  To be presented with a drop-down of
+            autocomplete options, type <span class="ph uicontrol">CTRL+spacebar</span> when entering a query.  </li>
+
+          <li class="li" id="topic_4_3_1__li_ilk_p5k_xj"><a name="topic_4_3_1__li_ilk_p5k_xj"><!-- --></a>To save your query and advanced settings to use
+            again later, click <strong class="ph b">Save As</strong>, enter a name and
+            description, and then click <strong class="ph b">OK</strong>. To save changes to an
+            existing query, click <strong class="ph b">Save.</strong>
+          </li>
+
+          <li class="li">If you want to view the execution plan for the query, click <strong class="ph b">Explain</strong>. For more information, see <a class="xref" href="http://wiki.apache.org/hadoop/Hive/LanguageManual/Explain" target="_blank">http://wiki.apache.org/hadoop/Hive/LanguageManual/Explain</a>.</li>
+
+        </ol>
+
+
+      </div>
+
+
+    </div>
+
+    <div class="topic concept nested2" id="concept_ch1_5gb_zj"><a name="concept_ch1_5gb_zj"><!-- --></a>
+      <h3 class="title topictitle3">Loading Queries into the Query Editor</h3>
+
+      <div class="body conbody">
+        <a name="concept_ch1_5gb_zj__ol_kkg_vgb_zj"><!-- --></a><ol class="ol" id="concept_ch1_5gb_zj__ol_kkg_vgb_zj">
+          <li class="li">Do one of the following:<a name="concept_ch1_5gb_zj__ul_xlq_jqb_zj"><!-- --></a><ul class="ul" id="concept_ch1_5gb_zj__ul_xlq_jqb_zj">
+              <li class="li">Click the <span class="ph uicontrol">My Queries</span>  tab.<a name="concept_ch1_5gb_zj__ol_qhz_lqb_zj"><!-- --></a><ol class="ol" type="a" id="concept_ch1_5gb_zj__ol_qhz_lqb_zj">
+                  <li class="li">Click the <span class="ph uicontrol">Recent Saved Queries</span> or <span class="ph uicontrol">Recent Run
+                      Queries</span> tab to display the respective queries. </li>
+
+                </ol>
+</li>
+
+              <li class="li">Click the <span class="ph uicontrol">Saved Queries</span> tab.</li>
+
+            </ul>
+</li>
+
+          <li class="li">Click a query name. The query is loaded into the Query Editor.</li>
+
+        </ol>
+
+      </div>
+
+    </div>
+
+    <div class="topic concept nested2" id="concept_xsr_yx1_zj"><a name="concept_xsr_yx1_zj"><!-- --></a>
+      <h3 class="title topictitle3">Running Queries </h3>
+
+      <div class="body conbody">
+        <div class="note note" id="concept_xsr_yx1_zj__note_vbn_4xk_xj"><a name="concept_xsr_yx1_zj__note_vbn_4xk_xj"><!-- --></a><span class="notetitle"><img src="/static/art/help/note.jpg"/> 
+      <b>Note</b>:</span> To run a query, you must be logged in to
+          Hue as a user that also has a Unix user account on the remote server. </div>
+
+        <a name="concept_xsr_yx1_zj__ol_rnw_yx1_zj"><!-- --></a><ol class="ol" id="concept_xsr_yx1_zj__ol_rnw_yx1_zj">
+          <li class="li">To execute a portion of the query, highlight one or more query
+            statements.</li>
+
+          <li class="li">Click <strong class="ph b">Execute</strong>. The Query Results
+            window appears with the results of your query. <a name="concept_xsr_yx1_zj__ul_f4w_yx1_zj"><!-- --></a><ul class="ul" id="concept_xsr_yx1_zj__ul_f4w_yx1_zj">
+              <li class="li">To view a log of the query execution, click <strong class="ph b">Log</strong> at the top of the results display. You can use
+                the information in this tab to debug your query.</li>
+
+              <li class="li">To view the query that generated these results, click <strong class="ph b">Query</strong> at the top of the results display.</li>
+
+              <li class="li">To view the columns of the query, click <strong class="ph b">Columns</strong>.</li>
+
+              <li class="li">To return to the query in the Query Editor, click <strong class="ph b">Unsaved Query</strong>.</li>
+
+            </ul>
+</li>
+
+          <li class="li">If there are multiple statements in the query, click <span class="ph uicontrol">Next</span> in
+            the Multi-statement query pane to execute the remaining statements.</li>
+
+        </ol>
+
+        <div class="note note"><span class="notetitle"><img src="/static/art/help/note.jpg"/> 
+      <b>Note</b>:</span> Under <strong class="ph b">MR JOBS</strong>, you can view any MapReduce jobs that
+          the query generated.</div>
+
+      </div>
+
+    </div>
+
+    <div class="topic concept nested2" id="concept_axp_mgb_zj"><a name="concept_axp_mgb_zj"><!-- --></a>
+      <h3 class="title topictitle3">Downloading and Saving Query Results</h3>
+
+      <div class="body conbody">
+        <div class="note important"><span class="importanttitle"><img src="/static/art/help/important.jpg"/> 
+      <b>Important</b>:</span> 
+          <a name="concept_axp_mgb_zj__ul_p4w_yx1_zj"><!-- --></a><ul class="ul" id="concept_axp_mgb_zj__ul_p4w_yx1_zj">
+            <li class="li">You can only save results to a file when the results were
+              generated by a MapReduce job.</li>
+
+            <li class="li">This is the preferred way to save when the result is large (for
+              example &gt; 1M rows).</li>
+
+          </ul>
+
+        </div>
+
+        <a name="concept_axp_mgb_zj__ol_agb_4gb_zj"><!-- --></a><ol class="ol" id="concept_axp_mgb_zj__ol_agb_4gb_zj">
+          <li class="li">Do any of the following to download or save the query results: <a name="concept_axp_mgb_zj__ul_i4w_yx1_zj"><!-- --></a><ul class="ul" id="concept_axp_mgb_zj__ul_i4w_yx1_zj">
+              <li class="li">Click <strong class="ph b">Download as CSV</strong> to
+                download the results in a comma-separated values file suitable for use in other
+                applications.</li>
+
+              <li class="li">Click <strong class="ph b">Download as XLS</strong> to
+                download the results in a Microsoft Office Excel worksheet file.</li>
+
+              <li class="li">Click <strong class="ph b">Save</strong> to save the
+                results in a table or HDFS file.<a name="concept_axp_mgb_zj__ul_m4w_yx1_zj"><!-- --></a><ul class="ul" id="concept_axp_mgb_zj__ul_m4w_yx1_zj">
+                  <li class="li">To save the results in a new table, select <strong class="ph b">In a new table</strong>, enter a table name, and then
+                    click <strong class="ph b">Save</strong>.</li>
+
+                  <li class="li">To save the results in an HDFS file, select <strong class="ph b">In an HDFS directory</strong>, enter a path and then
+                    click <strong class="ph b">Save</strong>. You can then download the file
+                    with <a class="xref" href="filebrowser.html#topic_6">File Browser</a>.  </li>
+
+                </ul>
+</li>
+
+            </ul>
+</li>
+
+        </ol>
+
+      </div>
+
+    </div>
+
+
+    <div class="topic concept nested2" xml:lang="en-US" lang="en-US" id="topic_4_3_2"><a name="topic_4_3_2"><!-- --></a>
+
+      <h3 class="title topictitle3">Advanced Query Settings </h3>
+
+
+      <div class="body conbody">
+
+        <p class="p">The pane to the left of the Query Editor lets you specify the
+          following options: </p>
+
+
+        
+<div class="tablenoborder"><table cellpadding="4" cellspacing="0" summary="" class="table" frame="hsides" border="1" rules="all">
+            
+            
+            <thead class="thead" align="left">
+              <tr class="row">
+                <th class="entry" valign="top" width="16.666666666666664%" id="d5062e390">
+                  <p class="p">
+                    <strong class="ph b">Option</strong>
+                  </p>
+
+                </th>
+
+                <th class="entry" valign="top" width="83.33333333333334%" id="d5062e399">
+                  <p class="p">
+                    <strong class="ph b">Description</strong>
+                  </p>
+
+                </th>
+
+              </tr>
+
+            </thead>
+
+            <tbody class="tbody">
+              <tr class="row">
+                <td class="entry" valign="top" width="16.666666666666664%" headers="d5062e390 ">
+                  <p class="p"><strong class="ph b">DATABASE</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="83.33333333333334%" headers="d5062e399 ">
+                  <p class="p">The database containing the table definitions. </p>
+
+                </td>
+
+              </tr>
+
+              <tr class="row">
+                <td class="entry" valign="top" width="16.666666666666664%" headers="d5062e390 ">
+                  <p class="p"><strong class="ph b">SETTINGS</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="83.33333333333334%" headers="d5062e399 ">
+                  <div class="p">Override the Hive and Hadoop default settings.  To configure
+                    a new setting: <a name="topic_4_3_2__ol_k5v_hcc_zj"><!-- --></a><ol class="ol" id="topic_4_3_2__ol_k5v_hcc_zj">
+                      <li class="li">Click <strong class="ph b">Add</strong>.</li>
+
+                      <li class="li">For <strong class="ph b">Key</strong>, enter a Hive or Hadoop
+                        configuration variable name. </li>
+
+                      <li class="li">For <strong class="ph b">Value</strong>, enter the value you want to
+                        use for the variable. </li>
+
+                    </ol>
+</div>
+
+                  <p class="p">For example, to override the directory where structured
+                    Hive query logs are created, you would enter <samp class="ph codeph">hive.querylog.location</samp> for <strong class="ph b">Key</strong>, and a path for <strong class="ph b">Value.</strong>
+                  </p>
+
+                  <p class="p">To view the default settings, click the <strong class="ph b">Settings</strong> tab at the top of the page. For
+                    information about Hive configuration variables, see: <a class="xref" href="http://wiki.apache.org/hadoop/Hive/AdminManual/Configuration" target="_blank">http://wiki.apache.org/hadoop/Hive/AdminManual/Configuration</a>. For
+                    information about Hadoop configuration variables, see: <a class="xref" href="http://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml" target="_blank">http://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml</a>.</p>
+
+                </td>
+
+              </tr>
+
+              <tr class="row">
+                <td class="entry" valign="top" width="16.666666666666664%" headers="d5062e390 ">
+                  <p class="p"><strong class="ph b">FILE RESOURCES</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="83.33333333333334%" headers="d5062e399 ">
+                  <div class="p">Make files locally accessible at query execution time
+                    available on the Hadoop cluster. Hive uses the Hadoop Distributed Cache to
+                    distribute the added files to all machines in the cluster at query execution
+                      time.<a name="topic_4_3_2__ol_flf_vfc_zj"><!-- --></a><ol class="ol" id="topic_4_3_2__ol_flf_vfc_zj">
+                      <li class="li">Click <strong class="ph b">Add</strong> to
+                        configure a new setting.</li>
+
+                      <li class="li">From the <strong class="ph b">Type</strong>
+                        drop-down menu, choose one of the following:<a name="topic_4_3_2__ul_bkb_wfc_zj"><!-- --></a><ul class="ul" id="topic_4_3_2__ul_bkb_wfc_zj">
+                          <li class="li">
+                            <strong class="ph b">jar</strong> — Adds the specified resources to
+                            the Java classpath.</li>
+
+                          <li class="li"><strong class="ph b">archive</strong> —
+                            Unarchives the specified resources when distributing them. </li>
+
+                          <li class="li"><strong class="ph b">file</strong> — Adds the
+                            specified resources to the distributed cache. Typically, this might be a
+                            transform script (or similar) to be executed. </li>
+
+                        </ul>
+</li>
+
+                      <li class="li">For <strong class="ph b">Path</strong>, enter the
+                        path to the file or click <a name="topic_4_3_2__image_sdp_cgc_zj"><!-- --></a><img class="image" id="topic_4_3_2__image_sdp_cgc_zj" src="/static/art/help/browse.png"/> to browse and select the file.  </li>
+
+                    </ol>
+</div>
+
+
+                  <div class="note note"><span class="notetitle"><img src="/static/art/help/note.jpg"/> 
+      <b>Note</b>:</span> It is not necessary to specify files used in a
+										transform script if the files are available in the same path
+										on all machines in the Hadoop cluster.</div>
+
+                </td>
+
+              </tr>
+
+              <tr class="row">
+                <td class="entry" valign="top" width="16.666666666666664%" headers="d5062e390 ">
+                  <p class="p"><strong class="ph b">USER-DEFINED FUNCTIONS</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="83.33333333333334%" headers="d5062e399 ">
+                  <p class="p">Specify user-defined functions. Click <strong class="ph b">Add</strong> to configure a new setting. Specify the
+                    function name in the <strong class="ph b">Name</strong> field, and specify
+                    the class name for <strong class="ph b">Class</strong>
+                    <strong class="ph b">name</strong>. </p>
+
+                  <p class="p">You <em class="ph i">must</em> specify a JAR file for the user-defined
+                    functions in <strong class="ph b"><strong class="ph b">FILE
+                        RESOURCES</strong></strong>. </p>
+
+                  <p class="p">To include a user-defined function in a query, add a $
+                    (dollar sign) before the function name in the query. For example, if <var class="keyword varname">MyTable</var> is a user-defined
+                    function name in the query, you would type: <samp class="ph codeph">SELECT * $</samp><samp class="ph codeph"><var class="keyword varname">MyTable</var></samp>
+                  </p>
+
+                </td>
+
+              </tr>
+
+              <tr class="row">
+                <td class="entry" valign="top" width="16.666666666666664%" headers="d5062e390 ">
+                  <p class="p"><strong class="ph b">PARAMETERIZATION</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="83.33333333333334%" headers="d5062e399 ">
+                  <p class="p">Indicate that a dialog box should display to enter parameter
+                    values when a query containing the string $<var class="keyword varname">parametername</var> is
+                    executed. Enabled by default. </p>
+
+                </td>
+
+              </tr>
+
+              <tr class="row">
+                <td class="entry" valign="top" width="16.666666666666664%" headers="d5062e390 ">
+                  <p class="p"><strong class="ph b">EMAIL NOTIFICATION</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="83.33333333333334%" headers="d5062e399 ">
+                  <p class="p">Indicate that an email message should be sent after a query
+                    completes. The email is sent to the email address specified in the logged-in
+                    user's profile. </p>
+
+                </td>
+
+              </tr>
+
+            </tbody>
+
+          </table>
+</div>
+
+
+      </div>
+
+
+    </div>
+
+
+    <div class="topic concept nested2" xml:lang="en-US" lang="en-US" id="topic_4_3_3"><a name="topic_4_3_3"><!-- --></a>
+
+      <h3 class="title topictitle3">Viewing Query History </h3>
+
+
+      <div class="body conbody">
+
+        <p class="p">You can view the history of queries that you have run previously.
+          Results for these queries are available for one week or until Hue is restarted. </p>
+
+
+        <ol class="ol">
+
+          <li class="li">Click <strong class="ph b">History</strong>. A list of your
+            saved and unsaved queries displays in the Query History window.</li>
+
+
+          <li class="li">To display the queries for all users, click <strong class="ph b">Show everyone's queries</strong>.
+						To display your queries only, click <strong class="ph b">Show my queries</strong>.</li>
+
+
+          <li class="li">To display the automatically generated actions performed on a
+            user's behalf, click <strong class="ph b">Show auto actions</strong>. To display
+            user queries again, click <strong class="ph b">Show user queries</strong>.</li>
+
+
+        </ol>
+
+
+      </div>
+
+
+    </div>
+
+
+    <div class="topic concept nested2" xml:lang="en-US" lang="en-US" id="topic_4_3_4"><a name="topic_4_3_4"><!-- --></a>
+
+      <h3 class="title topictitle3">Viewing, Editing, Copying, and Deleting
+        Saved Queries </h3>
+
+
+      <div class="body conbody">
+
+        <p class="p">You can view a list of saved queries of all users by clicking <strong class="ph b">My
+            Queries</strong> and then selecting either  <span class="ph uicontrol">Recent Saved Queries</span> or
+            <span class="ph uicontrol">Recent Run Queries</span> tab to display the respective queries or
+          clicking <strong class="ph b">Saved Queries</strong>. You can copy any query, but you
+          can edit, delete, and view the history of only your own queries. </p>
+
+        
+<div class="tablenoborder"><a name="topic_4_3_4__table_rxy_gdh_b3"><!-- --></a><table cellpadding="4" cellspacing="0" summary="" id="topic_4_3_4__table_rxy_gdh_b3" class="table" frame="hsides" border="1" rules="all">
+            
+            
+            <thead class="thead" align="left">
+              <tr class="row">
+                <th class="entry" valign="top" width="25%" id="d5062e719">Saved Query</th>
+
+                <th class="entry" valign="top" width="75%" id="d5062e722">Procedure</th>
+
+              </tr>
+
+            </thead>
+
+            <tbody class="tbody">
+              <tr class="row">
+                <td class="entry" valign="top" width="25%" headers="d5062e719 ">
+                  <p class="p">
+                    <strong class="ph b">Edit</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="75%" headers="d5062e722 ">
+                  <div class="p">
+                    <a name="topic_4_3_4__ol_hbl_xbh_dk"><!-- --></a><ol class="ol" id="topic_4_3_4__ol_hbl_xbh_dk">
+                      <li class="li">Click <strong class="ph b">Saved Queries</strong>.
+                        The Queries window displays.</li>
+
+                      <li class="li">Check the checkbox next to the query and click <strong class="ph b">Edit</strong>. The query displays in the Query
+                        Editor window.</li>
+
+                      <li class="li">Change the query and then click <strong class="ph b">Save.</strong> You can also click <strong class="ph b">Save As</strong>, enter a new name, and click <strong class="ph b">OK</strong> to save a copy of the query.</li>
+
+                    </ol>
+
+                  </div>
+
+                </td>
+
+              </tr>
+
+              <tr class="row">
+                <td class="entry" valign="top" width="25%" headers="d5062e719 ">
+                  <p class="p">
+                    <strong class="ph b">Copy</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="75%" headers="d5062e722 ">
+                  <div class="p">
+                    <a name="topic_4_3_4__ol_dqp_xbh_dk"><!-- --></a><ol class="ol" id="topic_4_3_4__ol_dqp_xbh_dk">
+                      <li class="li">Click <strong class="ph b">Saved Queries</strong>.
+                        The Queries window displays.</li>
+
+                      <li class="li">Check the checkbox next to the query and click <strong class="ph b">Copy</strong>. The query displays in the Query
+                        Editor window.</li>
+
+                      <li class="li">Change the query as necessary and then click <strong class="ph b">Save.</strong> You can also click <strong class="ph b">Save As</strong>, enter a new name, and click <strong class="ph b">OK</strong> to save a copy of the query.</li>
+
+                    </ol>
+
+                  </div>
+
+                </td>
+
+              </tr>
+
+              <tr class="row">
+                <td class="entry" valign="top" width="25%" headers="d5062e719 ">
+                  <p class="p">
+                    <strong class="ph b">Copy in Query History</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="75%" headers="d5062e722 ">
+                  <div class="p">
+                    <a name="topic_4_3_4__ol_ut5_xbh_dk"><!-- --></a><ol class="ol" id="topic_4_3_4__ol_ut5_xbh_dk">
+                      <li class="li">Click <strong class="ph b">History</strong>. The
+                        Query History window displays.</li>
+
+                      <li class="li">To display the queries for all users, click <strong class="ph b">Show everyone's queries</strong>. The queries for
+                        all users display in the History window.</li>
+
+                      <li class="li">Click the query you want to copy. A copy of the query
+                        displays in the Query Editor window.</li>
+
+                      <li class="li">Change the query, if necessary, and then click <strong class="ph b">Save As</strong>, enter a new name, and click <strong class="ph b">OK</strong> to save the query.</li>
+
+                    </ol>
+
+                  </div>
+
+                </td>
+
+              </tr>
+
+              <tr class="row">
+                <td class="entry" valign="top" width="25%" headers="d5062e719 ">
+                  <p class="p">
+                    <strong class="ph b">Delete</strong>
+                  </p>
+
+                </td>
+
+                <td class="entry" valign="top" width="75%" headers="d5062e722 ">
+                  <a name="topic_4_3_4__ol_fgh_1vc_zj"><!-- --></a><ol class="ol" id="topic_4_3_4__ol_fgh_1vc_zj">
+                    <li class="li">Click <strong class="ph b">Saved Queries</strong>.
+                      The Queries window displays.</li>
+
+                    <li class="li">Check the checkbox next to the query and click <strong class="ph b">Delete</strong>.</li>
+
+                    <li class="li">Click <strong class="ph b">Yes</strong> to confirm
+                      the deletion.</li>
+
+                  </ol>
+
+                </td>
+
+              </tr>
+
+            </tbody>
+
+          </table>
+</div>
+
+
+      </div>
+
+
+    </div>
+
+
+  </div>
+
+
+
+</body>
+</html>

+ 232 - 0
apps/rdbms/static/js/rdbms.vm.js

@@ -0,0 +1,232 @@
+// 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.
+
+
+function RdbmsViewModel() {
+  var self = this;
+
+  self.selectedServer = ko.observable();
+  self.servers = ko.observable({});
+  self.selectedDatabase = ko.observable(0);
+  self.databases = ko.observableArray([]);
+  self.query = ko.mapping.fromJS({
+    'id': -1,
+    'query': '',
+    'name': null,
+    'description': null,
+    'errors': []
+  });
+  self.rows = ko.observableArray();
+  self.columns = ko.observableArray();
+
+  self.filter = ko.observable("");
+  self.isLoading = ko.observable(false);
+  self.isReady = ko.observable(false);
+
+  self.server = ko.computed({
+    'read': function() {
+      if (self.servers() && self.selectedServer()) {
+        return self.servers()[self.selectedServer()];
+      } else{
+        return "";
+      }
+    },
+    'write': function(value) {
+      self.selectedServer(value);
+    }
+  });
+
+  self.database = ko.computed({
+    'read': function() {
+      if (self.databases) {
+        return self.databases()[self.selectedDatabase()];
+      } else{
+        return "";
+      }
+    },
+    'write': function(value) {
+      self.selectedDatabase(self.databases.indexOf(value));
+    }
+  });
+
+  self.selectedServer.subscribe(function(value) {
+    self.fetchDatabases();
+  });
+
+  self.updateResults = function(results) {
+    var rows = [];
+    self.columns(results.columns);
+    $.each(results.rows, function(i, result_row) {
+      var row = [];
+      $.each(self.columns(), function(j, column) {
+        row.push(result_row[column]);
+      });
+      rows.push(row);
+    });
+    self.rows(rows);
+  };
+
+  self.updateServers = function(servers) {
+    self.servers(servers);
+    if (servers) {
+      self.selectedServer(Object.keys(servers)[0]);
+    }
+  };
+
+  self.updateDatabases = function(databases) {
+    self.databases(databases);
+    self.selectedDatabase.valueHasMutated();
+  };
+
+  self.updateQuery = function(design) {
+    self.query.query(design.query);
+    self.query.id(design.id);
+    self.query.name(design.name);
+    self.query.description(design.desc);
+    self.database(design.database);
+    self.server(design.server);
+  };
+
+  self.chooseServer = function(value, e) {
+    self.selectedServer(value);
+  };
+
+  self.chooseDatabase = function(value, e) {
+    self.selectedDatabase(self.databases.indexOf(value));
+  };
+
+  self.explainQuery = function() {
+    var data = ko.mapping.toJS(self.query);
+    data.database = self.database();
+    data.server = self.selectedServer();
+    var request = {
+      url: '/rdbms/api/explain/',
+      dataType: 'json',
+      type: 'POST',
+      success: function(data) {
+        if (data.status === 0) {
+          $(document).trigger('explain.query', data);
+          self.updateResults(data.results);
+          self.query.id(data.design);
+          $(document).trigger('explained.query', data);
+        } else {
+          self.query.errors.removeAll();
+          self.query.errors.push(data.message);
+        }
+      },
+      error: $.noop,
+      data: data
+    };
+    $.ajax(request);
+  };
+
+  self.fetchQuery = function(id) {
+    var _id = id || self.query.id();
+    if (_id && _id != -1) {
+      var request = {
+        url: '/rdbms/api/query/' + _id + '/get',
+        dataType: 'json',
+        type: 'GET',
+        success: function(data) {
+          self.updateQuery(data.design);
+        },
+        error: $.noop
+      };
+      $.ajax(request);
+    }
+  };
+
+  self.saveQuery = function() {
+    var self = this;
+    if (self.query.query() && self.query.name()) {
+      var data = ko.mapping.toJS(self.query);
+      data['desc'] = data['description'];
+      data['server'] = self.selectedServer();
+      data['database'] = self.database();
+      var url = '/rdbms/api/query/';
+      if (self.query.id() && self.query.id() != -1) {
+        url += self.query.id() + '/';
+      }
+      var request = {
+        url: url,
+        dataType: 'json',
+        type: 'POST',
+        success: function(data) {
+          $(document).trigger('saved.query', data);
+        },
+        error: function() {
+          $(document).trigger('error.query');
+        },
+        data: data
+      };
+      $.ajax(request);
+    }
+  };
+
+  self.executeQuery = function() {
+    var data = ko.mapping.toJS(self.query);
+    data.database = self.database();
+    data.server = self.selectedServer();
+    var request = {
+      url: '/rdbms/api/execute/',
+      dataType: 'json',
+      type: 'POST',
+      success: function(data) {
+        if (data.status === 0) {
+          $(document).trigger('execute.query', data);
+          self.updateResults(data.results);
+          self.query.id(data.design);
+          $(document).trigger('executed.query', data);
+        } else {
+          self.query.errors.removeAll();
+          self.query.errors.push(data.message);
+        }
+      },
+      error: $.noop,
+      data: data
+    };
+    $.ajax(request);
+  };
+
+  self.fetchServers = function() {
+    var request = {
+      url: '/rdbms/api/servers/',
+      dataType: 'json',
+      type: 'GET',
+      success: function(data) {
+        self.updateServers(data.servers);
+        self.fetchDatabases();
+      },
+      error: $.noop
+    };
+    $.ajax(request);
+  };
+
+  self.fetchDatabases = function() {
+    if (self.selectedServer()) {
+      var request = {
+        url: '/rdbms/api/servers/' + self.selectedServer() + '/databases/',
+        dataType: 'json',
+        type: 'GET',
+        success: function(data) {
+          self.updateDatabases(data.databases);
+        },
+        error: $.noop
+      };
+      $.ajax(request);
+    }
+  };
+}

+ 17 - 0
desktop/conf.dist/hue.ini

@@ -611,3 +611,20 @@
 [useradmin]
   # The name of the default user group that users will be a member of
   ## default_user_group=default
+
+
+###########################################################################
+# Settings for the RDBMS application
+###########################################################################
+
+[rdbms]
+  [[databases]]
+    # mysql or postgresql
+    ## [[[mydb]]]
+    ## nice_name=
+    ## engine=sqlite3
+    ## host=
+    ## port=
+    ## user=
+    ## password=
+    ## options={}

+ 17 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -616,3 +616,20 @@
 [useradmin]
   # The name of the default user group that users will be a member of
   ## default_user_group=default
+
+
+###########################################################################
+# Settings for the RDBMS application
+###########################################################################
+
+[rdbms]
+  [[databases]]
+    # mysql or postgresql
+    ## [[[mydb]]]
+    ## nice_name=
+    ## engine=sqlite3
+    ## host=
+    ## port=
+    ## user=
+    ## password=
+    ## options={}

+ 4 - 1
desktop/core/src/desktop/settings.py

@@ -23,10 +23,12 @@
 import logging
 import os
 import sys
+import pkg_resources
+
 import desktop.conf
 import desktop.log
 from desktop.lib.paths import get_desktop_root
-import pkg_resources
+
 
 HUE_DESKTOP_VERSION = pkg_resources.get_distribution("desktop").version or "Unknown"
 NICE_NAME = "Hue"
@@ -270,6 +272,7 @@ DATABASES = {
   'default': default_db
 }
 
+
 # Configure sessions
 SESSION_COOKIE_AGE = desktop.conf.SESSION.TTL.get()
 SESSION_COOKIE_SECURE = desktop.conf.SESSION.SECURE.get()

+ 3 - 0
desktop/core/src/desktop/templates/common_header.mako

@@ -275,6 +275,9 @@ from django.utils.translation import ugettext as _
            % if 'impala' in apps:
            <li><a href="/${apps['impala'].display_name}"><img src="${ apps['impala'].icon_path }"/> ${_('Impala')}</a></li>
            % endif
+           % if 'rdbms' in apps:
+           <li><a href="/${apps['rdbms'].display_name}"><img src="${ apps['rdbms'].icon_path }"/> ${_('DB Query')}</a></li>
+           % endif
            % if 'pig' in apps:
            <li><a href="/${apps['pig'].display_name}"><img src="${ apps['pig'].icon_path }"/> ${_('Pig')}</a></li>
            % endif

+ 1 - 1
desktop/core/src/desktop/views.py

@@ -464,7 +464,7 @@ def commonheader(title, section, user, padding="90px"):
     apps = appmanager.get_apps(user)
     apps_list = appmanager.get_apps_dict(user)
     for app in apps:
-      if app.display_name not in ['beeswax', 'impala', 'pig', 'jobsub', 'jobbrowser', 'metastore', 'hbase', 'sqoop', 'oozie', 'filebrowser', 'useradmin', 'search', 'help', 'about', 'zookeeper', 'proxy']:
+      if app.display_name not in ['beeswax', 'impala', 'pig', 'jobsub', 'jobbrowser', 'metastore', 'hbase', 'sqoop', 'oozie', 'filebrowser', 'useradmin', 'search', 'help', 'about', 'zookeeper', 'proxy', 'rdbms']:
         other_apps.append(app)
       if section == app.display_name:
         current_app = app

+ 105 - 0
desktop/core/static/js/codemirror-sql-hint.js

@@ -0,0 +1,105 @@
+(function () {
+  "use strict";
+
+  var tables;
+  var keywords;
+
+  function getKeywords(editor) {
+    var mode = editor.doc.modeOption;
+    if(mode === "sql") mode = "text/x-sql";
+    return CodeMirror.resolveMode(mode).keywords;
+  }
+
+  function match(string, word) {
+    var len = string.length;
+    var sub = word.substr(0, len);
+    return string.toUpperCase() === sub.toUpperCase();
+  }
+
+  function addMatches(result, search, wordlist, formatter) {
+    for(var word in wordlist) {
+      if(!wordlist.hasOwnProperty(word)) continue;
+      if(Array.isArray(wordlist)) {
+        word = wordlist[word];
+      }
+      if(match(search, word)) {
+        result.push(formatter(word));
+      }
+    }
+  }
+
+  function columnCompletion(result, editor) {
+    var cur = editor.getCursor();
+    var token = editor.getTokenAt(cur);
+    var string = token.string.substr(1);
+    var prevCur = CodeMirror.Pos(cur.line, token.start);
+    var table = editor.getTokenAt(prevCur).string;
+    var columns = tables[table];
+    if(!columns) {
+      table = findTableByAlias(table, editor);
+    }
+    columns = tables[table];
+    if(!columns) {
+      return;
+    }
+    addMatches(result, string, columns,
+        function(w) {return "." + w;});
+  }
+
+  function eachWord(line, f) {
+    var words = line.text.split(" ");
+    for(var i = 0; i < words.length; i++) {
+      f(words[i]);
+    }
+  }
+
+  // Tries to find possible table name from alias.
+  function findTableByAlias(alias, editor) {
+    var aliasUpperCase = alias.toUpperCase();
+    var previousWord = "";
+    var table = "";
+
+    editor.eachLine(function(line) {
+      eachWord(line, function(word) {
+        var wordUpperCase = word.toUpperCase();
+        if(wordUpperCase === aliasUpperCase) {
+          if(tables.hasOwnProperty(previousWord)) {
+            table = previousWord;
+          }
+        }
+        if(wordUpperCase !== "AS") {
+          previousWord = word;
+        }
+      });
+    });
+    return table;
+  }
+
+  function sqlHint(editor, options) {
+    tables = (options && options.tables) || {};
+    keywords = keywords || getKeywords(editor);
+    var cur = editor.getCursor();
+    var token = editor.getTokenAt(cur);
+
+    var result = [];
+
+    var search = token.string.trim();
+
+    addMatches(result, search, keywords,
+        function(w) {return w.toUpperCase();});
+
+    addMatches(result, search, tables,
+        function(w) {return w;});
+
+    if(search.lastIndexOf('.') === 0) {
+      columnCompletion(result, editor);
+    }
+
+    return {
+      list: result,
+        from: CodeMirror.Pos(cur.line, token.start),
+        to: CodeMirror.Pos(cur.line, token.end)
+    };
+  }
+  CodeMirror.sqlHint = sqlHint;
+})();