We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
datatablesview: show columns feature bug
## CKAN version
master, 2.10 (earlier?)

## Describe the bug
If any columns are unselected when creating a view, the view will not appear.

### Steps to reproduce
When creating or editing a datatablesview (not viewing an existing one) un-check some columns and save the view.

### Expected behavior
Those columns should be excluded but view should still work.

### Additional details




--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ckanext/datastore/helpers.py`
Content:
```
1 # encoding: utf-8
2 from __future__ import annotations
3 
4 import json
5 import logging
6 from typing import (
7     Any, Iterable, Optional, Sequence, Union, cast, overload
8 )
9 from typing_extensions import Literal
10 
11 import sqlparse
12 import six
13 
14 import ckan.common as converters
15 import ckan.plugins.toolkit as tk
16 from ckan.types import Context
17 
18 
19 log = logging.getLogger(__name__)
20 
21 
22 def is_single_statement(sql: str):
23     '''Returns True if received SQL string contains at most one statement'''
24     return len(sqlparse.split(sql)) <= 1
25 
26 
27 def is_valid_field_name(name: str):
28     '''
29     Check that field name is valid:
30     * can't start or end with whitespace characters
31     * can't start with underscore
32     * can't contain double quote (")
33     * can't be empty
34     '''
35     return (name and name == name.strip() and
36             not name.startswith('_') and
37             '"' not in name)
38 
39 
40 def is_valid_table_name(name: str):
41     if '%' in name:
42         return False
43     return is_valid_field_name(name)
44 
45 
46 @overload
47 def get_list(input: Literal[None], strip_values: bool = ...) -> Literal[None]:
48     ...
49 
50 
51 @overload
52 def get_list(input: Union[str, "Sequence[Any]"],
53              strip_values: bool = ...) -> list[str]:
54     ...
55 
56 
57 def get_list(input: Any, strip_values: bool = True) -> Optional[list[str]]:
58     '''Transforms a string or list to a list'''
59     if input is None:
60         return
61     if input == '':
62         return []
63 
64     converters_list = converters.aslist(input, ',', True)
65     if strip_values:
66         return [_strip(x) for x in converters_list]
67     else:
68         return converters_list
69 
70 
71 def validate_int(i: Any, non_negative: bool = False):
72     try:
73         i = int(i)
74     except ValueError:
75         return False
76     return i >= 0 or not non_negative
77 
78 
79 def _strip(s: Any):
80     if isinstance(s, str) and len(s) and s[0] == s[-1]:
81         return s.strip().strip('"')
82     return s
83 
84 
85 def should_fts_index_field_type(field_type: str):
86     return field_type.lower() in ['tsvector', 'text', 'number']
87 
88 
89 def get_table_and_function_names_from_sql(context: Context, sql: str):
90     '''Parses the output of EXPLAIN (FORMAT JSON) looking for table and
91     function names
92 
93     It performs an EXPLAIN query against the provided SQL, and parses
94     the output recusively.
95 
96     Note that this requires Postgres 9.x.
97 
98     :param context: a CKAN context dict. It must contain a 'connection' key
99         with the current DB connection.
100     :type context: dict
101     :param sql: the SQL statement to parse for table and function names
102     :type sql: string
103 
104     :rtype: a tuple with two list of strings, one for table and one for
105     function names
106     '''
107 
108     queries = [sql]
109     table_names: list[str] = []
110     function_names: list[str] = []
111 
112     while queries:
113         sql = queries.pop()
114 
115         function_names.extend(_get_function_names_from_sql(sql))
116 
117         result = context['connection'].execute(
118             'EXPLAIN (VERBOSE, FORMAT JSON) {0}'.format(
119                 six.ensure_str(sql))).fetchone()
120 
121         try:
122             query_plan = json.loads(result['QUERY PLAN'])
123             plan = query_plan[0]['Plan']
124 
125             t, q, f = _parse_query_plan(plan)
126             table_names.extend(t)
127             queries.extend(q)
128 
129             function_names = list(set(function_names) | set(f))
130 
131         except ValueError:
132             log.error('Could not parse query plan')
133             raise
134 
135     return table_names, function_names
136 
137 
138 def _parse_query_plan(
139         plan: dict[str, Any]) -> tuple[list[str], list[str], list[str]]:
140     '''
141     Given a Postgres Query Plan object (parsed from the output of an EXPLAIN
142     query), returns a tuple with three items:
143 
144     * A list of tables involved
145     * A list of remaining queries to parse
146     * A list of function names involved
147     '''
148 
149     table_names: list[str] = []
150     queries: list[str] = []
151     functions: list[str] = []
152 
153     if plan.get('Relation Name'):
154         table_names.append(plan['Relation Name'])
155     if 'Function Name' in plan:
156         if plan['Function Name'].startswith(
157                 'crosstab'):
158             try:
159                 queries.append(_get_subquery_from_crosstab_call(
160                     plan['Function Call']))
161             except ValueError:
162                 table_names.append('_unknown_crosstab_sql')
163         else:
164             functions.append(plan['Function Name'])
165 
166     if 'Plans' in plan:
167         for child_plan in plan['Plans']:
168             t, q, f = _parse_query_plan(child_plan)
169             table_names.extend(t)
170             queries.extend(q)
171             functions.extend(f)
172 
173     return table_names, queries, functions
174 
175 
176 def _get_function_names_from_sql(sql: str):
177     function_names: list[str] = []
178 
179     def _get_function_names(tokens: Iterable[Any]):
180         for token in tokens:
181             if isinstance(token, sqlparse.sql.Function):
182                 function_name = cast(str, token.get_name())
183                 if function_name not in function_names:
184                     function_names.append(function_name)
185             if hasattr(token, 'tokens'):
186                 _get_function_names(token.tokens)
187 
188     parsed = sqlparse.parse(sql)[0]
189     _get_function_names(parsed.tokens)
190 
191     return function_names
192 
193 
194 def _get_subquery_from_crosstab_call(ct: str):
195     """
196     Crosstabs are a useful feature some sites choose to enable on
197     their datastore databases. To support the sql parameter passed
198     safely we accept only the simple crosstab(text) form where text
199     is a literal SQL string, otherwise raise ValueError
200     """
201     if not ct.startswith("crosstab('") or not ct.endswith("'::text)"):
202         raise ValueError('only simple crosstab calls supported')
203     ct = ct[10:-8]
204     if "'" in ct.replace("''", ""):
205         raise ValueError('only escaped single quotes allowed in query')
206     return ct.replace("''", "'")
207 
208 
209 def datastore_dictionary(resource_id: str):
210     """
211     Return the data dictionary info for a resource
212     """
213     try:
214         return [
215             f for f in tk.get_action('datastore_search')(
216                 {}, {
217                     u'resource_id': resource_id,
218                     u'limit': 0,
219                     u'include_total': False})['fields']
220             if not f['id'].startswith(u'_')]
221     except (tk.ObjectNotFound, tk.NotAuthorized):
222         return []
223
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ckanext/datastore/helpers.py b/ckanext/datastore/helpers.py
--- a/ckanext/datastore/helpers.py
+++ b/ckanext/datastore/helpers.py
@@ -206,9 +206,13 @@
     return ct.replace("''", "'")
 
 
