jupyter-duckdb 1.2.101__py3-none-any.whl → 1.2.103__py3-none-any.whl
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.
- duckdb_kernel/db/Connection.py +3 -0
- duckdb_kernel/db/implementation/duckdb/Connection.py +3 -0
- duckdb_kernel/db/implementation/postgres/Connection.py +4 -0
- duckdb_kernel/db/implementation/sqlite/Connection.py +3 -0
- duckdb_kernel/kernel.py +229 -190
- duckdb_kernel/magics/MagicCommandCallback.py +7 -14
- duckdb_kernel/magics/MagicCommandHandler.py +9 -9
- duckdb_kernel/magics/MagicState.py +11 -0
- duckdb_kernel/magics/__init__.py +1 -0
- duckdb_kernel/visualization/Plotly.py +8 -18
- duckdb_kernel/visualization/RATreeDrawer.py +34 -2
- duckdb_kernel/visualization/lib/__init__.py +53 -0
- duckdb_kernel/visualization/lib/plotly-3.0.1.min.js +3879 -0
- duckdb_kernel/visualization/lib/ra.css +3 -0
- duckdb_kernel/visualization/lib/ra.js +55 -0
- {jupyter_duckdb-1.2.101.dist-info → jupyter_duckdb-1.2.103.dist-info}/METADATA +13 -12
- {jupyter_duckdb-1.2.101.dist-info → jupyter_duckdb-1.2.103.dist-info}/RECORD +19 -15
- {jupyter_duckdb-1.2.101.dist-info → jupyter_duckdb-1.2.103.dist-info}/WHEEL +1 -1
- duckdb_kernel/magics/StringWrapper.py +0 -3
- {jupyter_duckdb-1.2.101.dist-info → jupyter_duckdb-1.2.103.dist-info}/top_level.txt +0 -0
duckdb_kernel/kernel.py
CHANGED
|
@@ -39,8 +39,11 @@ class DuckDBKernel(Kernel):
|
|
|
39
39
|
self._magics: MagicCommandHandler = MagicCommandHandler()
|
|
40
40
|
|
|
41
41
|
self._magics.add(
|
|
42
|
-
MagicCommand('create').arg('database').opt('of').opt('
|
|
43
|
-
MagicCommand('load').arg('database').opt('
|
|
42
|
+
MagicCommand('create').arg('database').opt('of').opt('name').on(self._create_magic),
|
|
43
|
+
MagicCommand('load').arg('database').opt('name').on(self._load_magic),
|
|
44
|
+
MagicCommand('copy').arg('source').arg('target').on(self._copy_magic),
|
|
45
|
+
MagicCommand('use').arg('name').on(self._use_magic),
|
|
46
|
+
MagicCommand('load_tests').arg('tests').on(self._load_tests_magic),
|
|
44
47
|
MagicCommand('test').arg('name').result(True).on(self._test_magic),
|
|
45
48
|
MagicCommand('all', 'all_rows').on(self._all_magic),
|
|
46
49
|
MagicCommand('max_rows').arg('count').on(self._max_rows_magic),
|
|
@@ -59,8 +62,8 @@ class DuckDBKernel(Kernel):
|
|
|
59
62
|
)
|
|
60
63
|
|
|
61
64
|
# create placeholders for database and tests
|
|
62
|
-
self._db:
|
|
63
|
-
self._tests:
|
|
65
|
+
self._db: Dict[str, Connection] = {}
|
|
66
|
+
self._tests: Dict = {}
|
|
64
67
|
|
|
65
68
|
# output related functions
|
|
66
69
|
def print(self, text: str, name: str = 'stdout'):
|
|
@@ -97,131 +100,143 @@ class DuckDBKernel(Kernel):
|
|
|
97
100
|
})
|
|
98
101
|
|
|
99
102
|
# database related functions
|
|
100
|
-
def _load_database(self, path: str):
|
|
101
|
-
if self._db
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
self._db = DuckDB(path)
|
|
103
|
+
def _load_database(self, path: str, name: str) -> Connection:
|
|
104
|
+
if name in self._db:
|
|
105
|
+
raise ValueError(f'duplicate database name {name}')
|
|
106
|
+
|
|
107
|
+
# If the provided path looks like a postgres url,
|
|
108
|
+
# we want to use the postgres driver.
|
|
109
|
+
if path.startswith(('postgresql://', 'postgres://', 'pgsql://', 'psql://', 'pg://')):
|
|
110
|
+
# pull data from connection string
|
|
111
|
+
re_expr = r'(postgresql|postgres|pgsql|psql|pg)://((.*?)(:(.*?))?@)?(.*?)(:(\d+))?(/(.*))?'
|
|
112
|
+
match = re.fullmatch(re_expr, path)
|
|
113
|
+
|
|
114
|
+
host = match.group(6)
|
|
115
|
+
port = int(match.group(8)) if match.group(8) is not None else 5432
|
|
116
|
+
username = match.group(3)
|
|
117
|
+
password = match.group(5)
|
|
118
|
+
database_name = match.group(10)
|
|
119
|
+
|
|
120
|
+
# load and create instance
|
|
121
|
+
from .db.implementation.postgres import Connection as Postgres
|
|
122
|
+
self._db[name] = Postgres(host, port, username, password, database_name)
|
|
123
|
+
|
|
124
|
+
# Otherwise the provided path is used to create an
|
|
125
|
+
# in-process instance.
|
|
126
|
+
else:
|
|
127
|
+
# By default, we try to load DuckDB.
|
|
128
|
+
try:
|
|
129
|
+
from .db.implementation.duckdb import Connection as DuckDB
|
|
130
|
+
self._db[name] = DuckDB(path)
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
# If DuckDB is not installed or fails to load,
|
|
133
|
+
# we use SQLite instead.
|
|
134
|
+
except ImportError:
|
|
135
|
+
self.print('DuckDB is not available\n')
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
from .db.implementation.sqlite import Connection as SQLite
|
|
138
|
+
self._db[name] = SQLite(path)
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
return self._db[name]
|
|
141
|
+
|
|
142
|
+
def _unload_database(self, name: str):
|
|
143
|
+
if name in self._db:
|
|
144
|
+
self._db[name].close()
|
|
145
|
+
del self._db[name]
|
|
141
146
|
|
|
142
|
-
def _unload_database(self):
|
|
143
|
-
if self._db is not None:
|
|
144
|
-
self._db.close()
|
|
145
|
-
self._db = None
|
|
146
147
|
return True
|
|
147
148
|
else:
|
|
148
149
|
return False
|
|
149
150
|
|
|
150
|
-
def _execute_stmt(self,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if self._db is None:
|
|
151
|
+
def _execute_stmt(self, silent: bool, state: MagicState, name: str, query: str) \
|
|
152
|
+
-> Tuple[Optional[List[str]], Optional[List[List]]]:
|
|
153
|
+
if state.db is None:
|
|
154
154
|
raise AssertionError('load a database first')
|
|
155
155
|
|
|
156
156
|
# execute query and store start and end timestamp
|
|
157
157
|
st = time.time()
|
|
158
158
|
|
|
159
159
|
try:
|
|
160
|
-
columns, rows =
|
|
160
|
+
columns, rows = state.db.execute(query)
|
|
161
161
|
except EmptyResultError:
|
|
162
162
|
columns, rows = None, None
|
|
163
163
|
|
|
164
164
|
et = time.time()
|
|
165
165
|
|
|
166
|
-
#
|
|
167
|
-
if silent:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
self.print_data(f'<b>{ekey}</b><br><pre>{evalue}</pre>')
|
|
174
|
-
|
|
175
|
-
return None, None
|
|
176
|
-
|
|
177
|
-
# print every other query as a table
|
|
178
|
-
else:
|
|
179
|
-
if columns is not None:
|
|
180
|
-
# table header
|
|
181
|
-
mapped_columns = (column_name_mapping.get(c, c) for c in columns)
|
|
182
|
-
table_header = ''.join(f'<th>{c}</th>' for c in mapped_columns)
|
|
183
|
-
|
|
184
|
-
# table data
|
|
185
|
-
if max_rows is not None and len(rows) > max_rows:
|
|
186
|
-
table_data = f'''
|
|
187
|
-
{rows_table(rows[:math.ceil(max_rows / 2)])}
|
|
188
|
-
<tr>
|
|
189
|
-
<td colspan="{len(columns)}"
|
|
190
|
-
style="text-align: center"
|
|
191
|
-
title="{row_count(len(rows) - max_rows)} omitted">
|
|
192
|
-
...
|
|
193
|
-
</td>
|
|
194
|
-
</tr>
|
|
195
|
-
{rows_table(rows[-math.floor(max_rows // 2):])}
|
|
196
|
-
'''
|
|
166
|
+
# print result if not silent
|
|
167
|
+
if not silent:
|
|
168
|
+
# print EXPLAIN queries as raw text if using DuckDB
|
|
169
|
+
if query.strip().startswith('EXPLAIN') and state.db.plain_explain():
|
|
170
|
+
for ekey, evalue in rows:
|
|
171
|
+
html = f'<b>{ekey}</b><br><pre>{evalue}</pre>'
|
|
172
|
+
break
|
|
197
173
|
else:
|
|
198
|
-
|
|
174
|
+
html = ''
|
|
199
175
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
</
|
|
206
|
-
|
|
176
|
+
# print every other query as a table
|
|
177
|
+
else:
|
|
178
|
+
if columns is not None:
|
|
179
|
+
# table header
|
|
180
|
+
mapped_columns = (state.column_name_mapping.get(c, c) for c in columns)
|
|
181
|
+
table_header = ''.join(f'<th>{c}</th>' for c in mapped_columns)
|
|
182
|
+
|
|
183
|
+
# table data
|
|
184
|
+
if state.max_rows is not None and len(rows) > state.max_rows:
|
|
185
|
+
table_data = f'''
|
|
186
|
+
{rows_table(rows[:math.ceil(state.max_rows / 2)])}
|
|
187
|
+
<tr>
|
|
188
|
+
<td colspan="{len(columns)}"
|
|
189
|
+
style="text-align: center"
|
|
190
|
+
title="{row_count(len(rows) - state.max_rows)} omitted">
|
|
191
|
+
...
|
|
192
|
+
</td>
|
|
193
|
+
</tr>
|
|
194
|
+
{rows_table(rows[-math.floor(state.max_rows // 2):])}
|
|
195
|
+
'''
|
|
196
|
+
else:
|
|
197
|
+
table_data = rows_table(rows)
|
|
198
|
+
|
|
199
|
+
# send to client
|
|
200
|
+
html = (f'''
|
|
201
|
+
<table class="duckdb-query-result-table">
|
|
202
|
+
{table_header}
|
|
203
|
+
{table_data}
|
|
204
|
+
</table>
|
|
205
|
+
|
|
206
|
+
{row_count(len(rows))} in {et - st:.3f}s
|
|
207
|
+
''')
|
|
207
208
|
|
|
208
|
-
|
|
209
|
+
else:
|
|
210
|
+
html = f'statement executed without result in {et - st:.3f}s'
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
self.print_data(f'''
|
|
213
|
+
<div class="duckdb-query-result {name}">
|
|
214
|
+
{html}
|
|
215
|
+
</div>
|
|
216
|
+
''')
|
|
212
217
|
|
|
213
218
|
return columns, rows
|
|
214
219
|
|
|
215
220
|
# magic command related functions
|
|
216
|
-
def _create_magic(self, silent: bool,
|
|
217
|
-
|
|
221
|
+
def _create_magic(self, silent: bool, state: MagicState,
|
|
222
|
+
path: str, of: Optional[str], name: Optional[str]):
|
|
223
|
+
self._load(silent, state, path, True, of, name)
|
|
224
|
+
|
|
225
|
+
def _load_magic(self, silent: bool, state: MagicState,
|
|
226
|
+
path: str, name: Optional[str]):
|
|
227
|
+
self._load(silent, state, path, False, None, name)
|
|
228
|
+
|
|
229
|
+
def _load(self, silent: bool, state: MagicState,
|
|
230
|
+
path: str, create: bool, of: Optional[str], name: Optional[str]):
|
|
231
|
+
# use default name if non provided
|
|
232
|
+
if name is None:
|
|
233
|
+
name = 'default'
|
|
218
234
|
|
|
219
|
-
|
|
220
|
-
|
|
235
|
+
if not silent:
|
|
236
|
+
self.print(f'--- connection {name} ---\n')
|
|
221
237
|
|
|
222
|
-
def _load(self, silent: bool, path: str, create: bool, of: Optional[str], with_tests: Optional[str]):
|
|
223
238
|
# unload current database if necessary
|
|
224
|
-
if self._unload_database():
|
|
239
|
+
if self._unload_database(name):
|
|
225
240
|
if not silent:
|
|
226
241
|
self.print('unloaded database\n')
|
|
227
242
|
|
|
@@ -238,10 +253,10 @@ class DuckDBKernel(Kernel):
|
|
|
238
253
|
if create and os.path.exists(path):
|
|
239
254
|
os.remove(path)
|
|
240
255
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
256
|
+
state.db = self._load_database(path, name)
|
|
257
|
+
if not silent:
|
|
258
|
+
# self.print(f'loaded database "{path}"\n')
|
|
259
|
+
self.print(str(state.db) + '\n')
|
|
245
260
|
|
|
246
261
|
# copy data from source database
|
|
247
262
|
if of is not None:
|
|
@@ -257,18 +272,18 @@ class DuckDBKernel(Kernel):
|
|
|
257
272
|
content = file.read()
|
|
258
273
|
|
|
259
274
|
# You can only execute one statement at a time using SQLite.
|
|
260
|
-
if not
|
|
275
|
+
if not state.db.multiple_statements_per_query():
|
|
261
276
|
statements = re.split(r';\r?\n', content)
|
|
262
277
|
for statement in statements:
|
|
263
278
|
try:
|
|
264
|
-
|
|
279
|
+
state.db.execute(statement)
|
|
265
280
|
except EmptyResultError:
|
|
266
281
|
pass
|
|
267
282
|
|
|
268
283
|
# Other DBMS can execute multiple statements at a time.
|
|
269
284
|
else:
|
|
270
285
|
try:
|
|
271
|
-
|
|
286
|
+
state.db.execute(content)
|
|
272
287
|
except EmptyResultError:
|
|
273
288
|
pass
|
|
274
289
|
|
|
@@ -282,29 +297,51 @@ class DuckDBKernel(Kernel):
|
|
|
282
297
|
of_db.execute('SHOW TABLES')
|
|
283
298
|
for table, in of_db.fetchall():
|
|
284
299
|
transfer_df = of_db.query(f'SELECT * FROM {table}').to_df()
|
|
285
|
-
|
|
300
|
+
state.db.execute(f'CREATE TABLE {table} AS SELECT * FROM transfer_df')
|
|
286
301
|
|
|
287
302
|
if not silent:
|
|
288
303
|
self.print(f'transferred table {table}\n')
|
|
289
304
|
|
|
290
|
-
|
|
291
|
-
if
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
305
|
+
def _copy_magic(self, silent: bool, state: MagicState, source: str, target: str):
|
|
306
|
+
if source not in self._db:
|
|
307
|
+
raise ValueError(f'unknown connection {source}')
|
|
308
|
+
|
|
309
|
+
if not silent:
|
|
310
|
+
self.print(f'--- connection {target} ---\n')
|
|
311
|
+
|
|
312
|
+
# unload current database if necessary
|
|
313
|
+
if self._unload_database(target):
|
|
314
|
+
if not silent:
|
|
315
|
+
self.print('unloaded database\n')
|
|
316
|
+
|
|
317
|
+
# copy connection
|
|
318
|
+
self._db[target] = self._db[source].copy()
|
|
319
|
+
state.db = self._db[target]
|
|
320
|
+
|
|
321
|
+
if not silent:
|
|
322
|
+
self.print(str(state.db) + '\n')
|
|
323
|
+
|
|
324
|
+
def _use_magic(self, silent: bool, state: MagicState, name: str):
|
|
325
|
+
if name not in self._db:
|
|
326
|
+
raise ValueError(f'unknown connection {name}')
|
|
302
327
|
|
|
303
|
-
|
|
328
|
+
state.db = self._db[name]
|
|
304
329
|
|
|
305
|
-
|
|
330
|
+
def _load_tests_magic(self, silent: bool, state: MagicState, tests: str):
|
|
331
|
+
with open(tests, 'r', encoding='utf-8') as tests_file:
|
|
332
|
+
self._tests = json.load(tests_file)
|
|
333
|
+
for test in self._tests.values():
|
|
334
|
+
if 'attributes' in test:
|
|
335
|
+
rows = {k: [] for k in test['attributes']}
|
|
336
|
+
for row in test['equals']:
|
|
337
|
+
for k, v in zip(test['attributes'], row):
|
|
338
|
+
rows[k].append(v)
|
|
306
339
|
|
|
307
|
-
|
|
340
|
+
test['equals'] = rows
|
|
341
|
+
|
|
342
|
+
self.print(f'loaded tests from {tests}\n')
|
|
343
|
+
|
|
344
|
+
def _test_magic(self, silent: bool, state: MagicState, result_columns: List[str], result: List[List], name: str):
|
|
308
345
|
# If the query was empty, result_columns and result may be None.
|
|
309
346
|
if result_columns is None or result is None:
|
|
310
347
|
self.print_data(wrap_image(False, 'Statement did not return data.'))
|
|
@@ -393,31 +430,29 @@ class DuckDBKernel(Kernel):
|
|
|
393
430
|
elif above > 0:
|
|
394
431
|
raise TestError(f'{row_count(above)} unnecessary')
|
|
395
432
|
|
|
396
|
-
def _all_magic(self, silent: bool):
|
|
397
|
-
|
|
398
|
-
'max_rows': None
|
|
399
|
-
}
|
|
433
|
+
def _all_magic(self, silent: bool, state: MagicState):
|
|
434
|
+
state.max_rows = None
|
|
400
435
|
|
|
401
|
-
def _max_rows_magic(self, silent: bool, count: str):
|
|
436
|
+
def _max_rows_magic(self, silent: bool, state: MagicState, count: str):
|
|
402
437
|
if count.lower() != 'none':
|
|
403
438
|
DuckDBKernel.DEFAULT_MAX_ROWS = int(count)
|
|
404
439
|
else:
|
|
405
440
|
DuckDBKernel.DEFAULT_MAX_ROWS = None
|
|
406
441
|
|
|
407
|
-
|
|
408
|
-
return {
|
|
409
|
-
'max_rows': int(count) if count.lower() != 'none' else None
|
|
410
|
-
}
|
|
442
|
+
state.max_rows = DuckDBKernel.DEFAULT_MAX_ROWS
|
|
411
443
|
|
|
412
|
-
def
|
|
413
|
-
if
|
|
414
|
-
raise AssertionError('load a database first')
|
|
444
|
+
def _query_max_rows_magic(self, silent: bool, state: MagicState, count: str):
|
|
445
|
+
state.max_rows = int(count) if count.lower() != 'none' else None
|
|
415
446
|
|
|
447
|
+
def _schema_magic(self, silent: bool, state: MagicState, td: bool, only: Optional[str]):
|
|
416
448
|
if silent:
|
|
417
449
|
return
|
|
418
450
|
|
|
451
|
+
if state.db is None:
|
|
452
|
+
raise AssertionError('load a database first')
|
|
453
|
+
|
|
419
454
|
# analyze tables
|
|
420
|
-
tables =
|
|
455
|
+
tables = state.db.analyze()
|
|
421
456
|
|
|
422
457
|
# apply filter
|
|
423
458
|
if only is None:
|
|
@@ -459,7 +494,9 @@ class DuckDBKernel(Kernel):
|
|
|
459
494
|
|
|
460
495
|
self.print_data(svg)
|
|
461
496
|
|
|
462
|
-
def _store_magic(self, silent: bool,
|
|
497
|
+
def _store_magic(self, silent: bool, state: MagicState,
|
|
498
|
+
result_columns: List[str], result: List[List],
|
|
499
|
+
file: str, noheader: bool):
|
|
463
500
|
_, ext = file.rsplit('.', 1)
|
|
464
501
|
|
|
465
502
|
# csv
|
|
@@ -479,37 +516,38 @@ class DuckDBKernel(Kernel):
|
|
|
479
516
|
else:
|
|
480
517
|
raise ValueError(f'extension {ext} not supported')
|
|
481
518
|
|
|
482
|
-
def _ra_magic(self, silent: bool,
|
|
483
|
-
if self._db is None:
|
|
484
|
-
raise AssertionError('load a database first')
|
|
485
|
-
|
|
519
|
+
def _ra_magic(self, silent: bool, state: MagicState, analyze: bool):
|
|
486
520
|
if silent:
|
|
487
521
|
return
|
|
488
522
|
|
|
489
|
-
if not code.strip():
|
|
523
|
+
if not state.code.strip():
|
|
490
524
|
return
|
|
491
525
|
|
|
526
|
+
if state.db is None:
|
|
527
|
+
raise AssertionError('load a database first')
|
|
528
|
+
|
|
492
529
|
# analyze tables
|
|
493
|
-
tables =
|
|
530
|
+
tables = state.db.analyze()
|
|
494
531
|
|
|
495
532
|
# parse ra input
|
|
496
|
-
root_node = RAParser.parse_query(code)
|
|
533
|
+
root_node = RAParser.parse_query(state.code)
|
|
497
534
|
|
|
498
535
|
# create and show visualization
|
|
499
536
|
if analyze:
|
|
500
|
-
vd = RATreeDrawer(
|
|
501
|
-
svg = vd.to_svg(True)
|
|
537
|
+
vd = RATreeDrawer(state.db, root_node, tables)
|
|
502
538
|
|
|
539
|
+
svg = vd.to_interactive_svg()
|
|
503
540
|
self.print_data(svg)
|
|
504
541
|
|
|
505
|
-
|
|
506
|
-
|
|
542
|
+
state.code = {
|
|
543
|
+
node_id: node.to_sql_with_renamed_columns(tables)
|
|
544
|
+
for node_id, node in vd.nodes.items()
|
|
545
|
+
}
|
|
507
546
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
547
|
+
else:
|
|
548
|
+
state.code = root_node.to_sql_with_renamed_columns(tables)
|
|
511
549
|
|
|
512
|
-
def _all_ra_magic(self, silent: bool, value: str):
|
|
550
|
+
def _all_ra_magic(self, silent: bool, state: MagicState, value: str):
|
|
513
551
|
if value.lower() in ('1', 'on', 'true'):
|
|
514
552
|
self._magics['ra'].default(True)
|
|
515
553
|
self._magics['dc'].default(False)
|
|
@@ -518,31 +556,29 @@ class DuckDBKernel(Kernel):
|
|
|
518
556
|
else:
|
|
519
557
|
self._magics['ra'].default(False)
|
|
520
558
|
|
|
521
|
-
def _dc_magic(self, silent: bool,
|
|
522
|
-
if self._db is None:
|
|
523
|
-
raise AssertionError('load a database first')
|
|
524
|
-
|
|
559
|
+
def _dc_magic(self, silent: bool, state: MagicState):
|
|
525
560
|
if silent:
|
|
526
561
|
return
|
|
527
562
|
|
|
528
|
-
if not code.strip():
|
|
563
|
+
if not state.code.strip():
|
|
529
564
|
return
|
|
530
565
|
|
|
566
|
+
if state.db is None:
|
|
567
|
+
raise AssertionError('load a database first')
|
|
568
|
+
|
|
531
569
|
# analyze tables
|
|
532
|
-
tables =
|
|
570
|
+
tables = state.db.analyze()
|
|
533
571
|
|
|
534
572
|
# parse dc input
|
|
535
|
-
root_node = DCParser.parse_query(code)
|
|
573
|
+
root_node = DCParser.parse_query(state.code)
|
|
536
574
|
|
|
537
575
|
# generate sql
|
|
538
576
|
sql, cnm = root_node.to_sql_with_renamed_columns(tables)
|
|
539
577
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
'column_name_mapping': cnm
|
|
543
|
-
}
|
|
578
|
+
state.code = sql
|
|
579
|
+
state.column_name_mapping.update(cnm)
|
|
544
580
|
|
|
545
|
-
def _all_dc_magic(self, silent: bool, value: str):
|
|
581
|
+
def _all_dc_magic(self, silent: bool, state: MagicState, value: str):
|
|
546
582
|
if value.lower() in ('1', 'on', 'true'):
|
|
547
583
|
self._magics['dc'].default(True)
|
|
548
584
|
self._magics['ra'].default(False)
|
|
@@ -551,41 +587,43 @@ class DuckDBKernel(Kernel):
|
|
|
551
587
|
else:
|
|
552
588
|
self._magics['dc'].default(False)
|
|
553
589
|
|
|
554
|
-
def _guess_parser_magic(self, silent: bool, value: str):
|
|
590
|
+
def _guess_parser_magic(self, silent: bool, state: MagicState, value: str):
|
|
555
591
|
if value.lower() in ('1', 'on', 'true'):
|
|
556
592
|
self._magics['auto_parser'].default(True)
|
|
557
593
|
self.print('The correct parser is guessed for each subsequently executed cell.\n')
|
|
558
594
|
else:
|
|
559
595
|
self._magics['auto_parser'].default(False)
|
|
560
596
|
|
|
561
|
-
def _auto_parser_magic(self, silent: bool,
|
|
597
|
+
def _auto_parser_magic(self, silent: bool, state: MagicState):
|
|
562
598
|
# do not handle statements starting with SQL keywords
|
|
563
|
-
first_word = code.strip().split(maxsplit=1)
|
|
599
|
+
first_word = state.code.strip().split(maxsplit=1)
|
|
564
600
|
if len(first_word) > 0:
|
|
565
601
|
if first_word[0].upper() in SQL_KEYWORDS:
|
|
566
602
|
return
|
|
567
603
|
|
|
568
604
|
# try to parse DC
|
|
569
605
|
try:
|
|
570
|
-
|
|
606
|
+
self._dc_magic(silent, state)
|
|
571
607
|
except ParserError as e:
|
|
572
608
|
if e.depth > 0:
|
|
573
609
|
raise e
|
|
574
610
|
|
|
575
611
|
# try to parse RA
|
|
576
612
|
try:
|
|
577
|
-
|
|
613
|
+
self._ra_magic(silent, state, analyze=False)
|
|
578
614
|
except ParserError as e:
|
|
579
615
|
if e.depth > 0:
|
|
580
616
|
raise e
|
|
581
617
|
|
|
582
|
-
def _plotly_magic(self, silent: bool,
|
|
618
|
+
def _plotly_magic(self, silent: bool, state: MagicState,
|
|
619
|
+
cols: List, rows: List[Tuple],
|
|
620
|
+
type: str, mapping: str, title: str = None):
|
|
583
621
|
# split mapping and handle asterisks
|
|
584
622
|
mapping = [m.strip() for m in mapping.split(',')]
|
|
585
623
|
|
|
586
624
|
for i in range(len(mapping)):
|
|
587
625
|
if mapping[i] == '*':
|
|
588
|
-
mapping = mapping[:i] + cols + mapping[i+1:]
|
|
626
|
+
mapping = mapping[:i] + cols + mapping[i + 1:]
|
|
589
627
|
|
|
590
628
|
# convert all column names to lower case
|
|
591
629
|
lower_cols = [c.lower() for c in cols]
|
|
@@ -654,7 +692,9 @@ class DuckDBKernel(Kernel):
|
|
|
654
692
|
# finally print the code
|
|
655
693
|
self.print_data(html, mime='text/html')
|
|
656
694
|
|
|
657
|
-
def _plotly_raw_magic(self, silent: bool,
|
|
695
|
+
def _plotly_raw_magic(self, silent: bool, state: MagicState,
|
|
696
|
+
cols: List, rows: List[Tuple],
|
|
697
|
+
title: str = None):
|
|
658
698
|
if len(cols) != 1 and len(rows) != 1:
|
|
659
699
|
raise ValueError(f'expected exactly one column and one row')
|
|
660
700
|
|
|
@@ -669,30 +709,27 @@ class DuckDBKernel(Kernel):
|
|
|
669
709
|
**kwargs):
|
|
670
710
|
try:
|
|
671
711
|
# get magic command
|
|
672
|
-
|
|
712
|
+
if len(self._db) > 0:
|
|
713
|
+
init_db = self._db[list(self._db.keys())[0]]
|
|
714
|
+
else:
|
|
715
|
+
init_db = None
|
|
673
716
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
'max_rows': DuckDBKernel.DEFAULT_MAX_ROWS
|
|
677
|
-
}
|
|
717
|
+
magic_state = MagicState(init_db, code, DuckDBKernel.DEFAULT_MAX_ROWS)
|
|
718
|
+
pre_query_callbacks, post_query_callbacks = self._magics(silent, magic_state)
|
|
678
719
|
|
|
720
|
+
# execute magic commands here if it does not depend on query results
|
|
679
721
|
for callback in pre_query_callbacks:
|
|
680
|
-
|
|
722
|
+
callback()
|
|
681
723
|
|
|
682
|
-
#
|
|
683
|
-
|
|
684
|
-
clean_code = execution_args['generated_code']
|
|
685
|
-
del execution_args['generated_code']
|
|
724
|
+
# execute statement if needed
|
|
725
|
+
cols, rows = None, None
|
|
686
726
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
execution_args['column_name_mapping'] = {}
|
|
727
|
+
if not isinstance(magic_state.code, dict):
|
|
728
|
+
magic_state.code = {'default': magic_state.code}
|
|
690
729
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
else:
|
|
695
|
-
cols, rows = None, None
|
|
730
|
+
for name, code in magic_state.code.items():
|
|
731
|
+
if code.strip():
|
|
732
|
+
cols, rows = self._execute_stmt(silent, magic_state, name, code)
|
|
696
733
|
|
|
697
734
|
# execute magic command here if it does depend on query results
|
|
698
735
|
for callback in post_query_callbacks:
|
|
@@ -716,5 +753,7 @@ class DuckDBKernel(Kernel):
|
|
|
716
753
|
}
|
|
717
754
|
|
|
718
755
|
def do_shutdown(self, restart):
|
|
719
|
-
self.
|
|
756
|
+
for name in list(self._db.keys()):
|
|
757
|
+
self._unload_database(name)
|
|
758
|
+
|
|
720
759
|
return super().do_shutdown(restart)
|