From fefcb928da85873a240c206a7e201724b1652a84 Mon Sep 17 00:00:00 2001
From: Alexander Leibzon <alexander.leibzon@gmail.com>
Date: Tue, 27 Sep 2016 19:13:44 +0300
Subject: [PATCH 1/4] add memsql as datasource

---
 Vagrantfile                                |   2 +-
 redash/models.py                           |   2 +-
 redash/query_runner/memsql_ds.py           | 147 +++++++++++++++++++++
 redash/settings.py                         |   1 +
 requirements.txt                           |   2 +
 requirements_all_ds.txt                    |   1 +
 setup/ubuntu/files/redash_supervisord_init |   0
 7 files changed, 153 insertions(+), 2 deletions(-)
 create mode 100644 redash/query_runner/memsql_ds.py
 mode change 100644 => 100755 setup/ubuntu/files/redash_supervisord_init

diff --git a/Vagrantfile b/Vagrantfile
index 875cca8fa6..1611006f3d 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -10,6 +10,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
   config.vm.network "forwarded_port", guest: 5000, host: 9001
   config.vm.provision "shell" do |s|
     s.inline = "/opt/redash/current/setup/vagrant/provision.sh"
-    s.privileged = false
+    s.privileged = true
   end
 end
diff --git a/redash/models.py b/redash/models.py
index c9ca984b9a..5df37508a5 100644
--- a/redash/models.py
+++ b/redash/models.py
@@ -1225,7 +1225,7 @@ def to_dict(self):
         return d
 
 
-all_models = (Organization, Group, DataSource, DataSourceGroup, User, QueryResult, Query, Alert, Dashboard, Visualization, Widget, Event, NotificationDestination, AlertSubscription, ApiKey)
+all_models = (Organization, Group, DataSource, DataSourceGroup, User, QueryResult, Query, Alert, Dashboard, Visualization, Widget, Event, NotificationDestination, AlertSubscription, ApiKey, QuerySnippet)
 
 
 def init_db():
