jupyter-duckdb 1.2.0.3__tar.gz → 1.2.0.5__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 (93) hide show
  1. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/PKG-INFO +37 -6
  2. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/README.md +36 -5
  3. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/kernel.py +81 -24
  4. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/magics/MagicCommand.py +34 -10
  5. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/magics/MagicCommandCallback.py +14 -3
  6. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/magics/MagicCommandHandler.py +47 -6
  7. jupyter_duckdb-1.2.0.5/src/duckdb_kernel/magics/StringWrapper.py +3 -0
  8. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/DCParser.py +10 -7
  9. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/LogicParser.py +6 -6
  10. jupyter_duckdb-1.2.0.5/src/duckdb_kernel/parser/ParserError.py +18 -0
  11. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/RAParser.py +12 -14
  12. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/__init__.py +1 -0
  13. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/DCOperand.py +7 -4
  14. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/ConditionalSet.py +30 -9
  15. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/tokenizer/Token.py +24 -3
  16. jupyter_duckdb-1.2.0.5/src/duckdb_kernel/util/TestError.py +4 -0
  17. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/jupyter_duckdb.egg-info/PKG-INFO +37 -6
  18. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/jupyter_duckdb.egg-info/SOURCES.txt +3 -0
  19. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/test/test_dc.py +96 -2
  20. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/test/test_ra.py +2 -2
  21. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/setup.cfg +0 -0
  22. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/setup.py +0 -0
  23. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/__init__.py +0 -0
  24. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/__main__.py +0 -0
  25. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/Column.py +0 -0
  26. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/Connection.py +0 -0
  27. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/Constraint.py +0 -0
  28. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/DatabaseError.py +0 -0
  29. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/ForeignKey.py +0 -0
  30. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/Table.py +0 -0
  31. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/__init__.py +0 -0
  32. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/error/EmptyResultError.py +0 -0
  33. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/error/__init__.py +0 -0
  34. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/implementation/duckdb/Connection.py +0 -0
  35. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/implementation/duckdb/__init__.py +0 -0
  36. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/implementation/postgres/Connection.py +0 -0
  37. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/implementation/postgres/__init__.py +0 -0
  38. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/implementation/postgres/util.py +0 -0
  39. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/implementation/sqlite/Connection.py +0 -0
  40. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/db/implementation/sqlite/__init__.py +0 -0
  41. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/kernel.json +0 -0
  42. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/magics/MagicCommandException.py +0 -0
  43. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/magics/__init__.py +0 -0
  44. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/LogicElement.py +0 -0
  45. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/LogicOperand.py +0 -0
  46. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/LogicOperator.py +0 -0
  47. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/RABinaryOperator.py +0 -0
  48. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/RAElement.py +0 -0
  49. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/RAOperand.py +0 -0
  50. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/RAOperator.py +0 -0
  51. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/RAUnaryOperator.py +0 -0
  52. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/__init__.py +0 -0
  53. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Add.py +0 -0
  54. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/And.py +0 -0
  55. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/ArrowLeft.py +0 -0
  56. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Cross.py +0 -0
  57. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Difference.py +0 -0
  58. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Divide.py +0 -0
  59. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Division.py +0 -0
  60. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Equal.py +0 -0
  61. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/GreaterThan.py +0 -0
  62. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/GreaterThanEqual.py +0 -0
  63. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Intersection.py +0 -0
  64. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Join.py +0 -0
  65. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/LessThan.py +0 -0
  66. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/LessThanEqual.py +0 -0
  67. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Minus.py +0 -0
  68. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Multiply.py +0 -0
  69. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Or.py +0 -0
  70. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Unequal.py +0 -0
  71. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/Union.py +0 -0
  72. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/binary/__init__.py +0 -0
  73. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/unary/Not.py +0 -0
  74. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/unary/Projection.py +0 -0
  75. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/unary/Rename.py +0 -0
  76. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/unary/Selection.py +0 -0
  77. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/elements/unary/__init__.py +0 -0
  78. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/tokenizer/Tokenizer.py +0 -0
  79. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/tokenizer/__init__.py +0 -0
  80. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/util/RenamableColumn.py +0 -0
  81. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/util/RenamableColumnList.py +0 -0
  82. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/parser/util/__init__.py +0 -0
  83. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/util/ResultSetComparator.py +0 -0
  84. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/util/__init__.py +0 -0
  85. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/util/formatting.py +0 -0
  86. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/visualization/Drawer.py +0 -0
  87. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/visualization/RATreeDrawer.py +0 -0
  88. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/visualization/SchemaDrawer.py +0 -0
  89. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/duckdb_kernel/visualization/__init__.py +0 -0
  90. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/jupyter_duckdb.egg-info/dependency_links.txt +0 -0
  91. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/jupyter_duckdb.egg-info/requires.txt +0 -0
  92. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/src/jupyter_duckdb.egg-info/top_level.txt +0 -0
  93. {jupyter_duckdb-1.2.0.3 → jupyter_duckdb-1.2.0.5}/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.3