-def datastore_dictionary(resource_id: str):
+def datastore_dictionary(
+        resource_id: str, include_columns: Optional[list[str]] = None):
     """
-    Return the data dictionary info for a resource
+    Return the data dictionary info for a resource, optionally filtering
+    columns returned.
+
+    include_columns is a list of column ids to include in the output
     """
     try:
         return [
@@ -217,6 +221,8 @@
                     u'resource_id': resource_id,
                     u'limit': 0,
                     u'include_total': False})['fields']
-            if not f['id'].startswith(u'_')]
+            if not f['id'].startswith(u'_') and (
+                include_columns is None or f['id'] in include_columns)
+            ]
     except (tk.ObjectNotFound, tk.NotAuthorized):
         return []

{"golden_diff": "diff --git a/ckanext/datastore/helpers.py b/ckanext/datastore/helpers.py\n--- a/ckanext/datastore/helpers.py\n+++ b/ckanext/datastore/helpers.py\n@@ -206,9 +206,13 @@\n     return ct.replace(\"''\", \"'\")\n \n \n-def datastore_dictionary(resource_id: str):\n+def datastore_dictionary(\n+        resource_id: str, include_columns: Optional[list[str]] = None):\n     \"\"\"\n-    Return the data dictionary info for a resource\n+    Return the data dictionary info for a resource, optionally filtering\n+    columns returned.\n+\n+    include_columns is a list of column ids to include in the output\n     \"\"\"\n     try:\n         return [\n@@ -217,6 +221,8 @@\n                     u'resource_id': resource_id,\n                     u'limit': 0,\n                     u'include_total': False})['fields']\n-            if not f['id'].startswith(u'_')]\n+            if not f['id'].startswith(u'_') and (\n+                include_columns is None or f['id'] in include_columns)\n+            ]\n     except (tk.ObjectNotFound, tk.NotAuthorized):\n         return []\n", "issue": "datatablesview: show columns feature bug\n## CKAN version\r\nmaster, 2.10 (earlier?)\r\n\r\n## Describe the bug\r\nIf any columns are unselected when creating a view, the view will not appear.\r\n\r\n### Steps to reproduce\r\nWhen creating or editing a datatablesview (not viewing an existing one) un-check some columns and save the view.\r\n\r\n### Expected behavior\r\nThose columns should be excluded but view should still work.\r\n\r\n### Additional details\r\n\r\n\r\n\n", "before_files": [{"content": "# encoding: utf-8\nfrom __future__ import annotations\n\nimport json\nimport logging\nfrom typing import (\n    Any, Iterable, Optional, Sequence, Union, cast, overload\n)\nfrom typing_extensions import Literal\n\nimport sqlparse\nimport six\n\nimport ckan.common as converters\nimport ckan.plugins.toolkit as tk\nfrom ckan.types import Context\n\n\nlog = logging.getLogger(__name__)\n\n\ndef is_single_statement(sql: str):\n    '''Returns True if received SQL string contains at most one statement'''\n    return len(sqlparse.split(sql)) <= 1\n\n\ndef is_valid_field_name(name: str):\n    '''\n    Check that field name is valid:\n    * can't start or end with whitespace characters\n    * can't start with underscore\n    * can't contain double quote (\")\n    * can't be empty\n    '''\n    return (name and name == name.strip() and\n            not name.startswith('_') and\n            '\"' not in name)\n\n\ndef is_valid_table_name(name: str):\n    if '%' in name:\n        return False\n    return is_valid_field_name(name)\n\n\n@overload\ndef get_list(input: Literal[None], strip_values: bool = ...) -> Literal[None]:\n    ...\n\n\n@overload\ndef get_list(input: Union[str, \"Sequence[Any]\"],\n             strip_values: bool = ...) -> list[str]:\n    ...\n\n\ndef get_list(input: Any, strip_values: bool = True) -> Optional[list[str]]:\n    '''Transforms a string or list to a list'''\n    if input is None:\n        return\n    if input == '':\n        return []\n\n    converters_list = converters.aslist(input, ',', True)\n    if strip_values:\n        return [_strip(x) for x in converters_list]\n    else:\n        return converters_list\n\n\ndef validate_int(i: Any, non_negative: bool = False):\n    try:\n        i = int(i)\n    except ValueError:\n        return False\n    return i >= 0 or not non_negative\n\n\ndef _strip(s: Any):\n    if isinstance(s, str) and len(s) and s[0] == s[-1]:\n        return s.strip().strip('\"')\n    return s\n\n\ndef should_fts_index_field_type(field_type: str):\n    return field_type.lower() in ['tsvector', 'text', 'number']\n\n\ndef get_table_and_function_names_from_sql(context: Context, sql: str):\n    '''Parses the output of EXPLAIN (FORMAT JSON) looking for table and\n    function names\n\n    It performs an EXPLAIN query against the provided SQL, and parses\n    the output recusively.\n\n    Note that this requires Postgres 9.x.\n\n    :param context: a CKAN context dict. It must contain a 'connection' key\n        with the current DB connection.\n    :type context: dict\n    :param sql: the SQL statement to parse for table and function names\n    :type sql: string\n\n    :rtype: a tuple with two list of strings, one for table and one for\n    function names\n    '''\n\n    queries = [sql]\n    table_names: list[str] = []\n    function_names: list[str] = []\n\n    while queries:\n        sql = queries.pop()\n\n        function_names.extend(_get_function_names_from_sql(sql))\n\n        result = context['connection'].execute(\n            'EXPLAIN (VERBOSE, FORMAT JSON) {0}'.format(\n                six.ensure_str(sql))).fetchone()\n\n        try:\n            query_plan = json.loads(result['QUERY PLAN'])\n            plan = query_plan[0]['Plan']\n\n            t, q, f = _parse_query_plan(plan)\n            table_names.extend(t)\n            queries.extend(q)\n\n            function_names = list(set(function_names) | set(f))\n\n        except ValueError:\n            log.error('Could not parse query plan')\n            raise\n\n    return table_names, function_names\n\n\ndef _parse_query_plan(\n        plan: dict[str, Any]) -> tuple[list[str], list[str], list[str]]:\n    '''\n    Given a Postgres Query Plan object (parsed from the output of an EXPLAIN\n    query), returns a tuple with three items:\n\n    * A list of tables involved\n    * A list of remaining queries to parse\n    * A list of function names involved\n    '''\n\n    table_names: list[str] = []\n    queries: list[str] = []\n    functions: list[str] = []\n\n    if plan.get('Relation Name'):\n        table_names.append(plan['Relation Name'])\n    if 'Function Name' in plan:\n        if plan['Function Name'].startswith(\n                'crosstab'):\n            try:\n                queries.append(_get_subquery_from_crosstab_call(\n                    plan['Function Call']))\n            except ValueError:\n                table_names.append('_unknown_crosstab_sql')\n        else:\n            functions.append(plan['Function Name'])\n\n    if 'Plans' in plan:\n        for child_plan in plan['Plans']:\n            t, q, f = _parse_query_plan(child_plan)\n            table_names.extend(t)\n            queries.extend(q)\n            functions.extend(f)\n\n    return table_names, queries, functions\n\n\ndef _get_function_names_from_sql(sql: str):\n    function_names: list[str] = []\n\n    def _get_function_names(tokens: Iterable[Any]):\n        for token in tokens:\n            if isinstance(token, sqlparse.sql.Function):\n                function_name = cast(str, token.get_name())\n                if function_name not in function_names:\n                    function_names.append(function_name)\n            if hasattr(token, 'tokens'):\n                _get_function_names(token.tokens)\n\n    parsed = sqlparse.parse(sql)[0]\n    _get_function_names(parsed.tokens)\n\n    return function_names\n\n\ndef _get_subquery_from_crosstab_call(ct: str):\n    \"\"\"\n    Crosstabs are a useful feature some sites choose to enable on\n    their datastore databases. To support the sql parameter passed\n    safely we accept only the simple crosstab(text) form where text\n    is a literal SQL string, otherwise raise ValueError\n    \"\"\"\n    if not ct.startswith(\"crosstab('\") or not ct.endswith(\"'::text)\"):\n        raise ValueError('only simple crosstab calls supported')\n    ct = ct[10:-8]\n    if \"'\" in ct.replace(\"''\", \"\"):\n        raise ValueError('only escaped single quotes allowed in query')\n    return ct.replace(\"''\", \"'\")\n\n\ndef datastore_dictionary(resource_id: str):\n    \"\"\"\n    Return the data dictionary info for a resource\n    \"\"\"\n    try:\n        return [\n            f for f in tk.get_action('datastore_search')(\n                {}, {\n                    u'resource_id': resource_id,\n                    u'limit': 0,\n                    u'include_total': False})['fields']\n            if not f['id'].startswith(u'_')]\n    except (tk.ObjectNotFound, tk.NotAuthorized):\n        return []\n", "path": "ckanext/datastore/helpers.py"}], "after_files": [{"content": "# encoding: utf-8\nfrom __future__ import annotations\n\nimport json\nimport logging\nfrom typing import (\n    Any, Iterable, Optional, Sequence, Union, cast, overload\n)\nfrom typing_extensions import Literal\n\nimport sqlparse\nimport six\n\nimport ckan.common as converters\nimport ckan.plugins.toolkit as tk\nfrom ckan.types import Context\n\n\nlog = logging.getLogger(__name__)\n\n\ndef is_single_statement(sql: str):\n    '''Returns True if received SQL string contains at most one statement'''\n    return len(sqlparse.split(sql)) <= 1\n\n\ndef is_valid_field_name(name: str):\n    '''\n    Check that field name is valid:\n    * can't start or end with whitespace characters\n    * can't start with underscore\n    * can't contain double quote (\")\n    * can't be empty\n    '''\n    return (name and name == name.strip() and\n            not name.startswith('_') and\n            '\"' not in name)\n\n\ndef is_valid_table_name(name: str):\n    if '%' in name:\n        return False\n    return is_valid_field_name(name)\n\n\n@overload\ndef get_list(input: Literal[None], strip_values: bool = ...) -> Literal[None]:\n    ...\n\n\n@overload\ndef get_list(input: Union[str, \"Sequence[Any]\"],\n             strip_values: bool = ...) -> list[str]:\n    ...\n\n\ndef get_list(input: Any, strip_values: bool = True) -> Optional[list[str]]:\n    '''Transforms a string or list to a list'''\n    if input is None:\n        return\n    if input == '':\n        return []\n\n    converters_list = converters.aslist(input, ',', True)\n    if strip_values:\n        return [_strip(x) for x in converters_list]\n    else:\n        return converters_list\n\n\ndef validate_int(i: Any, non_negative: bool = False):\n    try:\n        i = int(i)\n    except ValueError:\n        return False\n    return i >= 0 or not non_negative\n\n\ndef _strip(s: Any):\n    if isinstance(s, str) and len(s) and s[0] == s[-1]:\n        return s.strip().strip('\"')\n    return s\n\n\ndef should_fts_index_field_type(field_type: str):\n    return field_type.lower() in ['tsvector', 'text', 'number']\n\n\ndef get_table_and_function_names_from_sql(context: Context, sql: str):\n    '''Parses the output of EXPLAIN (FORMAT JSON) looking for table and\n    function names\n\n    It performs an EXPLAIN query against the provided SQL, and parses\n    the output recusively.\n\n    Note that this requires Postgres 9.x.\n\n    :param context: a CKAN context dict. It must contain a 'connection' key\n        with the current DB connection.\n    :type context: dict\n    :param sql: the SQL statement to parse for table and function names\n    :type sql: string\n\n    :rtype: a tuple with two list of strings, one for table and one for\n    function names\n    '''\n\n    queries = [sql]\n    table_names: list[str] = []\n    function_names: list[str] = []\n\n    while queries:\n        sql = queries.pop()\n\n        function_names.extend(_get_function_names_from_sql(sql))\n\n        result = context['connection'].execute(\n            'EXPLAIN (VERBOSE, FORMAT JSON) {0}'.format(\n                six.ensure_str(sql))).fetchone()\n\n        try:\n            query_plan = json.loads(result['QUERY PLAN'])\n            plan = query_plan[0]['Plan']\n\n            t, q, f = _parse_query_plan(plan)\n            table_names.extend(t)\n            queries.extend(q)\n\n            function_names = list(set(function_names) | set(f))\n\n        except ValueError:\n            log.error('Could not parse query plan')\n            raise\n\n    return table_names, function_names\n\n\ndef _parse_query_plan(\n        plan: dict[str, Any]) -> tuple[list[str], list[str], list[str]]:\n    '''\n    Given a Postgres Query Plan object (parsed from the output of an EXPLAIN\n    query), returns a tuple with three items:\n\n    * A list of tables involved\n    * A list of remaining queries to parse\n    * A list of function names involved\n    '''\n\n    table_names: list[str] = []\n    queries: list[str] = []\n    functions: list[str] = []\n\n    if plan.get('Relation Name'):\n        table_names.append(plan['Relation Name'])\n    if 'Function Name' in plan:\n        if plan['Function Name'].startswith(\n                'crosstab'):\n            try:\n                queries.append(_get_subquery_from_crosstab_call(\n                    plan['Function Call']))\n            except ValueError:\n                table_names.append('_unknown_crosstab_sql')\n        else:\n            functions.append(plan['Function Name'])\n\n    if 'Plans' in plan:\n        for child_plan in plan['Plans']:\n            t, q, f = _parse_query_plan(child_plan)\n            table_names.extend(t)\n            queries.extend(q)\n            functions.extend(f)\n\n    return table_names, queries, functions\n\n\ndef _get_function_names_from_sql(sql: str):\n    function_names: list[str] = []\n\n    def _get_function_names(tokens: Iterable[Any]):\n        for token in tokens:\n            if isinstance(token, sqlparse.sql.Function):\n                function_name = cast(str, token.get_name())\n                if function_name not in function_names:\n                    function_names.append(function_name)\n            if hasattr(token, 'tokens'):\n                _get_function_names(token.tokens)\n\n    parsed = sqlparse.parse(sql)[0]\n    _get_function_names(parsed.tokens)\n\n    return function_names\n\n\ndef _get_subquery_from_crosstab_call(ct: str):\n    \"\"\"\n    Crosstabs are a useful feature some sites choose to enable on\n    their datastore databases. To support the sql parameter passed\n    safely we accept only the simple crosstab(text) form where text\n    is a literal SQL string, otherwise raise ValueError\n    \"\"\"\n    if not ct.startswith(\"crosstab('\") or not ct.endswith(\"'::text)\"):\n        raise ValueError('only simple crosstab calls supported')\n    ct = ct[10:-8]\n    if \"'\" in ct.replace(\"''\", \"\"):\n        raise ValueError('only escaped single quotes allowed in query')\n    return ct.replace(\"''\", \"'\")\n\n\ndef datastore_dictionary(\n        resource_id: str, include_columns: Optional[list[str]] = None):\n    \"\"\"\n    Return the data dictionary info for a resource, optionally filtering\n    columns returned.\n\n    include_columns is a list of column ids to include in the output\n    \"\"\"\n    try:\n        return [\n            f for f in tk.get_action('datastore_search')(\n                {}, {\n                    u'resource_id': resource_id,\n                    u'limit': 0,\n                    u'include_total': False})['fields']\n            if not f['id'].startswith(u'_') and (\n                include_columns is None or f['id'] in include_columns)\n            ]\n    except (tk.ObjectNotFound, tk.NotAuthorized):\n        return []\n", "path": "ckanext/datastore/helpers.py"}]}