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 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('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
  )
@@ -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
@@ -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 AssertionError(f'right operand missing after {tokens[-i]}')
28
+ raise RAParserError(f'right operand missing after {tokens[-i]}', depth)
28
29
  if i == len(tokens):
29
- raise AssertionError(f'left operand missing before {tokens[-i]}')
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 AssertionError(f'{tokens=}')
71
+ raise RAParserError(f'{tokens=}', depth)
74
72
 
75
73
  return RAOperand(tokens[0])
@@ -1,3 +1,4 @@
1
1
  from .DCParser import DCParser
2
2
  from .LogicParser import LogicParser
3
3
  from .RAParser import RAParser
4
+ from .ParserError import *
@@ -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 AssertionError('arguments must be separated by commas')
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, *args, **kwargs):
22
+ def __init__(self, relation: Token, columns: Tuple[Token, ...], skip_comma: bool = False, depth: int = 0):
22
23
  super().__init__()
23
- self.invert = False
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, le.names[:i] + (new_token,) + le.names[i + 1:], skip_comma=True)
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 to_sql(self, tables: Dict[str, Table]) -> str:
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 AssertionError(f'invalid number of attributes for relation {operand.relation}')
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 AssertionError(f'could not build join for relation {left_name}')
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 AssertionError('no common attributes found for join')
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 AssertionError('no valid topologic order found for positive joins')
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 AssertionError('no valid topologic order found for negative joins')
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 f'SELECT DISTINCT {sql_select} FROM {sql_tables} WHERE ({sql_join_filters}) AND ({sql_condition})'
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?
@@ -0,0 +1,4 @@
1
+ class TestError(Exception):
2
+ @property
3
+ def message(self) -> str:
4
+ return str(self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: jupyter-duckdb
3
- Version: 1.2.0.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
+
@@ -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=Td6S4qWDC9uuqgAhSllXwR-JHHdnGfT14RHkfYLzXxI,19441
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=OoQ6j4cNtIYjaK4MPVzJyv1eYTNu4_a7qoRx-5G3Hg0,2346
22
- duckdb_kernel/magics/MagicCommandCallback.py,sha256=r1kkJyRR7sZnrnlMH3w4bGqDAJL-BVTIB4-Kn66ynlM,764
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=4njm49cNnLBH9j8GazrmA-wF8XkXfSioJDuCoDyumqc,2749
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=ciNdM_ZFfa10bBlAc_bB2tKmPOxnXBcAGpYROjbAkVY,2120
27
- duckdb_kernel/parser/LogicParser.py,sha256=PI4NTe4UZIsnEvoAe_LgpEtmGraTTmYOsUk5_Qr3QOk,1137
28
- duckdb_kernel/parser/RAParser.py,sha256=PcyqVxsfHlHJexHy77tL6XIubA2eArBUqbrw13BZz6Q,2891
29
- duckdb_kernel/parser/__init__.py,sha256=Tcq5YTgF-FQSgleSx-nGzUXfR_GR02DKUNDKgBskKVQ,99
30
- duckdb_kernel/parser/elements/DCOperand.py,sha256=RGj_n-v4BimiK3bMGSwOtV03g2FTg-97d4lJ8tPUZmM,977
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=4KzvUTls2bodJw9ejCKx8se32PR5VFJbVupDZVx2NHE,16671
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=vwN5hHg11kqzOHLeL5GO1c1BbCTZzYDTuy0QR4kDzew,1800
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.3.dist-info/METADATA,sha256=O4AGOi_-WpenZIFXf6ZGNNPFGDWAHTNOYOA2CdEB4g0,7980
80
- jupyter_duckdb-1.2.0.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
81
- jupyter_duckdb-1.2.0.3.dist-info/top_level.txt,sha256=KvRRPMnmkQNuhyBsXoPmwyt26LRDp0O-0HN6u0Dm5jA,14
82
- jupyter_duckdb-1.2.0.3.dist-info/RECORD,,
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,,