3
+ Version: 1.2.0.5
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
 
@@ -244,7 +262,7 @@ magic command `RA` activates the relational algebra mode for a single cell:
244
262
 
245
263
  ```
246
264
  %RA
247
- π a, b (σ c = 1 (R))
265
+ π [a, b][c = 1] (R))
248
266
  ```
249
267
 
250
268
  The supported operations are:
@@ -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
@@ -273,3 +294,13 @@ magic command `DC` activates the domain calculus mode for a single cell:
273
294
  %DC
274
295
  { a, b | R(a, b, c) ∧ c = 1 }
275
296
  ```
297
+
298
+ ### Automated Parser Selection
299
+
300
+ `%ALL_RA` or `%ALL_DC` enables the corresponding parser for all subsequently
301
+ executed cells.
302
+
303
+ If the magic command `%AUTO_PARSER` is added to a cell, a parser is
304
+ automatically selected. If `%GUESS_PARSER` is executed, the parser is
305
+ automatically selected for all subsequent cells.
306
+
@@ -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
 
@@ -216,7 +234,7 @@ magic command `RA` activates the relational algebra mode for a single cell:
216
234
 
217
235
  ```
218
236
  %RA
219
- π a, b (σ c = 1 (R))
237
+ π [a, b][c = 1] (R))
220
238
  ```
221
239
 
222
240
  The supported operations are:
@@ -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
@@ -245,3 +266,13 @@ magic command `DC` activates the domain calculus mode for a single cell:
245
266
  %DC
246
267
  { a, b | R(a, b, c) ∧ c = 1 }
247
268
  ```
269
+
270
+ ### Automated Parser Selection
271
+
272
+ `%ALL_RA` or `%ALL_DC` enables the corresponding parser for all subsequently
273
+ executed cells.
274
+
275
+ If the magic command `%AUTO_PARSER` is added to a cell, a parser is
276
+ automatically selected. If `%GUESS_PARSER` is executed, the parser is
277
+ automatically selected for all subsequent cells.
278
+
@@ -12,8 +12,9 @@ from ipykernel.kernelbase import Kernel
12
12
  from .db import Connection, DatabaseError, Table
13
13
  from .db.error import *
14
14
  from .magics import *
15
- from .parser import RAParser, DCParser
15
+ from .parser import RAParser, DCParser, ParserError
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
 
@@ -45,8 +46,12 @@ class DuckDBKernel(Kernel):
45
46
  MagicCommand('query_max_rows').arg('count').on(self._query_max_rows_magic),
46
47
  MagicCommand('schema').flag('td').opt('only').on(self._schema_magic),
47
48
  MagicCommand('store').arg('file').flag('noheader').result(True).on(self._store_magic),