diff --git a/redash/query_runner/memsql_ds.py b/redash/query_runner/memsql_ds.py
new file mode 100644
index 0000000000..14e7f4d119
--- /dev/null
+++ b/redash/query_runner/memsql_ds.py
@@ -0,0 +1,147 @@
+import json
+import logging
+import sys
+
+from redash.query_runner import *
+from redash.utils import JSONEncoder
+
+logger = logging.getLogger(__name__)
+
+try:
+    from memsql.common import database
+    enabled = True
+except ImportError, e:
+    logger.warning(e)
+    enabled = False
+
+COLUMN_NAME = 0
+COLUMN_TYPE = 1
+
+types_map = {
+    'BIGINT': TYPE_INTEGER,
+    'TINYINT': TYPE_INTEGER,
+    'SMALLINT': TYPE_INTEGER,
+    'MEDIUMINT': TYPE_INTEGER,
+    'INT': TYPE_INTEGER,
+    'DOUBLE': TYPE_FLOAT,
+    'DECIMAL': TYPE_FLOAT,
+    'FLOAT': TYPE_FLOAT,
+    'REAL': TYPE_FLOAT,
+    'BOOL': TYPE_BOOLEAN,
+    'BOOLEAN': TYPE_BOOLEAN,
+    'TIMESTAMP': TYPE_DATETIME,
+    'DATETIME': TYPE_DATETIME,
+    'DATE': TYPE_DATETIME,
+    'JSON': TYPE_STRING,
+    'CHAR': TYPE_STRING,
+    'VARCHAR': TYPE_STRING
+}
+
+
+class MemSQL(BaseSQLQueryRunner):
+    @classmethod
+    def configuration_schema(cls):
+        return {
+            "type": "object",
+            "properties": {
+                "host": {
+                    "type": "string"
+                },
+                "port": {
+                    "type": "number"
+                },
+                "user": {
+                    "type": "string"
+                },
+                "password": {
+                    "type": "string"
+                }
+
+            },
+            "required": ["host", "port"]
+        }
+
+    @classmethod
+    def annotate_query(cls):
+        return False
+
+    @classmethod
+    def type(cls):
+        return "memsql"
+
+    @classmethod
+    def enabled(cls):
+        return enabled
+
+    def __init__(self, configuration):
+        super(MemSQL, self).__init__(configuration)
+
+    def _get_tables(self, schema):
+        try:
+            schemas_query = "show schemas"
+
+            tables_query = "show tables in %s"
+
+            columns_query = "show columns in %s"
+
+            for schema_name in filter(lambda a: len(a) > 0, map(lambda a: str(a['Database']), self._run_query_internal(schemas_query))):
+                for table_name in filter(lambda a: len(a) > 0, map(lambda a: str(a['Tables_in_%s' % schema_name]), self._run_query_internal(tables_query % schema_name))):
+                    columns = filter(lambda a: len(a) > 0, map(lambda a: str(a['Field']), self._run_query_internal(columns_query % table_name)))
+
+                    schema[table_name] = {'name': table_name, 'columns': columns}
+        except Exception, e:
+            raise sys.exc_info()[1], None, sys.exc_info()[2]
+        return schema.values()
+
+    def run_query(self, query):
+
+        cursor = None
+        try:
+            cursor = database.connect(**self.configuration.to_dict())
+
+            res = cursor.query(query)
+            # column_names = []
+            # columns = []
+            #
+            # for column in cursor.description:
+            #     column_name = column[COLUMN_NAME]
+            #     column_names.append(column_name)
+            #
+            #     columns.append({
+            #         'name': column_name,
+            #         'friendly_name': column_name,
+            #         'type': types_map.get(column[COLUMN_TYPE], None)
+            #     })
+
+            rows = [dict(zip(list(row.keys()), list(row.values()))) for row in res]
+
+
+            #====================================================================================================
+            #temporary - until https://github.com/memsql/memsql-python/pull/8 gets merged
+            #====================================================================================================
+            columns = []
+            column_names = rows[0].keys() if rows else None
+            for column in column_names:
+                columns.append({
+                    'name': column,
+                    'friendly_name': column,
+                    'type': None
+                })
+
+            data = {'columns': columns, 'rows': rows}
+            json_data = json.dumps(data, cls=JSONEncoder)
+            error = None
+        except KeyboardInterrupt:
+            cursor.close()
+            error = "Query cancelled by user."
+            json_data = None
+        except Exception as e:
+            logging.exception(e)
+            raise sys.exc_info()[1], None, sys.exc_info()[2]
+        finally:
+            if cursor:
+                cursor.close()
+
+        return json_data, error
+
+register(MemSQL)
diff --git a/redash/settings.py b/redash/settings.py
index 9725d9164f..5b1454df48 100644
--- a/redash/settings.py
+++ b/redash/settings.py
@@ -175,6 +175,7 @@ def all_settings():
     'redash.query_runner.sqlite',
     'redash.query_runner.dynamodb_sql',
     'redash.query_runner.mssql',
+    'redash.query_runner.memsql_ds',
 ]
 
 enabled_query_runners = array_from_string(os.environ.get("REDASH_ENABLED_QUERY_RUNNERS", ",".join(default_query_runners)))
diff --git a/requirements.txt b/requirements.txt
index 0a348ea924..d46a454d3c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -40,3 +40,5 @@ xlsxwriter==0.8.4
 pystache==0.5.4
 parsedatetime==2.1
 cryptography==1.4
+oauthlib==2.0.0
+WTForms==2.1
diff --git a/requirements_all_ds.txt b/requirements_all_ds.txt
index 674567e3b0..5b16cbaca8 100644
--- a/requirements_all_ds.txt
+++ b/requirements_all_ds.txt
@@ -17,3 +17,4 @@ sasl>=0.1.3
 thrift>=0.8.0
 thrift_sasl>=0.1.0
 cassandra-driver==3.1.1
+memsql==2.16.0
diff --git a/setup/ubuntu/files/redash_supervisord_init b/setup/ubuntu/files/redash_supervisord_init
old mode 100644
new mode 100755

From 79187cd29a57ba4c5c90e9e0c67aa61b966540f8 Mon Sep 17 00:00:00 2001
From: Alexander Leibzon <alexander.leibzon@gmail.com>
Date: Tue, 2 May 2017 23:36:11 +0300
Subject: [PATCH 2/4] get_schema fix

---
 redash/query_runner/memsql_ds.py | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/redash/query_runner/memsql_ds.py b/redash/query_runner/memsql_ds.py
index 14e7f4d119..ed2dfdad3f 100644
--- a/redash/query_runner/memsql_ds.py
+++ b/redash/query_runner/memsql_ds.py
@@ -39,6 +39,8 @@
 
 
 class MemSQL(BaseSQLQueryRunner):
+    noop_query = 'SELECT 1'
+
     @classmethod
     def configuration_schema(cls):
         return {
@@ -86,6 +88,7 @@ def _get_tables(self, schema):
 
             for schema_name in filter(lambda a: len(a) > 0, map(lambda a: str(a['Database']), self._run_query_internal(schemas_query))):
                 for table_name in filter(lambda a: len(a) > 0, map(lambda a: str(a['Tables_in_%s' % schema_name]), self._run_query_internal(tables_query % schema_name))):
+                    table_name = '.'.join((schema_name, table_name))
                     columns = filter(lambda a: len(a) > 0, map(lambda a: str(a['Field']), self._run_query_internal(columns_query % table_name)))
 
                     schema[table_name] = {'name': table_name, 'columns': columns}
@@ -93,7 +96,7 @@ def _get_tables(self, schema):
             raise sys.exc_info()[1], None, sys.exc_info()[2]
         return schema.values()
 
-    def run_query(self, query):
+    def run_query(self, query, user):
 
         cursor = None
         try:
@@ -121,12 +124,14 @@ def run_query(self, query):
             #====================================================================================================
             columns = []
             column_names = rows[0].keys() if rows else None
-            for column in column_names:
-                columns.append({
-                    'name': column,
-                    'friendly_name': column,
-                    'type': None
-                })
+
+            if column_names:
+                for column in column_names:
+                    columns.append({
+                        'name': column,
+                        'friendly_name': column,
+                        'type': None
+                    })
 
             data = {'columns': columns, 'rows': rows}
             json_data = json.dumps(data, cls=JSONEncoder)

From b9f8b6cdbf4fa1cc49865764d2e5b9007e64f885 Mon Sep 17 00:00:00 2001
From: Alexander Leibzon <alexander.leibzon@gmail.com>
Date: Tue, 2 May 2017 23:49:09 +0300
Subject: [PATCH 3/4] reformat, as for pep-8

---
 redash/query_runner/memsql_ds.py | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/redash/query_runner/memsql_ds.py b/redash/query_runner/memsql_ds.py
index ed2dfdad3f..ddba85a320 100644
--- a/redash/query_runner/memsql_ds.py
+++ b/redash/query_runner/memsql_ds.py
@@ -9,6 +9,7 @@
 
 try:
     from memsql.common import database
+
     enabled = True
 except ImportError, e:
     logger.warning(e)
@@ -86,10 +87,14 @@ def _get_tables(self, schema):
 
             columns_query = "show columns in %s"
 
-            for schema_name in filter(lambda a: len(a) > 0, map(lambda a: str(a['Database']), self._run_query_internal(schemas_query))):
-                for table_name in filter(lambda a: len(a) > 0, map(lambda a: str(a['Tables_in_%s' % schema_name]), self._run_query_internal(tables_query % schema_name))):
+            for schema_name in filter(lambda a: len(a) > 0,
+                                      map(lambda a: str(a['Database']), self._run_query_internal(schemas_query))):
+                for table_name in filter(lambda a: len(a) > 0, map(lambda a: str(a['Tables_in_%s' % schema_name]),
+                                                                   self._run_query_internal(
+                                                                           tables_query % schema_name))):
                     table_name = '.'.join((schema_name, table_name))
-                    columns = filter(lambda a: len(a) > 0, map(lambda a: str(a['Field']), self._run_query_internal(columns_query % table_name)))
+                    columns = filter(lambda a: len(a) > 0, map(lambda a: str(a['Field']),
+                                                               self._run_query_internal(columns_query % table_name)))
 
                     schema[table_name] = {'name': table_name, 'columns': columns}
         except Exception, e:
@@ -118,10 +123,9 @@ def run_query(self, query, user):
 
             rows = [dict(zip(list(row.keys()), list(row.values()))) for row in res]
 
-
-            #====================================================================================================
-            #temporary - until https://github.com/memsql/memsql-python/pull/8 gets merged
-            #====================================================================================================
+            # ====================================================================================================
+            # temporary - until https://github.com/memsql/memsql-python/pull/8 gets merged
+            # ====================================================================================================
             columns = []
             column_names = rows[0].keys() if rows else None
 
@@ -149,4 +153,5 @@ def run_query(self, query, user):
 
         return json_data, error
 
+
 register(MemSQL)

From 6b7234c910410d8bb90b8c1b9f95f298b928d775 Mon Sep 17 00:00:00 2001
From: Alexander Leibzon <alexander.leibzon@gmail.com>
Date: Thu, 18 May 2017 14:00:13 +0300
Subject: [PATCH 4/4] fixes

---
 redash/query_runner/memsql_ds.py | 32 +++++++++++++++-----------------
 requirements.txt                 |  2 --
 2 files changed, 15 insertions(+), 19 deletions(-)

diff --git a/redash/query_runner/memsql_ds.py b/redash/query_runner/memsql_ds.py
index ddba85a320..d0dd3735a4 100644
--- a/redash/query_runner/memsql_ds.py
+++ b/redash/query_runner/memsql_ds.py
@@ -61,7 +61,8 @@ def configuration_schema(cls):
                 }
 
             },
-            "required": ["host", "port"]
+            "required": ["host", "port"],
+            "secret": ["password"]
         }
 
     @classmethod
@@ -80,25 +81,22 @@ def __init__(self, configuration):
         super(MemSQL, self).__init__(configuration)
 
     def _get_tables(self, schema):
-        try:
-            schemas_query = "show schemas"
+        schemas_query = "show schemas"
 
-            tables_query = "show tables in %s"
+        tables_query = "show tables in %s"
 
-            columns_query = "show columns in %s"
+        columns_query = "show columns in %s"
 
-            for schema_name in filter(lambda a: len(a) > 0,
-                                      map(lambda a: str(a['Database']), self._run_query_internal(schemas_query))):
-                for table_name in filter(lambda a: len(a) > 0, map(lambda a: str(a['Tables_in_%s' % schema_name]),
-                                                                   self._run_query_internal(
-                                                                           tables_query % schema_name))):
-                    table_name = '.'.join((schema_name, table_name))
-                    columns = filter(lambda a: len(a) > 0, map(lambda a: str(a['Field']),
-                                                               self._run_query_internal(columns_query % table_name)))
+        for schema_name in filter(lambda a: len(a) > 0,
+                                  map(lambda a: str(a['Database']), self._run_query_internal(schemas_query))):
+            for table_name in filter(lambda a: len(a) > 0, map(lambda a: str(a['Tables_in_%s' % schema_name]),
+                                                               self._run_query_internal(
+                                                                       tables_query % schema_name))):
+                table_name = '.'.join((schema_name, table_name))
+                columns = filter(lambda a: len(a) > 0, map(lambda a: str(a['Field']),
+                                                           self._run_query_internal(columns_query % table_name)))
 
-                    schema[table_name] = {'name': table_name, 'columns': columns}
-        except Exception, e:
-            raise sys.exc_info()[1], None, sys.exc_info()[2]
+                schema[table_name] = {'name': table_name, 'columns': columns}
         return schema.values()
 
     def run_query(self, query, user):
@@ -134,7 +132,7 @@ def run_query(self, query, user):
                     columns.append({
                         'name': column,
                         'friendly_name': column,
-                        'type': None
+                        'type': TYPE_STRING
                     })
 
             data = {'columns': columns, 'rows': rows}
diff --git a/requirements.txt b/requirements.txt
index 8fac8920f6..b703b559c4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -42,6 +42,4 @@ xlsxwriter==0.9.3
 pystache==0.5.4
 parsedatetime==2.1
 cryptography==1.4
-oauthlib==2.0.0
-WTForms==2.1
 simplejson==3.10.0