Repository: jupyterhub/jupyterhub

Issue:
Index too large in MySQL with utf8mb4 collation 
**How to reproduce the issue**
Start jupyterhub with a mysql/mariadb db configured to utf8mb4 collation
**What you expected to happen**
Start jupyterhub
**What actually happens**
Get error:
```
    Traceback (most recent call last):
      File "/usr/local/lib/python3.6/dist-packages/jupyterhub/app.py", line 1620, in launch_instance_async
        yield self.initialize(argv)
      File "/usr/lib/python3.6/types.py", line 204, in __next__
        return next(self.__wrapped)
      File "/usr/local/lib/python3.6/dist-packages/jupyterhub/app.py", line 1373, in initialize
        self.init_db()
      File "/usr/local/lib/python3.6/dist-packages/jupyterhub/app.py", line 895, in init_db
        **self.db_kwargs
      File "/usr/local/lib/python3.6/dist-packages/jupyterhub/orm.py", line 532, in new_session_factory
        Base.metadata.create_all(engine)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/sql/schema.py", line 4004, in create_all
        tables=tables)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/engine/base.py", line 1940, in _run_visitor
        conn._run_visitor(visitorcallable, element, **kwargs)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/engine/base.py", line 1549, in _run_visitor
        **kwargs).traverse_single(element)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/sql/visitors.py", line 121, in traverse_single
        return meth(obj, **kw)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/sql/ddl.py", line 757, in visit_metadata
        _is_metadata_operation=True)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/sql/visitors.py", line 121, in traverse_single
        return meth(obj, **kw)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/sql/ddl.py", line 791, in visit_table
        include_foreign_key_constraints=include_foreign_key_constraints
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/engine/base.py", line 948, in execute
        return meth(self, multiparams, params)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/sql/ddl.py", line 68, in _execute_on_connection
        return connection._execute_ddl(self, multiparams, params)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/engine/base.py", line 1009, in _execute_ddl
        compiled
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/engine/base.py", line 1200, in _execute_context
        context)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/engine/base.py", line 1413, in _handle_dbapi_exception
        exc_info
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/util/compat.py", line 203, in raise_from_cause
        reraise(type(exception), exception, tb=exc_tb, cause=cause)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/util/compat.py", line 186, in reraise
        raise value.with_traceback(tb)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/engine/base.py", line 1193, in _execute_context
        context)
      File "/usr/local/lib/python3.6/dist-packages/sqlalchemy/engine/default.py", line 507, in do_execute
        cursor.execute(statement, parameters)
      File "/usr/local/lib/python3.6/dist-packages/pymysql/cursors.py", line 166, in execute
        result = self._query(query)
      File "/usr/local/lib/python3.6/dist-packages/pymysql/cursors.py", line 322, in _query
        conn.query(q)
      File "/usr/local/lib/python3.6/dist-packages/pymysql/connections.py", line 856, in query
        self._affected_rows = self._read_query_result(unbuffered=unbuffered)
      File "/usr/local/lib/python3.6/dist-packages/pymysql/connections.py", line 1057, in _read_query_result
        result.read()
      File "/usr/local/lib/python3.6/dist-packages/pymysql/connections.py", line 1340, in read
        first_packet = self.connection._read_packet()
      File "/usr/local/lib/python3.6/dist-packages/pymysql/connections.py", line 1014, in _read_packet
        packet.check_error()
      File "/usr/local/lib/python3.6/dist-packages/pymysql/connections.py", line 393, in check_error
        err.raise_mysql_exception(self._data)
      File "/usr/local/lib/python3.6/dist-packages/pymysql/err.py", line 107, in raise_mysql_exception
        raise errorclass(errno, errval)
    sqlalchemy.exc.InternalError: (pymysql.err.InternalError) (1709, 'Index column size too large. The maximum column size is 767 bytes.') [SQL: '\nCREATE TABLE `groups` (\n\tid INTEGER NOT NULL AUTO_INCREMENT, \n\tname VARCHAR(255), \n\tPRIMARY KEY (id), \n\tUNIQUE (name)\n)\n\n'] (Background on this error at: http://sqlalche.me/e/2j85)
```
**Share what version of JupyterHub you are using**
jupyterhub==0.8.1


MySQL and Mariadb can deal with this with setting ` innodb-large-prefix`, but it requires tables to be created either with a `ROW_FORMAT=DYNAMIC` or `ROW_FORMAT=COMPRESSED`.

Could have a config option to set this in the relevant tables at creation?


Discussion:

Resolving patch:
diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py
--- a/jupyterhub/orm.py
+++ b/jupyterhub/orm.py
@@ -621,6 +621,24 @@ def check_db_revision(engine):
         ))
 
 
+def mysql_large_prefix_check(engine):
+    """Check mysql has innodb_large_prefix set"""
+    if not str(engine.url).startswith('mysql'):
+        return False
+    variables =  dict(engine.execute(
+        'show variables where variable_name like '
+        '"innodb_large_prefix" or '
+        'variable_name like "innodb_file_format";').fetchall())
+    if (variables['innodb_file_format'] == 'Barracuda' and
+        variables['innodb_large_prefix'] == 'ON'):
+        return True
+    else:
+        return False
+
+def add_row_format(base):
+    for t in  base.metadata.tables.values():
+        t.dialect_kwargs['mysql_ROW_FORMAT'] = 'DYNAMIC'
+
 def new_session_factory(url="sqlite:///:memory:",
                         reset=False,
                         expire_on_commit=False,
@@ -643,8 +661,11 @@ def new_session_factory(url="sqlite:///:memory:",
     if reset:
         Base.metadata.drop_all(engine)
 
+    if mysql_large_prefix_check(engine):  # if mysql is allows large indexes
+        add_row_format(Base)              # set format on the tables
     # check the db revision (will raise, pointing to `upgrade-db` if version doesn't match)
     check_db_revision(engine)
+
     Base.metadata.create_all(engine)
 
     # We set expire_on_commit=False, since we don't actually need


Test patch: