jupyter-duckdb 1.2.0.3__py3-none-any.whl → 1.2.0.5__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/kernel.py +81 -24
- duckdb_kernel/magics/MagicCommand.py +34 -10
- duckdb_kernel/magics/MagicCommandCallback.py +14 -3
- duckdb_kernel/magics/MagicCommandHandler.py +47 -6
- duckdb_kernel/magics/StringWrapper.py +3 -0
- duckdb_kernel/parser/DCParser.py +10 -7
- duckdb_kernel/parser/LogicParser.py +6 -6
- duckdb_kernel/parser/ParserError.py +18 -0
- duckdb_kernel/parser/RAParser.py +12 -14
- duckdb_kernel/parser/__init__.py +1 -0
- duckdb_kernel/parser/elements/DCOperand.py +7 -4
- duckdb_kernel/parser/elements/binary/ConditionalSet.py +30 -9
- duckdb_kernel/parser/tokenizer/Token.py +24 -3
- duckdb_kernel/util/TestError.py +4 -0
- {jupyter_duckdb-1.2.0.3.dist-info → jupyter_duckdb-1.2.0.5.dist-info}/METADATA +37 -6
- {jupyter_duckdb-1.2.0.3.dist-info → jupyter_duckdb-1.2.0.5.dist-info}/RECORD +18 -15
- {jupyter_duckdb-1.2.0.3.dist-info → jupyter_duckdb-1.2.0.5.dist-info}/WHEEL +0 -0
- {jupyter_duckdb-1.2.0.3.dist-info → jupyter_duckdb-1.2.0.5.dist-info}/top_level.txt +0 -0
duckdb_kernel/kernel.py
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
326
|
+
if isinstance(test_data['equals'], dict):
|
|
309
327
|
# get column order
|
|
310
|
-
data_columns = list(
|
|
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
|
-
|
|
339
|
+
raise TestError(f'attribute {dc} missing')
|
|
322
340
|
if found >= 2:
|
|
323
|
-
|
|
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
|
-
|
|
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(*(
|
|
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 =
|
|
356
|
+
rows = test_data['equals']
|
|
339
357
|
|
|
340
358
|
# ordered test
|
|
341
|
-
if
|
|
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
|
-
|
|
365
|
+
raise TestError(f'{row_count(missing)} missing')
|
|
348
366
|
|
|
349
367
|
missing = len(rsc.ordered_left_only)
|
|
350
368
|
if missing > 0:
|
|
351
|
-
|
|
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
|
-
|
|
381
|
+
raise TestError(f'{row_count(below)} missing, {row_count(above)} unnecessary')
|
|
366
382
|
elif below > 0:
|
|
367
|
-
|
|
383
|
+
raise TestError(f'{row_count(below)} missing')
|
|
368
384
|
elif above > 0:
|
|
369
|
-
|
|
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.
|
|
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
|
-
|
|
44
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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 = [
|
|
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,
|
|
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
|
-
|
|
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
|
duckdb_kernel/parser/DCParser.py
CHANGED
|
@@ -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
|
|
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
|
)
|
|
@@ -4,12 +4,12 @@ from .tokenizer import *
|
|
|
4
4
|
|
|
5
5
|
class LogicParser:
|
|
6
6
|
@staticmethod
|
|
7
|
-
def parse_query(query: str) -> LogicElement:
|
|
7
|
+
def parse_query(query: str, depth: int = 0) -> LogicElement:
|
|
8
8
|
initial_token = Token(query)
|
|
9
|
-
return LogicParser.parse_tokens(initial_token)
|
|
9
|
+
return LogicParser.parse_tokens(initial_token, depth=depth)
|
|
10
10
|
|
|
11
11
|
@staticmethod
|
|
12
|
-
def parse_tokens(*tokens: Token) -> LogicElement:
|
|
12
|
+
def parse_tokens(*tokens: Token, depth: int = 0) -> LogicElement:
|
|
13
13
|
if len(tokens) == 1:
|
|
14
14
|
tokens = tuple(Tokenizer.tokenize(tokens[0]))
|
|
15
15
|
|
|
@@ -21,14 +21,14 @@ class LogicParser:
|
|
|
21
21
|
# return the operator
|
|
22
22
|
# with left part of tokens and right part of tokens
|
|
23
23
|
return operator(
|
|
24
|
-
LogicParser.parse_tokens(*tokens[:-i]),
|
|
25
|
-
LogicParser.parse_tokens(*tokens[-i + 1:])
|
|
24
|
+
LogicParser.parse_tokens(*tokens[:-i], depth=depth + 1),
|
|
25
|
+
LogicParser.parse_tokens(*tokens[-i + 1:], depth=depth + 1)
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
# not
|
|
29
29
|
if tokens[0] in LOGIC_NOT.symbols():
|
|
30
30
|
return LOGIC_NOT(
|
|
31
|
-
LogicParser.parse_tokens(*tokens[1:])
|
|
31
|
+
LogicParser.parse_tokens(*tokens[1:], depth=depth + 1)
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
# ArgList
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class ParserError(Exception):
|
|
2
|
+
def __init__(self, message: str, depth: int):
|
|
3
|
+
super().__init__(message)
|
|
4
|
+
|
|
5
|
+
self.message: str = message
|
|
6
|
+
self.depth: int = depth
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RAParserError(ParserError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DCParserError(ParserError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LogicParserError(ParserError):
|
|
18
|
+
pass
|
duckdb_kernel/parser/RAParser.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .LogicParser import LogicParser
|
|
2
|
+
from .ParserError import RAParserError
|
|
2
3
|
from .elements import *
|
|
3
4
|
from .tokenizer import *
|
|
4
5
|
|
|
@@ -10,10 +11,10 @@ class RAParser:
|
|
|
10
11
|
@staticmethod
|
|
11
12
|
def parse_query(query: str) -> RAElement:
|
|
12
13
|
initial_token = Token(query)
|
|
13
|
-
return RAParser.parse_tokens(initial_token)
|
|
14
|
+
return RAParser.parse_tokens(initial_token, depth=0)
|
|
14
15
|
|
|
15
16
|
@staticmethod
|
|
16
|
-
def parse_tokens(*tokens: Token, target: RAOperator | RAOperand = None) -> RAElement:
|
|
17
|
+
def parse_tokens(*tokens: Token, target: RAOperator | RAOperand = None, depth: int = 0) -> RAElement:
|
|
17
18
|
if len(tokens) == 1:
|
|
18
19
|
tokens = tuple(Tokenizer.tokenize(tokens[0]))
|
|
19
20
|
|
|
@@ -24,15 +25,15 @@ class RAParser:
|
|
|
24
25
|
if tokens[-i].lower() in operator.symbols():
|
|
25
26
|
# raise error if left or right operand missing
|
|
26
27
|
if i == 1:
|
|
27
|
-
raise
|
|
28
|
+
raise RAParserError(f'right operand missing after {tokens[-i]}', depth)
|
|
28
29
|
if i == len(tokens):
|
|
29
|
-
raise
|
|
30
|
+
raise RAParserError(f'left operand missing before {tokens[-i]}', depth)
|
|
30
31
|
|
|
31
32
|
# return the operator
|
|
32
33
|
# with left part of tokens and right part of tokens
|
|
33
34
|
return operator(
|
|
34
|
-
RAParser.parse_tokens(*tokens[:-i]),
|
|
35
|
-
RAParser.parse_tokens(*tokens[-i + 1:])
|
|
35
|
+
RAParser.parse_tokens(*tokens[:-i], depth=depth + 1),
|
|
36
|
+
RAParser.parse_tokens(*tokens[-i + 1:], depth=depth + 1)
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
# unary operators
|
|
@@ -44,8 +45,8 @@ class RAParser:
|
|
|
44
45
|
# the last token is the operators target.
|
|
45
46
|
if target is None:
|
|
46
47
|
op = operator(
|
|
47
|
-
RAParser.parse_tokens(tokens[-1]),
|
|
48
|
-
LogicParser.parse_tokens(*tokens[-i + 1:-1])
|
|
48
|
+
RAParser.parse_tokens(tokens[-1], depth=depth + 1),
|
|
49
|
+
LogicParser.parse_tokens(*tokens[-i + 1:-1], depth=depth + 1)
|
|
49
50
|
)
|
|
50
51
|
|
|
51
52
|
# Otherwise the handed target is this operator's
|
|
@@ -53,16 +54,13 @@ class RAParser:
|
|
|
53
54
|
else:
|
|
54
55
|
op = operator(
|
|
55
56
|
target,
|
|
56
|
-
LogicParser.parse_tokens(*tokens[-i + 1:])
|
|
57
|
+
LogicParser.parse_tokens(*tokens[-i + 1:], depth=depth + 1)
|
|
57
58
|
)
|
|
58
59
|
|
|
59
60
|
# If there are any more tokens the operator is
|
|
60
61
|
# the target for the next step.
|
|
61
62
|
if i < len(tokens):
|
|
62
|
-
return RAParser.parse_tokens(
|
|
63
|
-
*tokens[:-i],
|
|
64
|
-
target=op
|
|
65
|
-
)
|
|
63
|
+
return RAParser.parse_tokens(*tokens[:-i], target=op, depth=depth + 1)
|
|
66
64
|
|
|
67
65
|
# Otherwise the operator is the return value.
|
|
68
66
|
else:
|
|
@@ -70,6 +68,6 @@ class RAParser:
|
|
|
70
68
|
|
|
71
69
|
# return as name
|
|
72
70
|
if len(tokens) > 1:
|
|
73
|
-
raise
|
|
71
|
+
raise RAParserError(f'{tokens=}', depth)
|
|
74
72
|
|
|
75
73
|
return RAOperand(tokens[0])
|
duckdb_kernel/parser/__init__.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from typing import Tuple
|
|
2
2
|
|
|
3
3
|
from .LogicOperand import LogicOperand
|
|
4
|
+
from ..ParserError import DCParserError
|
|
4
5
|
from ..tokenizer import Token
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class DCOperand(LogicOperand):
|
|
8
|
-
def __new__(cls, relation: Token, columns: Tuple[Token], skip_comma: bool = False):
|
|
9
|
+
def __new__(cls, relation: Token, columns: Tuple[Token, ...], skip_comma: bool = False, depth: int = 0):
|
|
9
10
|
if not skip_comma and not all(t == ',' for i, t in enumerate(columns) if i % 2 == 1):
|
|
10
|
-
raise
|
|
11
|
+
raise DCParserError('arguments must be separated by commas', 0)
|
|
11
12
|
|
|
12
13
|
return tuple.__new__(
|
|
13
14
|
cls,
|
|
@@ -18,9 +19,11 @@ class DCOperand(LogicOperand):
|
|
|
18
19
|
))
|
|
19
20
|
)
|
|
20
21
|
|
|
21
|
-
def __init__(self,
|
|
22
|
+
def __init__(self, relation: Token, columns: Tuple[Token, ...], skip_comma: bool = False, depth: int = 0):
|
|
22
23
|
super().__init__()
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
self.depth: int = depth
|
|
26
|
+
self.invert: bool = False
|
|
24
27
|
|
|
25
28
|
@property
|
|
26
29
|
def relation(self) -> Token:
|
|
@@ -8,6 +8,7 @@ from ..LogicElement import LogicElement
|
|
|
8
8
|
from ..LogicOperand import LogicOperand
|
|
9
9
|
from ..LogicOperator import LogicOperator
|
|
10
10
|
from ..unary import Not
|
|
11
|
+
from ...ParserError import DCParserError
|
|
11
12
|
from ...tokenizer import Token
|
|
12
13
|
from ...util.RenamableColumnList import RenamableColumnList
|
|
13
14
|
from ....db import Table
|
|
@@ -42,8 +43,11 @@ class ConditionalSet:
|
|
|
42
43
|
|
|
43
44
|
# If a constant was found, we store the value and replace it with a random attribute name.
|
|
44
45
|
constant = le.names[i]
|
|
45
|
-
new_token = Token.random()
|
|
46
|
-
new_operand = DCOperand(le.relation,
|
|
46
|
+
new_token = Token.random(constant)
|
|
47
|
+
new_operand = DCOperand(le.relation,
|
|
48
|
+
le.names[:i] + (new_token,) + le.names[i + 1:],
|
|
49
|
+
skip_comma=True,
|
|
50
|
+
depth=le.depth)
|
|
47
51
|
|
|
48
52
|
# We now need an equality comparison to ensure the introduced attribute is equal to the constant.
|
|
49
53
|
equality = Equal(
|
|
@@ -103,7 +107,7 @@ class ConditionalSet:
|
|
|
103
107
|
# The default case is to return the LogicElement with not DCOperands.
|
|
104
108
|
return le, []
|
|
105
109
|
|
|
106
|
-
def
|
|
110
|
+
def to_sql_with_renamed_columns(self, tables: Dict[str, Table]) -> Tuple[str, Dict[str, str]]:
|
|
107
111
|
# First we have to find and remove all DCOperands from the operator tree.
|
|
108
112
|
condition, dc_operands = self.split_tree(self.condition)
|
|
109
113
|
|
|
@@ -121,7 +125,8 @@ class ConditionalSet:
|
|
|
121
125
|
# Raise an exception if the given number of operands does not match
|
|
122
126
|
# the number of attributes in the relation.
|
|
123
127
|
if len(source_columns) != len(operand.names):
|
|
124
|
-
raise
|
|
128
|
+
raise DCParserError(f'invalid number of attributes for relation {operand.relation}',
|
|
129
|
+
depth=operand.depth)
|
|
125
130
|
|
|
126
131
|
# Create a column list for this operand.
|
|
127
132
|
rcl: RenamableColumnList = RenamableColumnList.from_iter(source_columns)
|
|
@@ -215,7 +220,8 @@ class ConditionalSet:
|
|
|
215
220
|
if left_name != right_name:
|
|
216
221
|
break
|
|
217
222
|
else:
|
|
218
|
-
raise
|
|
223
|
+
raise DCParserError(f'could not build join for relation {left_name}',
|
|
224
|
+
depth=left_op.depth)
|
|
219
225
|
|
|
220
226
|
join_tuple = min(left_name, right_name), max(left_name, right_name)
|
|
221
227
|
|
|
@@ -253,7 +259,7 @@ class ConditionalSet:
|
|
|
253
259
|
|
|
254
260
|
# If no joins were discovered using this table, an exception is raised.
|
|
255
261
|
if discovered_joins == 0:
|
|
256
|
-
raise
|
|
262
|
+
raise DCParserError('no common attributes found for join', depth=right_op.depth)
|
|
257
263
|
|
|
258
264
|
# The joins have to be sorted in a topologic order starting from t0.
|
|
259
265
|
used_relations: Set[str] = {'t0'}
|
|
@@ -287,7 +293,8 @@ class ConditionalSet:
|
|
|
287
293
|
break
|
|
288
294
|
|
|
289
295
|
else:
|
|
290
|
-
raise
|
|
296
|
+
raise DCParserError('no valid topologic order found for positive joins',
|
|
297
|
+
depth=min(op.depth for _, _, op in relevant_positive))
|
|
291
298
|
|
|
292
299
|
all_negative_conditions: Dict[str, List[str]] = {}
|
|
293
300
|
all_negative_filters: Dict[str, List[str]] = {}
|
|
@@ -317,7 +324,8 @@ class ConditionalSet:
|
|
|
317
324
|
used_relations.add(target_name)
|
|
318
325
|
break
|
|
319
326
|
else:
|
|
320
|
-
raise
|
|
327
|
+
raise DCParserError('no valid topologic order found for negative joins',
|
|
328
|
+
depth=min(op.depth for _, _, op in relevant_negative))
|
|
321
329
|
|
|
322
330
|
# Build the SQL statement.
|
|
323
331
|
sql_select = ', '.join(select_columns[col] if col in select_columns else col
|
|
@@ -339,5 +347,18 @@ class ConditionalSet:
|
|
|
339
347
|
sql_join_filters += f' AND {join_filter}'
|
|
340
348
|
|
|
341
349
|
sql_condition = condition.to_sql(joined_columns) if condition is not None else '1=1'
|
|
350
|
+
sql_query = f'SELECT DISTINCT {sql_select} FROM {sql_tables} WHERE ({sql_join_filters}) AND ({sql_condition})'
|
|
351
|
+
|
|
352
|
+
# Create a mapping from intermediate column names to constant values.
|
|
353
|
+
column_name_mapping = {
|
|
354
|
+
p: p.constant
|
|
355
|
+
for o in dc_operands
|
|
356
|
+
for p in o.names
|
|
357
|
+
if p.constant is not None
|
|
358
|
+
}
|
|
342
359
|
|
|
343
|
-
return
|
|
360
|
+
return sql_query, column_name_mapping
|
|
361
|
+
|
|
362
|
+
def to_sql(self, tables: Dict[str, Table]) -> str:
|
|
363
|
+
sql, _ = self.to_sql_with_renamed_columns(tables)
|
|
364
|
+
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?
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: jupyter-duckdb
|
|
3
|
-
Version: 1.2.0.
|
|
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
|
-
[](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
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
duckdb_kernel/__init__.py,sha256=6auU6zeJrsA4fxPSr2PYamS8fG-SMXTn5YQFXF2cseo,33
|
|
2
2
|
duckdb_kernel/__main__.py,sha256=Z3GwHEBWoQjNm2Y84ijnbA0Lk66L7nsFREuqhZ_ptk0,165
|
|
3
3
|
duckdb_kernel/kernel.json,sha256=_7E8Ci2FSdCvnzCjsOaue8QE8AvpS5JLQuxORO5IGtA,127
|
|
4
|
-
duckdb_kernel/kernel.py,sha256=
|
|
4
|
+
duckdb_kernel/kernel.py,sha256=4OFNtWvrfSs27udrk9e1ClBMdltvLA9gJemSBZskj0k,21827
|
|
5
5
|
duckdb_kernel/db/Column.py,sha256=GM5P6sFdlYK92hiKln5-6038gIDOTxh1AYbR4kiga_w,559
|
|
6
6
|
duckdb_kernel/db/Connection.py,sha256=5pH-CwGh-r9Q2QwJKGSxvoINBU-sqmvZyG4Q1digfeE,599
|
|
7
7
|
duckdb_kernel/db/Constraint.py,sha256=1YgUHk7s8mHCVedbcuJKyXDykj7_ybbwT3Dk9p2VMis,287
|
|
@@ -18,16 +18,18 @@ duckdb_kernel/db/implementation/postgres/__init__.py,sha256=HKogB1es4wOiQUoh7_eT
|
|
|
18
18
|
duckdb_kernel/db/implementation/postgres/util.py,sha256=4nr1mqXhlwkMVXbJSfJ7dRlUm6UskpvgKApe7GRwmBI,281
|
|
19
19
|
duckdb_kernel/db/implementation/sqlite/Connection.py,sha256=L-aSUuN_7Z-hc5wczLjap2Xu-Vv6-dXmwXcN_WE8o98,6994
|
|
20
20
|
duckdb_kernel/db/implementation/sqlite/__init__.py,sha256=HKogB1es4wOiQUoh7_eT32xnUFLmzoCyR_0LuY9r8YQ,35
|
|
21
|
-
duckdb_kernel/magics/MagicCommand.py,sha256=
|
|
22
|
-
duckdb_kernel/magics/MagicCommandCallback.py,sha256=
|
|
21
|
+
duckdb_kernel/magics/MagicCommand.py,sha256=l0EmnqgGZ0HyqQhdTljAaftflVo_RYp-U5UiDftYxAA,3180
|
|
22
|
+
duckdb_kernel/magics/MagicCommandCallback.py,sha256=9XVidNtzs8Lwq_T59VrH89t5LUgJcc5C7grusElXVW4,1041
|
|
23
23
|
duckdb_kernel/magics/MagicCommandException.py,sha256=MwuWkpA6NoCqz437urdI0RVXhbSbVdziuRoi7slYFPc,49
|
|
24
|
-
duckdb_kernel/magics/MagicCommandHandler.py,sha256=
|
|
24
|
+
duckdb_kernel/magics/MagicCommandHandler.py,sha256=TCCtAEaop5yTUn2af4wzcReEto3nql-40-DpIdIjKEw,4367
|
|
25
|
+
duckdb_kernel/magics/StringWrapper.py,sha256=W6qIfeHU51do1edd6yXgw6K7orzjwSHU4oWAI5DuKEE,96
|
|
25
26
|
duckdb_kernel/magics/__init__.py,sha256=DA8gnQeRCUt1Scy3_NQ9w5CPmMEY9i8YwB-g392pN1U,204
|
|
26
|
-
duckdb_kernel/parser/DCParser.py,sha256=
|
|
27
|
-
duckdb_kernel/parser/LogicParser.py,sha256=
|
|
28
|
-
duckdb_kernel/parser/
|
|
29
|
-
duckdb_kernel/parser/
|
|
30
|
-
duckdb_kernel/parser/
|
|
27
|
+
duckdb_kernel/parser/DCParser.py,sha256=16c1mxa494KP9OreUKQHsSQKoDGZ7NNp2u_gi_D-dkw,2293
|
|
28
|
+
duckdb_kernel/parser/LogicParser.py,sha256=_vZwE5OPRUEN8aEC_fSZAYKR_dpexqNthXog9OFHYRY,1233
|
|
29
|
+
duckdb_kernel/parser/ParserError.py,sha256=qJQVloFtID1HgVDQ1Io247bODT1ic3oO9Z1ZrWR-2Mk,321
|
|
30
|
+
duckdb_kernel/parser/RAParser.py,sha256=dL5Q5J8tmlGuKQMfadvJK6cb9KmzfZLkfcUAthqDmXI,2993
|
|
31
|
+
duckdb_kernel/parser/__init__.py,sha256=nTmDm1ADvNPDHhVJQLxKYmArNJk6967EUXqn5AkT8FM,126
|
|
32
|
+
duckdb_kernel/parser/elements/DCOperand.py,sha256=qEg_6Us4WV1eK4Bq6oUsmFt_L_x5pJPGce_wSapzIYA,1149
|
|
31
33
|
duckdb_kernel/parser/elements/LogicElement.py,sha256=YasKHxWLDDP8UdyLIKbXzqIRA8-XaakjmvTj-1Iuzyc,280
|
|
32
34
|
duckdb_kernel/parser/elements/LogicOperand.py,sha256=B9NvriloQE5eP734dNMZBZwrdaaIfsuAmZlG1t2eMhs,1021
|
|
33
35
|
duckdb_kernel/parser/elements/LogicOperator.py,sha256=lkM4TAGkXUhsO4w4PLKVA0bgCRGPQQFpNA1FcWWOW9Q,1028
|
|
@@ -40,7 +42,7 @@ duckdb_kernel/parser/elements/__init__.py,sha256=4DA2M43hh9d1fZb5Z6YnTTI-IBkDyhC
|
|
|
40
42
|
duckdb_kernel/parser/elements/binary/Add.py,sha256=XGkZMfab01huk9EaI6JUfzkd2STbV1C_-TyC2guKE8I,190
|
|
41
43
|
duckdb_kernel/parser/elements/binary/And.py,sha256=0jgetTG8yo5TJSeK70Kj-PI9ERyek1eyMQXX5HBxa4Y,274
|
|
42
44
|
duckdb_kernel/parser/elements/binary/ArrowLeft.py,sha256=u4fZSoyT9lfvWXBwuhUl4DdjVZAOqyVIKmMVbpElLD4,203
|
|
43
|
-
duckdb_kernel/parser/elements/binary/ConditionalSet.py,sha256=
|
|
45
|
+
duckdb_kernel/parser/elements/binary/ConditionalSet.py,sha256=sZ3qrxPux7pb3fMrlyBg4Hw7n4-Ln-AeN70_Jp5dAPo,17652
|
|
44
46
|
duckdb_kernel/parser/elements/binary/Cross.py,sha256=jVY3cvD6qDWZkJ7q74lFUPO2VdDt4aAjdk2YAfg-ZC4,687
|
|
45
47
|
duckdb_kernel/parser/elements/binary/Difference.py,sha256=ZVRgJHYVMOFwnc97oPvGtKvLvHsjSCsn2Aao6ymxY8Y,742
|
|
46
48
|
duckdb_kernel/parser/elements/binary/Divide.py,sha256=d7mzaOeRYSRO1F-2IHsv_C939TuYtLppbf4-5GSRJXs,265
|
|
@@ -63,20 +65,21 @@ duckdb_kernel/parser/elements/unary/Projection.py,sha256=CJ-MIf1-__1ewTjNZVy5hOz
|
|
|
63
65
|
duckdb_kernel/parser/elements/unary/Rename.py,sha256=Zr2n9EJ3nA476lND0Djz2b6493nnsbSpJ9kkEgk5B_Y,1273
|
|
64
66
|
duckdb_kernel/parser/elements/unary/Selection.py,sha256=TKykDMw0QGQcMFp0r7g6ye4CkjshBTNq14c7qtMkqs4,955
|
|
65
67
|
duckdb_kernel/parser/elements/unary/__init__.py,sha256=48EDygy0pD7l3J_BlXGc-b7HYPaiHQa1-0Mcsj9Xzr0,270
|
|
66
|
-
duckdb_kernel/parser/tokenizer/Token.py,sha256=
|
|
68
|
+
duckdb_kernel/parser/tokenizer/Token.py,sha256=gsCzgU_zLiA-yD0FWvd2qS9LQUXbivESYH-34Glffqs,2404
|
|
67
69
|
duckdb_kernel/parser/tokenizer/Tokenizer.py,sha256=PWGgS7gYgpULiKGDho842UbaXuqmwEkccixuF10oi5g,5081
|
|
68
70
|
duckdb_kernel/parser/tokenizer/__init__.py,sha256=EOSmfc2RJwtB5cE1Hhj1JAra97tckxxS8-legybPy60,58
|
|
69
71
|
duckdb_kernel/parser/util/RenamableColumn.py,sha256=LxJhFDMUv_OxYYDLwKn63QGpBRfs08jVvhuJTzRtc9c,704
|
|
70
72
|
duckdb_kernel/parser/util/RenamableColumnList.py,sha256=GfhdGv4KYT64Z9YA9TCn-7hhcEcc3Gu3vI2zMZ52w-8,3015
|
|
71
73
|
duckdb_kernel/parser/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
74
|
duckdb_kernel/util/ResultSetComparator.py,sha256=RZDIfjJyx8-eR-HIqQlEYgZd_V1ympbszpVRF4TlA7o,2262
|
|
75
|
+
duckdb_kernel/util/TestError.py,sha256=iwlGHr9j6pFDa2cGxqGyvJ-exrFUtPJjVm_OhHi4n3g,97
|
|
73
76
|
duckdb_kernel/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
74
77
|
duckdb_kernel/util/formatting.py,sha256=cbt0CtERnqtzd97mLrOjeJpqM2Lo6pW96BjAYqrOTD8,793
|
|
75
78
|
duckdb_kernel/visualization/Drawer.py,sha256=D0LkiGMvuJ2v6cQSg_axLTGaM4VXAJEQJAynvedQ3So,296
|
|
76
79
|
duckdb_kernel/visualization/RATreeDrawer.py,sha256=j-Vy1zpYMzwZ3CsphyfPW-J7ou9a9tM6aXXgAlQTgDI,2128
|
|
77
80
|
duckdb_kernel/visualization/SchemaDrawer.py,sha256=9K-TUUmyeGdMYMTFQJ7evIU3p8p2KyMKeizUc7-y8co,3015
|
|
78
81
|
duckdb_kernel/visualization/__init__.py,sha256=5eMJmxJ01XAXcgWDn3t70eSZF2PGaXdNo6GK-x-0H3s,78
|
|
79
|
-
jupyter_duckdb-1.2.0.
|
|
80
|
-
jupyter_duckdb-1.2.0.
|
|
81
|
-
jupyter_duckdb-1.2.0.
|
|
82
|
-
jupyter_duckdb-1.2.0.
|
|
82
|
+
jupyter_duckdb-1.2.0.5.dist-info/METADATA,sha256=7twgLgE-u5--Y9lV-JCtqPoUFSL8GAwdPyVcmu1NzjA,9115
|
|
83
|
+
jupyter_duckdb-1.2.0.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
84
|
+
jupyter_duckdb-1.2.0.5.dist-info/top_level.txt,sha256=KvRRPMnmkQNuhyBsXoPmwyt26LRDp0O-0HN6u0Dm5jA,14
|
|
85
|
+
jupyter_duckdb-1.2.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|