Repository: jupyterhub/jupyterhub

Issue:
Fails to start when using mysql as db backend
Ran into `VARCHAR requires a length on dialect mysql` when attempting to use it with MySQL. All the Unicode columns need a length field for MySQL to work.



Discussion:

Resolving patch:
diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py
--- a/jupyterhub/orm.py
+++ b/jupyterhub/orm.py
@@ -12,7 +12,7 @@
 from tornado.log import app_log
 from tornado.httpclient import HTTPRequest, AsyncHTTPClient
 
-from sqlalchemy.types import TypeDecorator, VARCHAR
+from sqlalchemy.types import TypeDecorator, TEXT
 from sqlalchemy import (
     inspect,
     Column, Integer, ForeignKey, Unicode, Boolean,
@@ -39,7 +39,7 @@ class JSONDict(TypeDecorator):
 
     """
 
-    impl = VARCHAR
+    impl = TEXT
 
     def process_bind_param(self, value, dialect):
         if value is not None:
@@ -59,20 +59,20 @@ def process_result_value(self, value, dialect):
 
 class Server(Base):
     """The basic state of a server
-    
+
     connection and cookie info
     """
     __tablename__ = 'servers'
     id = Column(Integer, primary_key=True)
-    proto = Column(Unicode, default='http')
-    ip = Column(Unicode, default='')
+    proto = Column(Unicode(15), default='http')
+    ip = Column(Unicode(255), default='')  # could also be a DNS name
     port = Column(Integer, default=random_port)
-    base_url = Column(Unicode, default='/')
-    cookie_name = Column(Unicode, default='cookie')
-    
+    base_url = Column(Unicode(255), default='/')
+    cookie_name = Column(Unicode(255), default='cookie')
+
     def __repr__(self):
         return "<Server(%s:%s)>" % (self.ip, self.port)
-    
+
     @property
     def host(self):
         ip = self.ip
@@ -91,18 +91,18 @@ def url(self):
             host=self.host,
             uri=self.base_url,
         )
-    
+
     @property
     def bind_url(self):
         """representation of URL used for binding
-        
+
         Never used in APIs, only logging,
         since it can be non-connectable value, such as '', meaning all interfaces.
         """
         if self.ip in {'', '0.0.0.0'}:
             return self.url.replace('127.0.0.1', self.ip or '*', 1)
         return self.url
-    
+
     @gen.coroutine
     def wait_up(self, timeout=10, http=False):
         """Wait for this server to come up"""
@@ -110,7 +110,7 @@ def wait_up(self, timeout=10, http=False):
             yield wait_for_http_server(self.url, timeout=timeout)
         else:
             yield wait_for_server(self.ip or '127.0.0.1', self.port, timeout=timeout)
-    
+
     def is_up(self):
         """Is the server accepting connections?"""
         try:
@@ -136,7 +136,7 @@ def is_up(self):
 
 class Proxy(Base):
     """A configurable-http-proxy instance.
-    
+
     A proxy consists of the API server info and the public-facing server info,
     plus an auth token for configuring the proxy table.
     """
@@ -147,7 +147,7 @@ class Proxy(Base):
     public_server = relationship(Server, primaryjoin=_public_server_id == Server.id)
     _api_server_id = Column(Integer, ForeignKey('servers.id'))
     api_server = relationship(Server, primaryjoin=_api_server_id == Server.id)
-    
+
     def __repr__(self):
         if self.public_server:
             return "<%s %s:%s>" % (
@@ -155,7 +155,7 @@ def __repr__(self):
             )
         else:
             return "<%s [unconfigured]>" % self.__class__.__name__
-    
+
     def api_request(self, path, method='GET', body=None, client=None):
         """Make an authenticated API request of the proxy"""
         client = client or AsyncHTTPClient()
@@ -178,10 +178,11 @@ def add_user(self, user, client=None):
         self.log.info("Adding user %s to proxy %s => %s",
             user.name, user.proxy_path, user.server.host,
         )
+
         if user.spawn_pending:
             raise RuntimeError(
                 "User %s's spawn is pending, shouldn't be added to the proxy yet!", user.name)
-        
+
         yield self.api_request(user.proxy_path,
             method='POST',
             body=dict(
@@ -190,7 +191,7 @@ def add_user(self, user, client=None):
             ),
             client=client,
         )
-    
+
     @gen.coroutine
     def delete_user(self, user, client=None):
         """Remove a user's server to the proxy table."""
@@ -199,7 +200,7 @@ def delete_user(self, user, client=None):
             method='DELETE',
             client=client,
         )
