Commits

Kirill Simonov committed 630c141

Introduced per-request state, transaction management.

  • Participants
  • Parent commits 5ad91c7

Comments (0)

Files changed (8)

src/htsql/core/__init__.py

                domain, entity, error, introspect, mark, split_sql,
                tr, util, validator, wsgi)
 from .validator import DBVal, StrVal, BoolVal
-from .addon import Addon, Parameter, addon_registry
+from .addon import Addon, Parameter, Variable, addon_registry
 from .connect import connect, DBError
 from .introspect import introspect
 from .cache import GeneralCache
                       hint="""dump debug information""")
     ]
 
+    variables = [
+            Variable('connection'),
+            Variable('can_read', True),
+            Variable('can_write', True),
+    ]
+
     packages = ['.', '.cmd', '.fmt', '.tr', '.tr.fn']
     prerequisites = []
     postrequisites = ['engine']

src/htsql/core/addon.py

         return "%s=%s" % (attribute, value_name)
 
 
+class Variable(object):
+
+    def __init__(self, attribute, default=None):
+        assert isinstance(attribute, str)
+        assert re.match(r'^[a-zA-Z_][0-9a-zA-Z_]*$', attribute)
+        self.attribute = attribute
+        self.default = default
+
+
 class Addon(object):
     """
     Implements an addon for HTSQL applications.
 
     name = None
     parameters = []
+    variables = []
     hint = None
     help = None
 

src/htsql/core/application.py

 from .cmd.act import produce
 
 
+class EnvironmentGuard(object):
+
+    def __init__(self, env, updates):
+        self.env = env
+        self.updates = updates
+
+    def __enter__(self):
+        self.env.push(**self.updates)
+
+    def __exit__(self):
+        self.env.pop()
+
+
+class Environment(object):
+    """
+    Implements a per-request HTSQL state.
+    """
+
+    def __init__(self, **variables):
+        self.updates_stack = []
+        self.__dict__.update(variables)
+
+    def push(self, **updates):
+        reverse_updates = {}
+        for name in sorted(updates):
+            assert hasattr(self, name), name
+            reverse_updates[name] = getattr(self, name)
+            setattr(self, name, updates[name])
+        self.updates_stack.append(reverse_updates)
+
+    def pop(self):
+        assert self.updates_stack
+        updates = self.updates_stack.pop()
+        for name in updates:
+            setattr(self, name, updates[name])
+
+    def __call__(self, **updates):
+        return EnvironmentGuard(self, updates)
+
+
 class Application(object):
     """
     Implements an HTSQL application.
                 addon_instance_by_name[addon_name] = addon_instance
         for addon_name in sorted(addon_instance_by_name):
             self.addons.append(addon_instance_by_name[addon_name])
+        self.variables = {}
+        for addon in self.addons:
+            for variable in addon.variables:
+                if variable.attribute in self.variables:
+                    raise ImportError("duplicate HTSQL environment variable %r"
+                                      % variable.attribute)
+                self.variables[variable.attribute] = variable.default
         self.component_registry = ComponentRegistry(self.addons)
         with self:
             for addon in self.addons:
         """
         Activates the application in the current thread.
         """
-        context.push(self)
+        env = Environment(**self.variables)
+        context.push(self, env)
 
     def __exit__(self, exc_type, exc_value, exc_traceback):
         """

src/htsql/core/cmd/retrieve.py

 
 from ..adapter import adapt, Utility
 from ..util import Record, listof
+from ..context import context
 from ..domain import ListDomain, RecordDomain, Profile
 from .command import RetrieveCmd, SQLCmd
 from .act import (analyze, Act, ProduceAction, SafeProduceAction,
 from ..tr.reduce import reduce
 from ..tr.dump import serialize
 from ..tr.plan import Statement
-from ..connect import DBError, connect, normalize
-from ..error import EngineError
+from ..connect import transaction, normalize
+from ..error import PermissionError
 
 
 class Product(object):
 
     def __call__(self):
         binding = self.command.binding
+        if not context.env.can_read:
+            raise PermissionError("not enough permissions"
+                                  " to execute the query")
         expression = encode(binding)
         # FIXME: abstract it out.
         if isinstance(self.action, SafeProduceAction):
         data = None
         if plan.statement:
             stream = None
-            connection = None
-            try:
-                connection = connect()
+            with transaction() as connection:
                 cursor = connection.cursor()
                 stream = RowStream.open(plan.statement, cursor)
-                connection.commit()
-                connection.release()
-            except DBError, exc:
-                raise EngineError("failed to execute a database query: %s"
-                                  % exc)
-            except:
-                if connection is not None:
-                    connection.invalidate()
-                raise
             data = plan.compose(None, stream)
             stream.close()
         return Product(meta, data)

src/htsql/core/connect.py

 from .util import Record
 from .adapter import Adapter, Utility, adapt
 from .domain import Domain
+from .error import EngineError
 from .context import context
 
 
         return None
 
 
+class TransactionGuard(object):
+
+    def __init__(self):
+        self.connection = context.env.connection
+
+    def __enter__(self):
+        if self.connection is None:
+            try:
+                connection = connect()
+            except DBError, exc:
+                raise EngineError("failed to open a database connection: %s"
+                                  % exc)
+            context.env.push(connection=connection)
+            return connection
+        return self.connection
+
+    def __exit__(self, exc_type, exc_value, exc_traceback):
+        if self.connection is None:
+            connection = context.env.connection
+            context.env.pop()
+            if exc_type is None:
+                connection.commit()
+            else:
+                connection.rollback()
+                connection.invalidate()
+            connection.release()
+            if exc_type is not None and issubclass(exc_type, DBError):
+                if isinstance(exc_value, Exception):
+                    exception = exc_value
+                elif exc_value is None:
+                    exception = exc_type()
+                elif isinstance(exc_value, tuple):
+                    exception = exc_type(*exc_value)
+                else:
+                    exception = exc_type(exc_value)
+                raise EngineError("failed to execute a database query: %s"
+                                  % exception)
+
+
+def transaction():
+    return TransactionGuard()
+
+
 connect = Connect.__invoke__
 normalize = Normalize.__invoke__
 normalize_error = NormalizeError.__invoke__

src/htsql/core/context.py

 
 class ThreadContext(threading.local):
     """
-    Keeps the active HTSQL application.
+    Keeps the active HTSQL application and environment.
     """
 
     def __init__(self):
         self.active_app = None
-        self.app_stack = []
+        self.active_env = None
+        self.stack = []
 
-    def push(self, app):
-        self.app_stack.append(self.active_app)
+    def push(self, app, env):
+        self.stack.append((self.active_app, self.active_env))
         self.active_app = app
+        self.active_env = env
 
     def pop(self, app):
         assert app is self.active_app
-        assert self.app_stack
-        self.active_app = self.app_stack.pop()
+        assert self.stack
+        self.active_app, self.active_env = self.stack.pop()
 
     @property
     def app(self):
             raise RuntimeError("HTSQL application is not activated")
         return self.active_app
 
+    @property
+    def env(self):
+        if self.active_env is None:
+            raise RuntimeError("HTSQL environment is not activated")
+        return self.active_env
+
 
 context = ThreadContext()
 

src/htsql/core/error.py

 #
 
 
-class InvalidSyntaxError(BadRequestError):
-    """
-    Represents an invalid syntax error.
-
-    This exception is raised by the scanner when it cannot tokenize the query,
-    or by the parser when it finds an unexpected token.
-    """
-
-    kind = "invalid syntax"
-
-
-class InvalidArgumentError(BadRequestError):
-
-    kind = "invalid argument"
-
-
 class EngineError(ConflictError):
 
     kind = "engine failure"
 
 
+class PermissionError(ForbiddenError):
+
+    kind = "not enough permissions"
+
+

test/output/pgsql.yaml

           body: |2
              | datetime('2010-04-15') | datetime('2010-04-15 20:13') | datetime('2010-04-15T20:13:04.5') | datetime('2010-04-15 20:13:04.5 -0400') |
             -+------------------------+------------------------------+-----------------------------------+-----------------------------------------+-
-             | 2010-04-15             | 2010-04-15 20:13:00          | 2010-04-15 20:13:04.500000        | 2010-04-15 20:13:04.500000-04:00        |
+             | 2010-04-15             | 2010-04-15 20:13:00          | 2010-04-15 20:13:04.500000        | 2010-04-15 19:13:04.500000-05:00        |
 
              ----
              /{datetime('2010-04-15'),datetime('2010-04-15 20:13'),datetime('2010-04-15T20:13:04.5'),datetime('2010-04-15 20:13:04.5 -0400')}