48
- MagicCommand('ra').flag('analyze').code(True).on(self._ra_magic),
49
- MagicCommand('dc').code(True).on(self._dc_magic)
49
+ MagicCommand('ra').disable('dc', 'auto_parser').flag('analyze').code(True).on(self._ra_magic),
50
+ MagicCommand('all_ra').arg('value', '1').on(self._all_ra_magic),
51
+ MagicCommand('dc').disable('ra', 'auto_parser').code(True).on(self._dc_magic),
52
+ MagicCommand('all_dc').arg('value', '1').on(self._all_dc_magic),
53
+ MagicCommand('auto_parser').disable('ra', 'dc').code(True).on(self._auto_parser_magic),
54
+ MagicCommand('guess_parser').arg('value', '1').on(self._guess_parser_magic),
50
55
  )
51
56
 
52
57
  # create placeholders for database and tests
@@ -139,6 +144,7 @@ class DuckDBKernel(Kernel):
139
144
  return False
140
145
 
141
146
  def _execute_stmt(self, query: str, silent: bool,
147
+ column_name_mapping: Dict[str, str],
142
148
  max_rows: Optional[int]) -> Tuple[Optional[List[str]], Optional[List[List]]]:
143
149
  if self._db is None:
144
150
  raise AssertionError('load a database first')
@@ -168,7 +174,8 @@ class DuckDBKernel(Kernel):
168
174
  else:
169
175
  if columns is not None:
170
176
  # table header
171
- table_header = ''.join(f'<th>{c}</th>' for c in columns)
177
+ mapped_columns = (column_name_mapping.get(c, c) for c in columns)
178
+ table_header = ''.join(f'<th>{c}</th>' for c in mapped_columns)
172
179
 
173
180
  # table data
174
181
  if max_rows is not None and len(rows) > max_rows:
@@ -302,12 +309,23 @@ class DuckDBKernel(Kernel):
302
309
  result_columns = [col.rsplit('.', 1)[-1] for col in result_columns]
303
310
 
304
311
  # extract data for test
305
- data = self._tests[name]
312
+ test_data = self._tests[name]
306
313
 
314
+ # execute test
315
+ try:
316
+ self._execute_test(test_data, result_columns, result)
317
+ self.print_data(wrap_image(True))
318
+ except TestError as e:
319
+ self.print_data(wrap_image(False, e.message))
320
+ if os.environ.get('DUCKDB_TESTS_RAISE_EXCEPTION', 'false').lower() in ('true', '1'):
321
+ raise e
322
+
323
+ @staticmethod
324
+ def _execute_test(test_data: Dict, result_columns: List[str], result: List[List]):
307
325
  # check columns if required
308
- if isinstance(data['equals'], dict):
326
+ if isinstance(test_data['equals'], dict):
309
327
  # get column order
310
- data_columns = list(data['equals'].keys())
328
+ data_columns = list(test_data['equals'].keys())
311
329
  column_order = []
312
330
 
313
331
  for dc in data_columns:
@@ -318,39 +336,37 @@ class DuckDBKernel(Kernel):
318
336
  found += 1
319
337
 
320
338
  if found == 0:
321
- return self.print_data(wrap_image(False, f'attribute {dc} missing'))
339
+ raise TestError(f'attribute {dc} missing')
322
340
  if found >= 2:
323
- return self.print_data(wrap_image(False, f'ambiguous attribute {dc}'))
341
+ raise TestError(f'ambiguous attribute {dc}')
324
342
 
325
343
  # abort if columns from result are unnecessary
326
344
  for i, rc in enumerate(result_columns):
327
345
  if i not in column_order:
328
- return self.print_data(wrap_image(False, f'unnecessary attribute {rc}'))
346
+ raise TestError(f'unnecessary attribute {rc}')
329
347
 
330
348
  # reorder columns and transform to list of lists
331
349
  sorted_columns = [x for _, x in sorted(zip(column_order, data_columns))]
332
350
  rows = []
333
351
 
334
- for row in zip(*(data['equals'][col] for col in sorted_columns)):
352
+ for row in zip(*(test_data['equals'][col] for col in sorted_columns)):
335
353
  rows.append(row)
336
354
 
337
355
  else:
338
- rows = data['equals']
356
+ rows = test_data['equals']
339
357
 
340
358
  # ordered test