-    
+
     @gen.coroutine
     def get_routes(self, client=None):
         """Fetch the proxy's routes"""
@@ -209,7 +210,7 @@ def get_routes(self, client=None):
     @gen.coroutine
     def add_all_users(self, user_dict):
         """Update the proxy table from the database.
-        
+
         Used when loading up a new proxy.
         """
         db = inspect(self).session
@@ -251,9 +252,9 @@ def check_routes(self, user_dict, routes=None):
 
 class Hub(Base):
     """Bring it all together at the hub.
-    
+
     The Hub is a server, plus its API path suffix
-    
+
     the api_url is the full URL plus the api_path suffix on the end
     of the server base_url.
     """
@@ -262,12 +263,12 @@ class Hub(Base):
     _server_id = Column(Integer, ForeignKey('servers.id'))
     server = relationship(Server, primaryjoin=_server_id == Server.id)
     host = ''
-    
+
     @property
     def api_url(self):
         """return the full API url (with proto://host...)"""
         return url_path_join(self.server.url, 'api')
-    
+
     def __repr__(self):
         if self.server:
             return "<%s %s:%s>" % (
@@ -279,31 +280,31 @@ def __repr__(self):
 
 class User(Base):
     """The User table
-    
+
     Each user has a single server,
     and multiple tokens used for authorization.
-    
+
     API tokens grant access to the Hub's REST API.
     These are used by single-user servers to authenticate requests,
     and external services to manipulate the Hub.
-    
+
     Cookies are set with a single ID.
     Resetting the Cookie ID invalidates all cookies, forcing user to login again.
-    
+
     A `state` column contains a JSON dict,
     used for restoring state of a Spawner.
     """
     __tablename__ = 'users'
     id = Column(Integer, primary_key=True, autoincrement=True)
-    name = Column(Unicode)
+    name = Column(Unicode(1023))
     # should we allow multiple servers per user?
     _server_id = Column(Integer, ForeignKey('servers.id'))
     server = relationship(Server, primaryjoin=_server_id == Server.id)
     admin = Column(Boolean, default=False)
     last_activity = Column(DateTime, default=datetime.utcnow)
-    
+
     api_tokens = relationship("APIToken", backref="user")
-    cookie_id = Column(Unicode, default=new_token)
+    cookie_id = Column(Unicode(1023), default=new_token)
     state = Column(JSONDict)
 
     other_user_cookies = set([])
@@ -321,7 +322,7 @@ def __repr__(self):
                 cls=self.__class__.__name__,
                 name=self.name,
             )
-    
+
     def new_api_token(self):
         """Create a new API token"""
         assert self.id is not None
@@ -344,29 +345,29 @@ def find(cls, db, name):
 class APIToken(Base):
     """An API token"""
     __tablename__ = 'api_tokens'
-    
+
     @declared_attr
     def user_id(cls):
         return Column(Integer, ForeignKey('users.id'))
 
     id = Column(Integer, primary_key=True)
-    hashed = Column(Unicode)
-    prefix = Column(Unicode)
+    hashed = Column(Unicode(1023))
+    prefix = Column(Unicode(1023))
     prefix_length = 4
     algorithm = "sha512"
     rounds = 16384
     salt_bytes = 8
-    
+
     @property
     def token(self):
         raise AttributeError("token is write-only")
-    
+
     @token.setter
     def token(self, token):
         """Store the hashed value and prefix for a token"""
         self.prefix = token[:self.prefix_length]
         self.hashed = hash_token(token, rounds=self.rounds, salt=self.salt_bytes, algorithm=self.algorithm)
-    
+
     def __repr__(self):
         return "<{cls}('{pre}...', user='{u}')>".format(
             cls=self.__class__.__name__,
@@ -387,7 +388,7 @@ def find(cls, db, token):
         for orm_token in prefix_match:
             if orm_token.match(token):
                 return orm_token
-    
+
     def match(self, token):
         """Is this my token?"""
         return compare_token(self.hashed, token)


Test patch: