From 847f3e0c92b5ac17200b2090bedcc5443bb08e4b Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 13 Nov 2017 13:10:55 -0800 Subject: [PATCH] Implemented offset/limit pagination for views Closes #70 --- datasette/app.py | 28 +++++++++++++++++++++------- tests/test_app.py | 21 ++++++++++++++++----- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index e94c7442..31e48137 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -382,8 +382,12 @@ class TableView(BaseView): params = {} next = special_args.get('_next') + offset = '' if next: - if use_rowid: + if is_view: + # _next is an offset + offset = ' offset {}'.format(int(next)) + elif use_rowid: where_clauses.append( 'rowid > :p{}'.format( len(params), @@ -410,8 +414,13 @@ class TableView(BaseView): if order_by: order_by = 'order by {} '.format(order_by) - sql = 'select {} from {} {}{}limit {}'.format( - select, escape_sqlite_table_name(table), where_clause, order_by, self.page_size + 1, + sql = 'select {select} from {table_name} {where}{order_by}limit {limit}{offset}'.format( + select=select, + table_name=escape_sqlite_table_name(table), + where=where_clause, + order_by=order_by, + limit=self.page_size + 1, + offset=offset, ) rows, truncated, description = await self.execute(name, sql, params, truncate=True) @@ -423,11 +432,16 @@ class TableView(BaseView): rows = list(rows) info = self.ds.inspect() table_rows = info[name]['tables'].get(table) - next = None + next_value = None next_url = None if len(rows) > self.page_size: - next = path_from_row_pks(rows[-2], pks, use_rowid) - next_url = urllib.parse.urljoin(request.url, path_with_added_args(request, {'_next': next})) + if is_view: + next_value = int(next or 0) + self.page_size + else: + next_value = path_from_row_pks(rows[-2], pks, use_rowid) + next_url = urllib.parse.urljoin(request.url, path_with_added_args(request, { + '_next': next_value, + })) return { 'database': name, 'table': table, @@ -443,7 +457,7 @@ class TableView(BaseView): 'sql': sql, 'params': params, }, - 'next': next, + 'next': next_value and str(next_value) or None, 'next_url': next_url, }, lambda: { 'database_hash': hash, diff --git a/tests/test_app.py b/tests/test_app.py index ad5b9386..cf803d47 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -122,8 +122,11 @@ def test_table_with_slashes_in_name(app_client): }] -def test_paginate(app_client): - path = '/test_tables/no_primary_key.jsono' +@pytest.mark.parametrize('path,expected_rows,expected_pages', [ + ('/test_tables/no_primary_key.jsono', 201, 5), + ('/test_tables/paginated_view.jsono', 201, 5), +]) +def test_paginate_tables_and_views(app_client, path, expected_rows, expected_pages): fetched = [] count = 0 while path: @@ -132,9 +135,11 @@ def test_paginate(app_client): fetched.extend(response.json['rows']) path = response.json['next_url'] if path: - assert path.endswith(response.json['next']) - assert 201 == len(fetched) - assert 5 == count + assert response.json['next'] and path.endswith(response.json['next']) + assert count < 10, 'Possible infinite loop detected' + + assert expected_rows == len(fetched) + assert expected_pages == count def test_max_returned_rows(app_client): @@ -190,6 +195,12 @@ WITH RECURSIVE ) INSERT INTO no_primary_key SELECT * from cnt; +CREATE VIEW paginated_view AS + SELECT + content, + '- ' || content || ' -' AS content_extra + FROM no_primary_key; + CREATE TABLE "Table With Space In Name" ( pk varchar(30) primary key, content text