341
- if data['ordered']:
359
+ if test_data['ordered']:
342
360
  # calculate diff
343
361
  rsc = ResultSetComparator(result, rows)
344
362
 
345
363
  missing = len(rsc.ordered_right_only)
346
364
  if missing > 0:
347
- return self.print_data(wrap_image(False, f'{row_count(missing)} missing'))
365
+ raise TestError(f'{row_count(missing)} missing')
348
366
 
349
367
  missing = len(rsc.ordered_left_only)
350
368
  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))
369
+ raise TestError(f'{row_count(missing)} more than required')
354
370
 
355
371
  # unordered test
356
372
  else:
@@ -362,13 +378,11 @@ class DuckDBKernel(Kernel):
362
378
 
363
379
  # print result
364
380
  if below > 0 and above > 0:
365
- self.print_data(wrap_image(False, f'{row_count(below)} missing, {row_count(above)} unnecessary'))
381
+ raise TestError(f'{row_count(below)} missing, {row_count(above)} unnecessary')
366
382
  elif below > 0:
367
- self.print_data(wrap_image(False, f'{row_count(below)} missing'))
383
+ raise TestError(f'{row_count(below)} missing')
368
384
  elif above > 0:
369
- self.print_data(wrap_image(False, f'{row_count(above)} unnecessary'))
370
- else:
371
- self.print_data(wrap_image(True))
385
+ raise TestError(f'{row_count(above)} unnecessary')
372
386
 
373
387
  def _all_magic(self, silent: bool):
374
388
  return {
@@ -486,6 +500,15 @@ class DuckDBKernel(Kernel):
486
500
  'generated_code': sql
487
501
  }
488
502
 
503
+ def _all_ra_magic(self, silent: bool, value: str):
504
+ if value.lower() in ('1', 'on', 'true'):
505
+ self._magics['ra'].default(True)
506
+ self._magics['dc'].default(False)
507
+
508
+ self.print('All further cells are interpreted as %RA.\n')
509
+ else:
510
+ self._magics['ra'].default(False)
511
+
489
512
  def _dc_magic(self, silent: bool, code: str):
490
513
  if self._db is None:
491
514
  raise AssertionError('load a database first')
@@ -503,12 +526,42 @@ class DuckDBKernel(Kernel):
503
526
  root_node = DCParser.parse_query(code)
504
527
 
505
528
  # generate sql
506
- sql = root_node.to_sql(tables)
529
+ sql, cnm = root_node.to_sql_with_renamed_columns(tables)
507
530
 
508
531
  return {
509
- 'generated_code': sql
532
+ 'generated_code': sql,
533
+ 'column_name_mapping': cnm
510
534
  }
511
535
 
536
+ def _all_dc_magic(self, silent: bool, value: str):
537
+ if value.lower() in ('1', 'on', 'true'):
538
+ self._magics['dc'].default(True)
539
+ self._magics['ra'].default(False)
540
+
541
+ self.print('All further cells are interpreted as %DC.\n')
542
+ else:
543
+ self._magics['dc'].default(False)
544
+
545
+ def _guess_parser_magic(self, silent: bool, value: str):
546
+ if value.lower() in ('1', 'on', 'true'):
547
+ self._magics['auto_parser'].default(True)
548
+ self.print('The correct parser is guessed for each subsequently executed cell.\n')
549
+ else:
550
+ self._magics['auto_parser'].default(False)
551
+
552
+ def _auto_parser_magic(self, silent: bool, code: str):
553
+ try:
554
+ return self._ra_magic(silent, code, analyze=False)
555
+ except ParserError as e:
556
+ if e.depth > 0:
557
+ raise e
558
+
559
+ try:
560
+ return self._dc_magic(silent, code)
561
+ except ParserError as e:
562
+ if e.depth > 0:
563
+ raise e
564
+
512
565
  # jupyter related functions
