Commits

Kirill Simonov committed baa519b

Added parameter `tweak.shell.limit`, button "Load More Data".

Comments (0)

Files changed (7)

src/htsql/cmd/act.py

     pass
 
 
+class SafeProduceAction(ProduceAction):
+
+    def __init__(self, limit):
+        assert isinstance(limit, int) and limit > 0
+        self.limit = limit
+
+
 class AnalyzeAction(Action):
     pass
 
     return act(command, action)
 
 
+def safe_produce(command, limit):
+    action = SafeProduceAction(limit)
+    return act(command, action)
+
+
 def analyze(command):
     action = AnalyzeAction()
     return act(command, action)

src/htsql/cmd/retrieve.py

 
 from ..adapter import adapts, Utility
 from .command import RetrieveCmd, SQLCmd
-from .act import act, analyze, Act, ProduceAction, AnalyzeAction, RenderAction
+from .act import (act, analyze, Act, ProduceAction, SafeProduceAction,
+                  AnalyzeAction, RenderAction)
 from ..tr.encode import encode
+from ..tr.flow import OrderedFlow
 from ..tr.rewrite import rewrite
 from ..tr.compile import compile
 from ..tr.assemble import assemble
     def __call__(self):
         binding = self.command.binding
         expression = encode(binding)
+        # FIXME: abstract it out.
+        if isinstance(self.action, SafeProduceAction):
+            limit = self.action.limit
+            expression = self.safe_patch(expression, limit)
         expression = rewrite(expression)
         term = compile(expression)
         frame = assemble(term)
                 raise
         return Product(profile, records)
 
+    def safe_patch(self, expression, limit):
+        segment = expression.segment
+        if segment is None:
+            return expression
+        flow = segment.flow
+        while not flow.is_axis:
+            if (isinstance(flow, OrderedFlow) and flow.limit is not None
+                                              and flow.limit <= limit):
+                return expression
+            flow = flow.base
+        if flow.is_root:
+            return expression
+        if isinstance(segment.flow, OrderedFlow):
+            flow = segment.flow.clone(limit=limit)
+        else:
+            flow = OrderedFlow(segment.flow, [], limit, None, segment.binding)
+        segment = segment.clone(flow=flow)
+        expression = expression.clone(segment=segment)
+        return expression
+
 
 class AnalyzeRetrieve(Act):
 

src/htsql_tweak/autolimit/encode.py

     def __call__(self):
         code = super(AutolimitEncodeSegment, self).__call__()
         limit = context.app.tweak.autolimit.limit
-        if limit is None:
+        if limit is None or limit <= 0:
             return code
         flow = code.flow
-        while isinstance(flow, OrderedFlow):
-            if flow.limit is not None and flow.limit < limit:
+        while not flow.is_axis:
+            if (isinstance(flow, OrderedFlow) and flow.limit is not None
+                                              and flow.limit <= limit):
                 return code
             flow = flow.base
+        if flow.is_root:
+            return code
         flow = OrderedFlow(code.flow, [], limit, None, code.binding)
         return code.clone(flow=flow)
 

src/htsql_tweak/shell/__init__.py

 
 from . import command, locate
 from htsql.addon import Addon, Parameter
-from htsql.validator import StrVal
+from htsql.validator import StrVal, PIntVal
 
 
 class TweakShellAddon(Addon):
 
     parameters = [
             Parameter('server_root', StrVal(r'^https?://.+$')),
+            Parameter('limit', PIntVal(is_nullable=True), default=1000),
     ]
 
 

src/htsql_tweak/shell/command.py

 from htsql.domain import (Domain, BooleanDomain, NumberDomain, DateTimeDomain)
 from htsql.cmd.command import UniversalCmd, Command
 from htsql.cmd.act import (Act, RenderAction, UnsupportedActionError,
-                           produce, analyze)
+                           produce, safe_produce, analyze)
 from htsql.tr.error import TranslateError
 from htsql.tr.syntax import StringSyntax, NumberSyntax, SegmentSyntax
 from htsql.tr.binding import CommandBinding
 
 class EvaluateCmd(Command):
 
-    def __init__(self, query, action=None, limit=None):
+    def __init__(self, query, action=None, page=None):
         assert isinstance(query, str)
         assert isinstance(action, maybe(str))
-        assert isinstance(limit, maybe(int))
+        assert isinstance(page, maybe(int))
         self.query = query
         self.action = action
-        self.limit = limit
+        self.page = page
 
 
 class ShellSig(Signature):
     slots = [
             Slot('query'),
             Slot('action', is_mandatory=False),
-            Slot('limit', is_mandatory=False),
+            Slot('page', is_mandatory=False),
     ]
 
 
     named('evaluate')
     signature = EvaluateSig
 
-    def expand(self, query, action, limit):
+    def expand(self, query, action, page):
         if not isinstance(query, StringSyntax):
             raise BindError("a string literal is required", query.mark)
         query = query.value
                 raise BindError("'produce' or 'analyze' is expected",
                                 action.mark)
             action = action.value
-        if limit is not None:
-            if not isinstance(limit, NumberSyntax) and limit.is_integer:
-                raise BindError("an integer literal is required", limit.mark)
-            limit = int(limit.value)
-        command = EvaluateCmd(query, action, limit)
+        if page is not None:
+            if not isinstance(page, NumberSyntax) and page.is_integer:
+                raise BindError("an integer literal is required", page.mark)
+            page = int(page.value)
+        command = EvaluateCmd(query, action, page)
         return CommandBinding(self.state.scope, command, self.syntax)
 
 
     adapts(EvaluateCmd, RenderAction)
 
     def __call__(self):
