jupyter-duckdb 1.2.0.2__tar.gz → 1.2.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/PKG-INFO +26 -5
  2. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/README.md +25 -4
  3. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/kernel.py +38 -23
  4. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/ConditionalSet.py +16 -3
  5. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/tokenizer/Token.py +24 -3
  6. jupyter_duckdb-1.2.0.4/src/duckdb_kernel/util/TestError.py +4 -0
  7. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/jupyter_duckdb.egg-info/PKG-INFO +26 -5
  8. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/jupyter_duckdb.egg-info/SOURCES.txt +1 -0
  9. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/test/test_dc.py +96 -2
  10. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/setup.cfg +0 -0
  11. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/setup.py +0 -0
  12. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/__init__.py +0 -0
  13. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/__main__.py +0 -0
  14. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/Column.py +0 -0
  15. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/Connection.py +0 -0
  16. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/Constraint.py +0 -0
  17. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/DatabaseError.py +0 -0
  18. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/ForeignKey.py +0 -0
  19. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/Table.py +0 -0
  20. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/__init__.py +0 -0
  21. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/error/EmptyResultError.py +0 -0
  22. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/error/__init__.py +0 -0
  23. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/implementation/duckdb/Connection.py +0 -0
  24. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/implementation/duckdb/__init__.py +0 -0
  25. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/implementation/postgres/Connection.py +0 -0
  26. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/implementation/postgres/__init__.py +0 -0
  27. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/implementation/postgres/util.py +0 -0
  28. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/implementation/sqlite/Connection.py +0 -0
  29. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/db/implementation/sqlite/__init__.py +0 -0
  30. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/kernel.json +0 -0
  31. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/magics/MagicCommand.py +0 -0
  32. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/magics/MagicCommandCallback.py +0 -0
  33. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/magics/MagicCommandException.py +0 -0
  34. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/magics/MagicCommandHandler.py +0 -0
  35. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/magics/__init__.py +0 -0
  36. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/DCParser.py +0 -0
  37. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/LogicParser.py +0 -0
  38. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/RAParser.py +0 -0
  39. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/__init__.py +0 -0
  40. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/DCOperand.py +0 -0
  41. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/LogicElement.py +0 -0
  42. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/LogicOperand.py +0 -0
  43. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/LogicOperator.py +0 -0
  44. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/RABinaryOperator.py +0 -0
  45. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/RAElement.py +0 -0
  46. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/RAOperand.py +0 -0
  47. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/RAOperator.py +0 -0
  48. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/RAUnaryOperator.py +0 -0
  49. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/__init__.py +0 -0
  50. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Add.py +0 -0
  51. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/And.py +0 -0
  52. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/ArrowLeft.py +0 -0
  53. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Cross.py +0 -0
  54. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Difference.py +0 -0
  55. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Divide.py +0 -0
  56. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Division.py +0 -0
  57. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Equal.py +0 -0
  58. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/GreaterThan.py +0 -0
  59. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/GreaterThanEqual.py +0 -0
  60. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Intersection.py +0 -0
  61. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Join.py +0 -0
  62. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/LessThan.py +0 -0
  63. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/LessThanEqual.py +0 -0
  64. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Minus.py +0 -0
  65. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Multiply.py +0 -0
  66. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Or.py +0 -0
  67. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Unequal.py +0 -0
  68. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/Union.py +0 -0
  69. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/binary/__init__.py +0 -0
  70. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/unary/Not.py +0 -0
  71. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/unary/Projection.py +0 -0
  72. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/unary/Rename.py +0 -0
  73. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/unary/Selection.py +0 -0
  74. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/elements/unary/__init__.py +0 -0
  75. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/tokenizer/Tokenizer.py +0 -0
  76. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/tokenizer/__init__.py +0 -0
  77. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/util/RenamableColumn.py +0 -0
  78. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/util/RenamableColumnList.py +0 -0
  79. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/parser/util/__init__.py +0 -0
  80. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/util/ResultSetComparator.py +0 -0
  81. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/util/__init__.py +0 -0
  82. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/util/formatting.py +0 -0
  83. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/visualization/Drawer.py +0 -0
  84. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/visualization/RATreeDrawer.py +0 -0
  85. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/visualization/SchemaDrawer.py +0 -0
  86. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/duckdb_kernel/visualization/__init__.py +0 -0
  87. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/jupyter_duckdb.egg-info/dependency_links.txt +0 -0
  88. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/jupyter_duckdb.egg-info/requires.txt +0 -0
  89. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/src/jupyter_duckdb.egg-info/top_level.txt +0 -0
  90. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/test/test_ra.py +0 -0
  91. {jupyter_duckdb-1.2.0.2 → jupyter_duckdb-1.2.0.4}/test/test_result_comparison.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: jupyter-duckdb
3
- Version: 1.2.0.2
3
+ Version: 1.2.0.4
4
4
  Summary: a basic wrapper kernel for DuckDB
5
5
  Home-page: https://github.com/erictroebs/jupyter-duckdb
6
6
  Author: Eric Tröbs
@@ -32,10 +32,6 @@ This is a simple DuckDB wrapper kernel which accepts SQL as input, executes it
32
32
  using a previously loaded DuckDB instance and formats the output as a table.
33
33
  There are some magic commands that make teaching easier with this kernel.
34
34
 
35
- ## Quick Start
36
-
37
- [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/git/https%3A%2F%2Fdbgit.prakinf.tu-ilmenau.de%2Fertr8623%2Fjupyter-duckdb.git/master)
38
-
39
35
  ## Table of Contents
40
36
 
41
37
  - [Setup](#setup)
@@ -85,6 +81,12 @@ Execute the following command to pull and run a prepared image.
85
81
  docker run -p 8888:8888 troebs/jupyter-duckdb
86
82
  ```
87
83
 
84
+ There is also a second image. It contains an additional instance of PostgreSQL:
85
+
86
+ ```bash
87
+ docker run -p 8888:8888 troebs/jupyter-duckdb:postgresql
88
+ ```
89
+
88
90
  This image can also be used with JupyterHub and the
89
91
  [DockerSpawner / SwarmSpawner](https://github.com/jupyterhub/dockerspawner)
90
92
  and probably with the
@@ -138,6 +140,13 @@ Please note that `:memory:` is also a valid file path for DuckDB. The data is
138
140
  then stored exclusively in the main memory. In combination with `CREATE`
139
141
  and `OF` this makes it possible to work on a temporary copy in memory.
140
142
 
143
+ Although the name suggests otherwise, the kernel can also be used with other
144
+ databases:
145
+ - **SQLite** is automatically used as a fallback if the DuckDB dependency is
146
+ missing.
147
+ - To connect to a **PostgreSQL** instance, you need to specify a database URI
148
+ starting with `(postgresql|postgres|pgsql|psql|pg)://`.
149
+
141
150
  ### Schema Diagrams
142
151
 
143
152
  The magic command `SCHEMA` can be used to create a simple schema diagram of the
@@ -153,6 +162,10 @@ representation requires more space, but can improve readability.
153
162
  %SCHEMA TD
154
163
  ```
155
164
 
165
+ The optional argument `ONLY`, followed by one or more table names separated by a
166
+ comma, can be used to display only the named tables and all those connected with
167
+ a foreign key.
168
+
156
169
  Graphviz (`dot` in PATH) is required to render schema diagrams.
157
170
 
158
171
  ### Number of Rows
@@ -234,6 +247,11 @@ UNION
234
247
  SELECT 1, 'Name 1'
235
248
  ```
236
249
 
250
+ By default, failed tests will display an explanation, but the notebook will
251
+ continue to run. Set the `DUCKDB_TESTS_RAISE_EXCEPTION` environment variable to
252
+ `true` to raise an exception when a test fails. This can be useful for automated
253
+ testing in CI environments.
254
+
237
255
  Disclaimer: The integrated testing is work-in-progress and thus subject to
238
256
  potentially incompatible changes and enhancements.
239
257
 
@@ -259,6 +277,9 @@ The supported operations are:
259
277
  - Cross Product `×`
260
278
  - Division `÷`
261
279
 
280
+ The optional flag `ANALYZE` can be used to add an execution diagram to the
281
+ output.
282
+
262
283
  The Dockerfile also installs the Jupyter Lab plugin
263
284
  [jupyter-ra-extension](https://pypi.org/project/jupyter-ra-extension/). It adds
264
285
  the symbols mentioned above and some other supported symbols to the toolbar for
@@ -4,10 +4,6 @@ This is a simple DuckDB wrapper kernel which accepts SQL as input, executes it
4
4
  using a previously loaded DuckDB instance and formats the output as a table.
5
5
  There are some magic commands that make teaching easier with this kernel.
6
6
 
7
- ## Quick Start
8
-
9
- [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/git/https%3A%2F%2Fdbgit.prakinf.tu-ilmenau.de%2Fertr8623%2Fjupyter-duckdb.git/master)
10
-
11
7
  ## Table of Contents
12
8
 
13
9
  - [Setup](#setup)
@@ -57,6 +53,12 @@ Execute the following command to pull and run a prepared image.
57
53
  docker run -p 8888:8888 troebs/jupyter-duckdb
58
54
  ```
59
55
 
56
+ There is also a second image. It contains an additional instance of PostgreSQL:
57
+
58
+ ```bash
59
+ docker run -p 8888:8888 troebs/jupyter-duckdb:postgresql
60
+ ```
61
+
60
62
  This image can also be used with JupyterHub and the
61
63
  [DockerSpawner / SwarmSpawner](https://github.com/jupyterhub/dockerspawner)
62
64
  and probably with the
@@ -110,6 +112,13 @@ Please note that `:memory:` is also a valid file path for DuckDB. The data is
110
112
  then stored exclusively in the main memory. In combination with `CREATE`
111
113
  and `OF` this makes it possible to work on a temporary copy in memory.
112
114
 
115
+ Although the name suggests otherwise, the kernel can also be used with other
116
+ databases:
117
+ - **SQLite** is automatically used as a fallback if the DuckDB dependency is
118
+ missing.
119
+ - To connect to a **PostgreSQL** instance, you need to specify a database URI
120
+ starting with `(postgresql|postgres|pgsql|psql|pg)://`.
121
+
113
122
  ### Schema Diagrams
114
123
 
115
124
  The magic command `SCHEMA` can be used to create a simple schema diagram of the
@@ -125,6 +134,10 @@ representation requires more space, but can improve readability.
125
134
  %SCHEMA TD
126
135
  ```
127
136
 
137
+ The optional argument `ONLY`, followed by one or more table names separated by a
138
+ comma, can be used to display only the named tables and all those connected with
139
+ a foreign key.
140
+
128
141
  Graphviz (`dot` in PATH) is required to render schema diagrams.
129
142
 
130
143
  ### Number of Rows
@@ -206,6 +219,11 @@ UNION
206
219
  SELECT 1, 'Name 1'
207
220
  ```
208
221
 
222
+ By default, failed tests will display an explanation, but the notebook will
223
+ continue to run. Set the `DUCKDB_TESTS_RAISE_EXCEPTION` environment variable to
224
+ `true` to raise an exception when a test fails. This can be useful for automated
225
+ testing in CI environments.
226
+
209
227
  Disclaimer: The integrated testing is work-in-progress and thus subject to
210
228
  potentially incompatible changes and enhancements.
211
229
 
@@ -231,6 +249,9 @@ The supported operations are:
231
249
  - Cross Product `×`
232
250
  - Division `÷`
233
251
 
252
+ The optional flag `ANALYZE` can be used to add an execution diagram to the
253
+ output.
254
+
234
255
  The Dockerfile also installs the Jupyter Lab plugin
235
256
  [jupyter-ra-extension](https://pypi.org/project/jupyter-ra-extension/). It adds
236
257
  the symbols mentioned above and some other supported symbols to the toolbar for
@@ -9,11 +9,12 @@ from typing import Optional, Dict, List, Tuple
9
9
 
10
10
  from ipykernel.kernelbase import Kernel
11
11
 
12
- from .db import Connection, DatabaseError
12
+ from .db import Connection, DatabaseError, Table
13
13
  from .db.error import *
14
14
  from .magics import *
15
15
  from .parser import RAParser, DCParser
16
16
  from .util.ResultSetComparator import ResultSetComparator
17
+ from .util.TestError import TestError
17
18
  from .util.formatting import row_count, rows_table, wrap_image
18
19
  from .visualization import *
19
20
 
@@ -139,6 +140,7 @@ class DuckDBKernel(Kernel):
139
140
  return False
140
141
 
141
142
  def _execute_stmt(self, query: str, silent: bool,
143
+ column_name_mapping: Dict[str, str],
142
144
  max_rows: Optional[int]) -> Tuple[Optional[List[str]], Optional[List[List]]]:
143
145
  if self._db is None:
144
146
  raise AssertionError('load a database first')
@@ -168,7 +170,8 @@ class DuckDBKernel(Kernel):
168
170
  else:
169
171
  if columns is not None:
170
172
  # table header
171
- table_header = ''.join(f'<th>{c}</th>' for c in columns)
173
+ mapped_columns = (column_name_mapping.get(c, c) for c in columns)
174
+ table_header = ''.join(f'<th>{c}</th>' for c in mapped_columns)
172
175
 
173
176
  # table data
174
177
  if max_rows is not None and len(rows) > max_rows:
@@ -302,12 +305,23 @@ class DuckDBKernel(Kernel):
302
305
  result_columns = [col.rsplit('.', 1)[-1] for col in result_columns]
303
306
 
304
307
  # extract data for test
305
- data = self._tests[name]
308
+ test_data = self._tests[name]
306
309
 
310
+ # execute test
311
+ try:
312
+ self._execute_test(test_data, result_columns, result)
313
+ self.print_data(wrap_image(True))
314
+ except TestError as e:
315
+ self.print_data(wrap_image(False, e.message))
316
+ if os.environ.get('DUCKDB_TESTS_RAISE_EXCEPTION', 'false').lower() in ('true', '1'):
317
+ raise e
318
+
319
+ @staticmethod
320
+ def _execute_test(test_data: Dict, result_columns: List[str], result: List[List]):
307
321
  # check columns if required
308
- if isinstance(data['equals'], dict):
322
+ if isinstance(test_data['equals'], dict):
309
323
  # get column order
310
- data_columns = list(data['equals'].keys())
324
+ data_columns = list(test_data['equals'].keys())
311
325
  column_order = []
312
326
 
313
327
  for dc in data_columns:
@@ -318,39 +332,37 @@ class DuckDBKernel(Kernel):
318
332
  found += 1
319
333
 
320
334
  if found == 0:
321
- return self.print_data(wrap_image(False, f'attribute {dc} missing'))
335
+ raise TestError(f'attribute {dc} missing')
322
336
  if found >= 2:
323
- return self.print_data(wrap_image(False, f'ambiguous attribute {dc}'))
337
+ raise TestError(f'ambiguous attribute {dc}')
324
338
 
325
339
  # abort if columns from result are unnecessary
326
340
  for i, rc in enumerate(result_columns):
327
341
  if i not in column_order:
328
- return self.print_data(wrap_image(False, f'unnecessary attribute {rc}'))
342
+ raise TestError(f'unnecessary attribute {rc}')
329
343
 
330
344
  # reorder columns and transform to list of lists
331
345
  sorted_columns = [x for _, x in sorted(zip(column_order, data_columns))]
332
346
  rows = []
333
347
 
334
- for row in zip(*(data['equals'][col] for col in sorted_columns)):
348
+ for row in zip(*(test_data['equals'][col] for col in sorted_columns)):
335
349
  rows.append(row)
336
350
 
337
351
  else:
338
- rows = data['equals']
352
+ rows = test_data['equals']
339
353
 
340
354
  # ordered test
341
- if data['ordered']:
355
+ if test_data['ordered']:
342
356
  # calculate diff
343
357
  rsc = ResultSetComparator(result, rows)
344
358
 
345
359
  missing = len(rsc.ordered_right_only)
346
360
  if missing > 0:
347
- return self.print_data(wrap_image(False, f'{row_count(missing)} missing'))
361
+ raise TestError(f'{row_count(missing)} missing')
348
362
 
349
363
  missing = len(rsc.ordered_left_only)
350
364
  if missing > 0:
351
- return self.print_data(wrap_image(False, f'{row_count(missing)} more than required'))
352
-
353
- return self.print_data(wrap_image(True))
365
+ raise TestError(f'{row_count(missing)} more than required')
354
366
 
355
367
  # unordered test
356
368
  else:
@@ -362,13 +374,11 @@ class DuckDBKernel(Kernel):
362
374
 
363
375
  # print result
364
376
  if below > 0 and above > 0:
365
- self.print_data(wrap_image(False, f'{row_count(below)} missing, {row_count(above)} unnecessary'))
377
+ raise TestError(f'{row_count(below)} missing, {row_count(above)} unnecessary')
366
378
  elif below > 0:
367
- self.print_data(wrap_image(False, f'{row_count(below)} missing'))
379
+ raise TestError(f'{row_count(below)} missing')
368
380
  elif above > 0:
369
- self.print_data(wrap_image(False, f'{row_count(above)} unnecessary'))
370
- else:
371
- self.print_data(wrap_image(True))
381
+ raise TestError(f'{row_count(above)} unnecessary')
372
382
 
373
383
  def _all_magic(self, silent: bool):
374
384
  return {
@@ -404,7 +414,7 @@ class DuckDBKernel(Kernel):
404
414
  whitelist = set()
405
415
 
406
416
  # split and strip names
407
- names = [n.strip() for n in re.split(r'[, \t]', only)]
417
+ names = [Table.normalize_name(n.strip()) for n in re.split(r'[, \t]', only)]
408
418
 
409
419
  # add initial tables to result set
410
420
  for name in names:
@@ -503,10 +513,11 @@ class DuckDBKernel(Kernel):
503
513
  root_node = DCParser.parse_query(code)
504
514
 
505
515
  # generate sql
506
- sql = root_node.to_sql(tables)
516
+ sql, cnm = root_node.to_sql_with_renamed_columns(tables)
507
517
 
508
518
  return {
509
- 'generated_code': sql
519
+ 'generated_code': sql,
520
+ 'column_name_mapping': cnm
510
521
  }
511
522
 
512
523
  # jupyter related functions
@@ -530,6 +541,10 @@ class DuckDBKernel(Kernel):
530
541
  clean_code = execution_args['generated_code']
531
542
  del execution_args['generated_code']
532
543
 
544
+ # set default column name mapping if none provided
545
+ if 'column_name_mapping' not in execution_args:
546
+ execution_args['column_name_mapping'] = {}
547
+
533
548
  # execute statement if needed
534
549
  if clean_code.strip():
535
550
  cols, rows = self._execute_stmt(clean_code, silent, **execution_args)
@@ -42,7 +42,7 @@ class ConditionalSet:
42
42
 
43
43
  # If a constant was found, we store the value and replace it with a random attribute name.
44
44
  constant = le.names[i]
45
- new_token = Token.random()
45
+ new_token = Token.random(constant)
46
46
  new_operand = DCOperand(le.relation, le.names[:i] + (new_token,) + le.names[i + 1:], skip_comma=True)
47
47
 
48
48
  # We now need an equality comparison to ensure the introduced attribute is equal to the constant.
@@ -103,7 +103,7 @@ class ConditionalSet:
103
103
  # The default case is to return the LogicElement with not DCOperands.
104
104
  return le, []
105
105
 
106
- def to_sql(self, tables: Dict[str, Table]) -> str:
106
+ def to_sql_with_renamed_columns(self, tables: Dict[str, Table]) -> Tuple[str, Dict[str, str]]:
107
107
  # First we have to find and remove all DCOperands from the operator tree.
108
108
  condition, dc_operands = self.split_tree(self.condition)
109
109
 
@@ -339,5 +339,18 @@ class ConditionalSet:
339
339
  sql_join_filters += f' AND {join_filter}'
340
340
 
341
341
  sql_condition = condition.to_sql(joined_columns) if condition is not None else '1=1'
342
+ sql_query = f'SELECT DISTINCT {sql_select} FROM {sql_tables} WHERE ({sql_join_filters}) AND ({sql_condition})'
343
+
344
+ # Create a mapping from intermediate column names to constant values.
345
+ column_name_mapping = {
346
+ p: p.constant
347
+ for o in dc_operands
348
+ for p in o.names
349
+ if p.constant is not None
350
+ }
342
351
 
343
- return f'SELECT DISTINCT {sql_select} FROM {sql_tables} WHERE ({sql_join_filters}) AND ({sql_condition})'
352
+ return sql_query, column_name_mapping
353
+
354
+ def to_sql(self, tables: Dict[str, Table]) -> str:
355
+ sql, _ = self.to_sql_with_renamed_columns(tables)
356
+ return sql
@@ -1,8 +1,9 @@
1
+ from typing import Optional
1
2
  from uuid import uuid4
2
3
 
3
4
 
4
5
  class Token(str):
5
- def __new__(cls, value: str):
6
+ def __new__(cls, value: str, constant: 'Token' = None):
6
7
  while True:
7
8
  # strip whitespaces
8
9
  value = value.strip()
@@ -38,20 +39,40 @@ class Token(str):
38
39
 
39
40
  return super().__new__(cls, value)
40
41
 
42
+ def __init__(self, value: str, constant: 'Token' = None):
43
+ self.constant: Optional[Token] = constant
44
+
41
45
  @staticmethod
42
- def random() -> 'Token':
43
- return Token('__' + str(uuid4()).replace('-', '_'))
46
+ def random(constant: 'Token' = None) -> 'Token':
47
+ return Token('__' + str(uuid4()).replace('-', '_'), constant)
44
48
 
45
49
  @property
46
50
  def empty(self) -> bool:
47
51
  return len(self) == 0
48
52
 
53
+ @property
54
+ def is_temporary(self) -> bool:
55
+ return self.startswith('__')
56
+
49
57
  @property
50
58
  def is_constant(self) -> bool:
51
59
  return ((self[0] == '"' and self[-1] == '"') or
52
60
  (self[0] == "'" and self[-1] == "'") or
53
61
  self.replace('.', '', 1).isnumeric())
54
62
 
63
+ @property
64
+ def no_quotes(self) -> str:
65
+ quotes = ('"', "'")
66
+
67
+ if self[0] in quotes and self[-1] in quotes:
68
+ return self[1:-1]
69
+ if self[0] in quotes:
70
+ return self[1:]
71
+ if self[-1] in quotes:
72
+ return self[:-1]
73
+ else:
74
+ return self
75
+
55
76
  @property
56
77
  def single_quotes(self) -> str:
57
78
  # TODO Is this comparison useless because tokens are cleaned automatically?
@@ -0,0 +1,4 @@
1
+ class TestError(Exception):
2
+ @property
3
+ def message(self) -> str:
4
+ return str(self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: jupyter-duckdb
3
- Version: 1.2.0.2
3
+ Version: 1.2.0.4
4
4
  Summary: a basic wrapper kernel for DuckDB
5
5
  Home-page: https://github.com/erictroebs/jupyter-duckdb
6
6
  Author: Eric Tröbs
@@ -32,10 +32,6 @@ This is a simple DuckDB wrapper kernel which accepts SQL as input, executes it
32
32
  using a previously loaded DuckDB instance and formats the output as a table.
33
33
  There are some magic commands that make teaching easier with this kernel.
34
34
 
35
- ## Quick Start
36
-
37
- [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/git/https%3A%2F%2Fdbgit.prakinf.tu-ilmenau.de%2Fertr8623%2Fjupyter-duckdb.git/master)
38
-
39
35
  ## Table of Contents
40
36
 
41
37
  - [Setup](#setup)
@@ -85,6 +81,12 @@ Execute the following command to pull and run a prepared image.
85
81
  docker run -p 8888:8888 troebs/jupyter-duckdb
86
82
  ```
87
83
 
84
+ There is also a second image. It contains an additional instance of PostgreSQL:
85
+
86
+ ```bash
87
+ docker run -p 8888:8888 troebs/jupyter-duckdb:postgresql
88
+ ```
89
+
88
90
  This image can also be used with JupyterHub and the
89
91
  [DockerSpawner / SwarmSpawner](https://github.com/jupyterhub/dockerspawner)
90
92
  and probably with the
@@ -138,6 +140,13 @@ Please note that `:memory:` is also a valid file path for DuckDB. The data is
138
140
  then stored exclusively in the main memory. In combination with `CREATE`
139
141
  and `OF` this makes it possible to work on a temporary copy in memory.
140
142
 
143
+ Although the name suggests otherwise, the kernel can also be used with other
144
+ databases:
145
+ - **SQLite** is automatically used as a fallback if the DuckDB dependency is
146
+ missing.
147
+ - To connect to a **PostgreSQL** instance, you need to specify a database URI
148
+ starting with `(postgresql|postgres|pgsql|psql|pg)://`.
149
+
141
150
  ### Schema Diagrams
142
151
 
143
152
  The magic command `SCHEMA` can be used to create a simple schema diagram of the
@@ -153,6 +162,10 @@ representation requires more space, but can improve readability.
153
162
  %SCHEMA TD
154
163
  ```
155
164
 
165
+ The optional argument `ONLY`, followed by one or more table names separated by a
166
+ comma, can be used to display only the named tables and all those connected with
167
+ a foreign key.
168
+
156
169
  Graphviz (`dot` in PATH) is required to render schema diagrams.
157
170
 
158
171
  ### Number of Rows
@@ -234,6 +247,11 @@ UNION
234
247
  SELECT 1, 'Name 1'
235
248
  ```
236
249
 
250
+ By default, failed tests will display an explanation, but the notebook will
251
+ continue to run. Set the `DUCKDB_TESTS_RAISE_EXCEPTION` environment variable to
252
+ `true` to raise an exception when a test fails. This can be useful for automated
253
+ testing in CI environments.
254
+
237
255
  Disclaimer: The integrated testing is work-in-progress and thus subject to
238
256
  potentially incompatible changes and enhancements.
239
257
 
@@ -259,6 +277,9 @@ The supported operations are:
259
277
  - Cross Product `×`
260
278
  - Division `÷`
261
279
 
280
+ The optional flag `ANALYZE` can be used to add an execution diagram to the
281
+ output.
282
+
262
283
  The Dockerfile also installs the Jupyter Lab plugin
263
284
  [jupyter-ra-extension](https://pypi.org/project/jupyter-ra-extension/). It adds
264
285
  the symbols mentioned above and some other supported symbols to the toolbar for
@@ -72,6 +72,7 @@ src/duckdb_kernel/parser/util/RenamableColumn.py
72
72
  src/duckdb_kernel/parser/util/RenamableColumnList.py
73
73
  src/duckdb_kernel/parser/util/__init__.py
74
74
  src/duckdb_kernel/util/ResultSetComparator.py
75
+ src/duckdb_kernel/util/TestError.py
75
76
  src/duckdb_kernel/util/__init__.py
76
77
  src/duckdb_kernel/util/formatting.py
77
78
  src/duckdb_kernel/visualization/Drawer.py
@@ -7,7 +7,23 @@ def test_case_insensitivity():
7
7
  '{ username | users(id, username) }',
8
8
  '{ username | Users(id, username) }',
9
9
  '{ username | USERS(id, username) }',
10
- '{ username | uSers(id, username) }'
10
+ '{ username | uSers(id, username) }',
11
+ ):
12
+ root = DCParser.parse_query(query)
13
+
14
+ # execute to test case insensitivity
15
+ with Connection() as con:
16
+ assert con.execute_dc(root) == [
17
+ ('Alice',),
18
+ ('Bob',),
19
+ ('Charlie',)
20
+ ]
21
+
22
+ for query in (
23
+ '{ username | users(id, username) }',
24
+ '{ Username | users(id, username) }',
25
+ '{ USERNAME | users(id, username) }',
26
+ '{ userName | users(id, username) }',
11
27
  ):
12
28
  root = DCParser.parse_query(query)
13
29
 
@@ -79,7 +95,8 @@ def test_conditions():
79
95
  ]
80
96
 
81
97
  for query in [
82
- '{ id | Users(id, name) ∧ name > "B" ∧ name < "C" }'
98
+ '{ id | Users(id, name) ∧ name = "Bob" }',
99
+ '{ id | Users(id, name) ∧ name > "B" ∧ name < "C" }',
83
100
  ]:
84
101
  root = DCParser.parse_query(query)
85
102
  assert con.execute_dc(root) == [
@@ -189,6 +206,33 @@ def test_joins():
189
206
  ]
190
207
 
191
208
 
209
+ def test_disjunction_joins():
210
+ with Connection() as con:
211
+ for query in [
212
+ "{ enum, snum, sid | Episodes(enum, snum, sid, ename) ∧ (Characters('Character B', enum, snum, sid, _) ∨ Characters('Character D', enum, snum, sid, _)) }",
213
+ "{ enum, snum, sid | Episodes(enum, snum, sid, ename) ∧ (Characters(cname1, enum, snum, sid, _) ∧ cname1 = 'Character B' ∨ Characters(cname2, enum, snum, sid, _) ∧ cname2 = 'Character D') }",
214
+ ]:
215
+ root = DCParser.parse_query(query)
216
+ assert con.execute_dc(root) == [
217
+ (1, 1, 1),
218
+ (2, 1, 1)
219
+ ]
220
+
221
+
222
+ def test_cross_join():
223
+ with Connection() as con:
224
+ for query in [
225
+ "{ cname1, cname2 | Characters(cname1, _, _, 2, _) ∧ Characters(cname2, _, _, 2, _) }",
226
+ ]:
227
+ root = DCParser.parse_query(query)
228
+ assert con.execute_dc(root) == [
229
+ ('Character E', 'Character E'),
230
+ ('Character E', 'Character F'),
231
+ ('Character F', 'Character E'),
232
+ ('Character F', 'Character F'),
233
+ ]
234
+
235
+
192
236
  def test_underscores():
193
237
  with Connection() as con:
194
238
  # distinct underscores
@@ -239,3 +283,53 @@ def test_underscores():
239
283
  ('Show 2 / Season 2 / Episode 3',),
240
284
  ('Show 2 / Season 2 / Episode 4',)
241
285
  ]
286
+
287
+ def test_anonymous_column_names():
288
+ with Connection() as con:
289
+ for query in [
290
+ '{ * | Episodes(_, _, 2, ename) }',
291
+ ]:
292
+ root = DCParser.parse_query(query)
293
+ cols, _ = con.execute_dc_return_cols(root)
294
+
295
+ assert cols == ['2', 'ename']
296
+
297
+ for query in [
298
+ "{ * | Episodes(_, _, '2', ename) }",
299
+ ]:
300
+ root = DCParser.parse_query(query)
301
+ cols, _ = con.execute_dc_return_cols(root)
302
+
303
+ assert cols == ["'2'", 'ename']
304
+
305
+ for query in [
306
+ "{ * | Episodes(_, _, sid, 'ename') }",
307
+ ]:
308
+ root = DCParser.parse_query(query)
309
+ cols, _ = con.execute_dc_return_cols(root)
310
+
311
+ assert cols == ['sid', "'ename'"]
312
+
313
+ for query in [
314
+ '{ * | Episodes(_, 1, 2, ename) }',
315
+ ]:
316
+ root = DCParser.parse_query(query)
317
+ cols, _ = con.execute_dc_return_cols(root)
318
+
319
+ assert cols == ['1', '2', 'ename']
320
+
321
+ for query in [
322
+ '{ * | Episodes(_, 2, 2, ename) }',
323
+ ]:
324
+ root = DCParser.parse_query(query)
325
+ cols, _ = con.execute_dc_return_cols(root)
326
+
327
+ assert cols == ['2', '2', 'ename']
328
+
329
+ for query in [
330
+ '{ * | Episodes(_, _, sid, ename) ∧ sid = 2 }',
331
+ ]:
332
+ root = DCParser.parse_query(query)
333
+ cols, _ = con.execute_dc_return_cols(root)
334
+
335
+ assert cols == ['sid', 'ename']