513
566
  def do_execute(self, code: str, silent: bool,
514
567
  store_history: bool = True, user_expressions: dict = None, allow_stdin: bool = False,
@@ -530,6 +583,10 @@ class DuckDBKernel(Kernel):
530
583
  clean_code = execution_args['generated_code']
531
584
  del execution_args['generated_code']
532
585
 
586
+ # set default column name mapping if none provided
587
+ if 'column_name_mapping' not in execution_args:
588
+ execution_args['column_name_mapping'] = {}
589
+
533
590
  # execute statement if needed
534
591
  if clean_code.strip():
535
592
  cols, rows = self._execute_stmt(clean_code, silent, **execution_args)
@@ -1,27 +1,28 @@
1
- from typing import Any, List, Tuple, Callable, Dict
1
+ from typing import Any, List, Tuple, Callable, Dict, Set
2
2
 
3
3
 
4
4
  class MagicCommand:
5
- _ARG = '''([^ ]+?|'.+?'|".+?")'''
5
+ _ARG = '''([^ ]+?|'.+?'|".+?")?'''
6
6
 
7
7
  def __init__(self, *names: str):
8
- self._names: Tuple[str] = names
8
+ self._names: Tuple[str, ...] = names
9
9
 
10
- self._arguments: List[Tuple[str, str]] = []
10
+ self._arguments: List[Tuple[str, Any, str]] = []
11
11
  self._flags: List[Tuple[str, str]] = []
12
12
  self._optionals: List[Tuple[str, Any, str]] = []
13
-
13
+ self._disables: Set[str] = set()
14
14
  self._code: bool = False
15
15
  self._result: bool = False
16
+ self._default: bool = False
16
17
 
17
18
  self._on: List[Callable] = []
18
19
 
19
20
  @property
20
- def names(self) -> Tuple[str]:
21
+ def names(self) -> Tuple[str, ...]:
21
22
  return self._names
22
23
 
23
24
  @property
24
- def args(self) -> List[Tuple[str, str]]:
25
+ def args(self) -> List[Tuple[str, Any, str]]:
25
26
  return self._arguments
26
27
 
27
28
  @property
@@ -32,6 +33,10 @@ class MagicCommand:
32
33
  def optionals(self) -> List[Tuple[str, Any, str]]:
33
34
  return self._optionals
34
35
 
36
+ @property
37
+ def disables(self) -> Set[str]:
38
+ return self._disables
39
+
35
40
  @property
36
41
  def requires_code(self) -> bool:
37
42
  return self._code
@@ -40,8 +45,17 @@ class MagicCommand:
40
45
  def requires_query_result(self) -> bool:
41
46
  return self._result
42
47
 
43
- def arg(self, name: str, description: str = None) -> 'MagicCommand':
44
- self._arguments.append((name, description))
48
+ @property
49
+ def is_default(self) -> bool:
50
+ return self._default
51
+
52
+ def arg(self, name: str, default_value: Any = None, description: str = None) -> 'MagicCommand':
53
+ if len(self._arguments) > 0:
54
+ ln, ldv, _ = self._arguments[-1]
55
+ if ldv is not None and default_value is None:
56
+ raise ValueError(f'argument {name} without default value registered after argument {ln} with default value {ldv}')
57
+
58
+ self._arguments.append((name, default_value, description))
45
59
  return self
46
60
 
47
61
  def opt(self, name: str, default_value: Any = None, description: str = None) -> 'MagicCommand':
@@ -52,6 +66,12 @@ class MagicCommand:
52
66
  self._flags.append((name, description))
53
67
  return self
54
68
 
69
+ def disable(self, *name: str) -> 'MagicCommand':
70
+ for n in name:
71
+ self._disables.add(n)
72
+
73
+ return self
74
+
55
75
  def code(self, code: bool) -> 'MagicCommand':
56
76
  self._code = code
57
77
  return self
@@ -60,10 +80,14 @@ class MagicCommand:
60
80
  self._result = result
61
81
  return self
62
82
 
63
- def on(self, fun: Callable):
83
+ def on(self, fun: Callable) -> 'MagicCommand':
64
84
  self._on.append(fun)
65
85
  return self
66
86
 
87
+ def default(self, default: bool) -> 'MagicCommand':
88
+ self._default = default
89
+ return self
90
+
67
91
  @property
68
92
  def parameters(self) -> str:
69
93
  args = ' +'.join([self._ARG] * len(self._arguments))
@@ -1,20 +1,31 @@
1
1
  from typing import Optional, List
2
2
 
3
3
  from . import MagicCommand
4
+ from .StringWrapper import StringWrapper
4
5
 
5
6
 
6
7
  class MagicCommandCallback:
7
- def __init__(self, mc: MagicCommand, silent: bool, code: str, *args, **kwargs):
8
+ def __init__(self, mc: MagicCommand, silent: bool, code: StringWrapper, *args, **kwargs):
8
9
  self._mc: MagicCommand = mc
9
10
  self._silent: bool = silent
10
- self._code: str = code
11
+ self._code: StringWrapper = code
11
12
  self._args = args
12
13
  self._kwargs = kwargs
13
14
 
15
+ @property
16
+ def magic(self) -> MagicCommand:
17
+ return self._mc
18
+
14
19
  def __call__(self, columns: Optional[List[str]] = None, rows: Optional[List[List]] = None):
15
20
  if self._mc.requires_code:
16
- return self._mc(self._silent, self._code, *self._args, **self._kwargs)
21
+ result = self._mc(self._silent, self._code.value, *self._args, **self._kwargs)
22
+ if 'generated_code' in result:
23
+ self._code.value = result['generated_code']
24
+
25
+ return result
26
+
17
27
  if self._mc.requires_query_result:
18
28
  return self._mc(self._silent, columns, rows, *self._args, **self._kwargs)
29
+
19
30
  else:
20
31
  return self._mc(self._silent, *self._args, **self._kwargs)
@@ -1,7 +1,8 @@
1
1
  import re
2
- from typing import Dict, Tuple, List
2
+ from typing import Dict, Tuple, List, Optional
3
3
 
4
4
  from . import MagicCommand, MagicCommandException, MagicCommandCallback
5
+ from .StringWrapper import StringWrapper
5
6
 
6
7
 
7
8
  class MagicCommandHandler:
@@ -14,10 +15,23 @@ class MagicCommandHandler:
14
15
  key = key.lower()
15
16
  self._magics[key] = cmd
16
17
 
18
+ def __getitem__(self, key: str) -> MagicCommand:
19
+ return self._magics[key.lower()]
20
+
17
21
  def __call__(self, silent: bool, code: str) -> Tuple[str, List[MagicCommandCallback], List[MagicCommandCallback]]:
18
- pre_query_callbacks = []
19
- post_query_callbacks = []
22
+ code_wrapper = StringWrapper()
23
+ enabled_callbacks: List[MagicCommandCallback] = []
24
+
25
+ # enable commands with default==True
26
+ for magic in self._magics.values():
27
+ if magic.is_default:
28
+ flags = {name: False for name, _ in magic.flags}
29
+ optionals = {name: default for name, default, _ in magic.optionals}
30
+ callback = MagicCommandCallback(magic, silent, code_wrapper, **flags, **optionals)
20
31
 
32
+ enabled_callbacks.append(callback)
33
+
34
+ # search for magic commands in code
21
35
  while True:
22
36
  # ensure code starts with '%' or '%%' but not with '%%%'
23
37
  match = re.match(r'^%{1,2}([^% ]+?)([ \t]*$| .+?$)', code, re.MULTILINE | re.IGNORECASE)
@@ -45,7 +59,11 @@ class MagicCommandHandler:
45
59
  raise MagicCommandException(f'could not parse parameters for command "{command}"')
46
60
 
47
61
  # extract args
48
- args = [g for g, _ in zip(match.groups(), magic.args)]
62
+ args = [group if group is not None else default
63
+ for group, (_, default, _) in zip(match.groups(), magic.args)]
64
+
65
+ if any(arg is None for arg in args):
66
+ raise MagicCommandException(f'could not parse parameters for command "{command}"')
49
67
 
50
68
  i = len(args) + 1
51
69
 
@@ -73,12 +91,35 @@ class MagicCommandHandler:
73
91
  optionals[name.lower()] = value
74
92
 
75
93
  # add to callbacks
76
- callback = MagicCommandCallback(magic, silent, code, *args, **flags, **optionals)
94
+ callback = MagicCommandCallback(magic, silent, code_wrapper, *args, **flags, **optionals)
95
+ enabled_callbacks.append(callback)
96
+
97
+ # disable overwritten callbacks
98
+ callbacks = []
99
+ blacklist = set()
100
+
101
+ for callback in reversed(enabled_callbacks):
102
+ for name in callback.magic.names:
103
+ if name in blacklist:
104
+ break
105
+ else:
106
+ callbacks.append(callback)
107
+
108
+ for name in callback.magic.names:
109
+ blacklist.add(name)
110
+ for disable in callback.magic.disables:
111
+ blacklist.add(disable)
112
+
113
+ # prepare callback lists
114
+ pre_query_callbacks = []
115
+ post_query_callbacks = []
77
116
 
78
- if not magic.requires_query_result:
117
+ for callback in reversed(callbacks):
118
+ if not callback.magic.requires_query_result:
79
119
  pre_query_callbacks.append(callback)
80
120
  else:
81
121
  post_query_callbacks.append(callback)
82
122
 
83
123
  # return callbacks
124
+ code_wrapper.value = code
84
125
  return code, pre_query_callbacks, post_query_callbacks
@@ -0,0 +1,3 @@
1
+ class StringWrapper:
2
+ def __init__(self, value: str = None):
3
+ self.value: str = value
@@ -1,3 +1,4 @@
1
+ from .ParserError import DCParserError
1
2
  from .elements import *
2
3
  from .tokenizer import *
3
4
 
@@ -18,17 +19,17 @@ class DCParser:
18
19
  )