+        addon = context.app.tweak.shell
         status = "200 OK"
         headers = [('Content-Type', 'application/javascript')]
         command = UniversalCmd(self.command.query)
+        limit = None
         try:
             if self.command.action == 'analyze':
                 plan = analyze(command)
             else:
-                product = produce(command)
+                page = self.command.page
+                if page is not None and page > 0 and addon.limit is not None:
+                    limit = page*addon.limit
+                if limit is not None:
+                    product = safe_produce(command, limit+1)
+                else:
+                    product = produce(command)
         except UnsupportedActionError, exc:
             body = self.render_unsupported(exc)
         except HTTPError, exc:
                 body = self.render_sql(plan)
             else:
                 if product:
-                    body = self.render_product(product)
+                    body = self.render_product(product, limit)
                 else:
                     body = self.render_empty()
         return (status, headers, body)
         yield "  \"last_column\": %s\n" % last_column
         yield "}\n"
 
-    def render_product(self, product):
+    def render_product(self, product, limit):
         style = self.make_style(product)
         head = self.make_head(product)
-        body = self.make_body(product)
+        body = self.make_body(product, limit)
+        more = self.make_more(product, limit)
         yield "{\n"
         yield "  \"type\": \"product\",\n"
         yield "  \"style\": %s,\n" % style
         yield "  \"head\": %s,\n" % head
-        yield "  \"body\": %s\n" % body
+        yield "  \"body\": %s,\n" % body
+        yield "  \"more\": %s\n" % more
         yield "}\n"
 
     def render_empty(self):
             rows.append("[%s]" % ", ".join(cells))
         return "[%s]" % ", ".join(rows)
 
-    def make_body(self, product):
+    def make_body(self, product, limit):
         rows = []
         domains = [element.domain
                    for element in product.profile.segment.elements]
                     cell = escape(cgi.escape(format(value)))
                 cells.append(cell)
             rows.append("[%s]" % ", ".join(cells))
+        if limit is not None:
+            rows = rows[:limit]
         return "[%s]" % ", ".join(rows)
 
+    def make_more(self, product, limit):
+        if limit is not None and len(product.records) >= limit:
+            return "true"
+        return "false"
+
 
 class GetStyle(Adapter):
 

src/htsql_tweak/shell/static/shell.css

   max-width: 400px;
 }
 
+#grid button {
+  text-align: center;
+  padding: 0.2em 1.2em;
+  margin: 1em 2em 3em;
+  color: #1a1a1a;
+  background: #abbbbe;
+  border: 1px solid #999999;
+  -moz-border-radius: 3px;
+  -webkit-border-radius: 3px;
+  border-radius: 3px;
+  box-shadow: 0 1px 0 rgba(255,255,255,0.2) inset, 0 1px 2px rgba(0,0,0,0.05);
+  text-shadow: 0 1px 1px rgba(255,255,255,0.75);
+}
+
+#grid button:hover {
+  color: #fff;
+}
+
+#grid li button:active {
+  background-image: -moz-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.05), rgba(0,0,0,0));
+  background-image: -webkit-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.05), rgba(0,0,0,0));
+  background-image: linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.05), rgba(0,0,0,0));
+}
+
 /* Grid scrolling */
 
 #grid {

src/htsql_tweak/shell/static/shell.js

             waiting: false,
             lastQuery: null,
             lastAction: null,
+            lastPage: null,
+            lastOffset: null,
             marker: null,
             expansion: 0,
             lastPoint: null,
     function clickRun() {
         var query = getQuery();
         pushHistory(query)
-        run(query, null);
+        run(query);
     }
 
     function clickExpand(dir) {
         }
     }
 
+    function clickLoad() {
+        var query = state.lastQuery
+        var page = state.lastPage || 1;
+        state.lastOffset = $gridBody.scrollTop();
+        run(query, 'produce', page+1);
+    }
+
     function clickClose() {
         if (state.$panel) {
             state.$panel.hide();
             history.pushState(data, title, url);
     }
 
-    function run(query, action) {
+    function run(query, action, page) {
         if (!query)
             return;
         if (!config.serverRoot)
         }
         state.lastQuery = query;
         state.lastAction = action;
+        state.lastPage = page;
+        if (!action)
+            action = 'produce';
+        if (!page)
+            page = 1;
         query = "/evaluate('" + query.replace(/'/g,"''") + "'"
-                + (action ? ",'" + action + "'" : "") + ")";
+                + ",'" + action + "'" + "," + page + ")";
         var url = config.serverRoot+escape(query);
         $.ajax({
             url: url,
         table += '<td class="dummy">&nbsp;</td>';
         table += '</tbody>';
         table += '</table>';
+        if (output.more) {
+            table += '<button id="load">Load More Data</button>';
+        }
         $gridHead.empty();
         $gridBody.empty()
             .css({ top: 0,
         updateTitle(title);
         $gridBody.html(table);
         $gridBody.scrollLeft(0).scrollTop(0);
+        if (state.lastPage && state.lastOffset) {
+            $gridBody.scrollTop(state.lastOffset);
+            state.lastOffset = 0;
+        }
+        $('#load').click(clickLoad);
         setTimeout(configureGrid, 0);
     }