19
20
 
20
21
  # raise exception if query is not in the correct format
21
- raise AssertionError('The expression shall be of the format "{ x1, ..., xn | f(x1, ..., xn) }".')
22
+ raise DCParserError('The expression shall be of the format "{ x1, ..., xn | f(x1, ..., xn) }".', 0)
22
23
 
23
24
  @staticmethod
24
- def parse_projection(*tokens: Token) -> LogicOperand:
25
+ def parse_projection(*tokens: Token, depth: int = 0) -> LogicOperand:
25
26
  if len(tokens) == 1:
26
27
  tokens = tuple(Tokenizer.tokenize(tokens[0]))
27
28
 
28
29
  return LogicOperand(*tokens)
29
30
 
30
31
  @staticmethod
31
- def parse_condition(*tokens: Token) -> LogicElement:
32
+ def parse_condition(*tokens: Token, depth: int = 0) -> LogicElement:
32
33
  if len(tokens) == 1:
33
34
  tokens = tuple(Tokenizer.tokenize(tokens[0]))
34
35
 
@@ -40,8 +41,8 @@ class DCParser:
40
41
  # return the operator
41
42
  # with left part of tokens and right part of tokens
42
43
  return operator(
43
- DCParser.parse_condition(*tokens[:-i]),
44
- DCParser.parse_condition(*tokens[-i + 1:])
44
+ DCParser.parse_condition(*tokens[:-i], depth=depth + 1),
45
+ DCParser.parse_condition(*tokens[-i + 1:], depth=depth + 1)
45
46
  )
46
47
 
47
48
  # not
@@ -56,10 +57,12 @@ class DCParser:
56
57
  elif len(tokens) == 2:
57
58
  return DCOperand(
58
59
  tokens[0],
59
- tuple(Tokenizer.tokenize(tokens[1]))
60
+ tuple(Tokenizer.tokenize(tokens[1])),
61
+ depth=depth + 1
60
62
  )
61
63
  else:
62
64
  return DCOperand(
63
65
  tokens[0],
64
- tokens[1:]
66
+ tokens[1:],
67
+ depth=depth + 1
65
68
  )