vastdb 0.1.5__py3-none-any.whl → 0.1.6__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.
vastdb/bench/test_perf.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import logging
2
2
  import time
3
3
 
4
- import pyarrow as pa
5
4
  import pytest
6
5
 
7
6
  from vastdb import util
@@ -20,7 +19,7 @@ def test_bench(session, clean_bucket_name, parquets_path, crater_path):
20
19
  t = util.create_table_from_files(s, 't1', files, config=ImportConfig(import_concurrency=8))
21
20
  config = QueryConfig(num_splits=8, num_sub_splits=4)
22
21
  s = time.time()
23
- pa_table = pa.Table.from_batches(t.select(columns=['sid'], predicate=t['sid'] == 10033007, config=config))
22
+ pa_table = t.select(columns=['sid'], predicate=t['sid'] == 10033007, config=config).read_all()
24
23
  e = time.time()
25
24
  log.info("'SELECT sid from TABLE WHERE sid = 10033007' returned in %s seconds.", e - s)
26
25
  if crater_path:
vastdb/bucket.py CHANGED
@@ -5,7 +5,7 @@ It is possible to list and access VAST snapshots generated over a bucket.
5
5
  """
6
6
 
7
7
  import logging
8
- from dataclasses import dataclass
8
+ from dataclasses import dataclass, field
9
9
  from typing import TYPE_CHECKING, List, Optional
10
10
 
11
11
  from . import errors, schema, transaction
@@ -22,48 +22,23 @@ class Bucket:
22
22
 
23
23
  name: str
24
24
  tx: "transaction.Transaction"
25
+ _root_schema: "Schema" = field(init=False, compare=False, repr=False)
25
26
 
26
- def create_schema(self, path: str, fail_if_exists=True) -> "Schema":
27
+ def __post_init__(self):
28
+ """Root schema is empty."""
29
+ self._root_schema = schema.Schema(name="", bucket=self)
30
+
31
+ def create_schema(self, name: str, fail_if_exists=True) -> "Schema":
27
32
  """Create a new schema (a container of tables) under this bucket."""
28
- if current := self.schema(path, fail_if_missing=False):
29
- if fail_if_exists:
30
- raise errors.SchemaExists(self.name, path)
31
- else:
32
- return current
33
- self.tx._rpc.api.create_schema(self.name, path, txid=self.tx.txid)
34
- log.info("Created schema: %s", path)
35
- return self.schema(path) # type: ignore[return-value]
33
+ return self._root_schema.create_schema(name=name, fail_if_exists=fail_if_exists)
36
34
 
37
- def schema(self, path: str, fail_if_missing=True) -> Optional["Schema"]:
35
+ def schema(self, name: str, fail_if_missing=True) -> Optional["Schema"]:
38
36
  """Get a specific schema (a container of tables) under this bucket."""
39
- s = self.schemas(path)
40
- log.debug("schema: %s", s)
41
- if not s:
42
- if fail_if_missing:
43
- raise errors.MissingSchema(self.name, path)
44
- else:
45
- return None
46
- assert len(s) == 1, f"Expected to receive only a single schema, but got: {len(s)}. ({s})"
47
- log.debug("Found schema: %s", s[0].name)
48
- return s[0]
37
+ return self._root_schema.schema(name=name, fail_if_missing=fail_if_missing)
49
38
 
50
- def schemas(self, name: Optional[str] = None) -> List["Schema"]:
39
+ def schemas(self, batch_size=None):
51
40
  """List bucket's schemas."""
52
- schemas = []
53
- next_key = 0
54
- exact_match = bool(name)
55
- log.debug("list schemas param: schema=%s, exact_match=%s", name, exact_match)
56
- while True:
57
- _bucket_name, curr_schemas, next_key, is_truncated, _ = \
58
- self.tx._rpc.api.list_schemas(bucket=self.name, next_key=next_key, txid=self.tx.txid,
59
- name_prefix=name, exact_match=exact_match)
60
- if not curr_schemas:
61
- break
62
- schemas.extend(curr_schemas)
63
- if not is_truncated:
64
- break
65
-
66
- return [schema.Schema(name=name, bucket=self) for name, *_ in schemas]
41
+ return self._root_schema.schemas(batch_size=batch_size)
67
42
 
68
43
  def snapshot(self, name, fail_if_missing=True) -> Optional["Bucket"]:
69
44
  """Get snapshot by name (if exists)."""
vastdb/conftest.py CHANGED
@@ -30,14 +30,23 @@ def test_bucket_name(request):
30
30
  return request.config.getoption("--tabular-bucket-name")
31
31
 
32
32
 
33
+ def iter_schemas(s):
34
+ """Recusively scan all schemas."""
35
+ children = s.schemas()
36
+ for c in children:
37
+ yield from iter_schemas(c)
38
+ yield s
39
+
40
+
33
41
  @pytest.fixture(scope="function")
34
42
  def clean_bucket_name(request, test_bucket_name, session):
35
43
  with session.transaction() as tx:
36
44
  b = tx.bucket(test_bucket_name)
37
- for s in b.schemas():
38
- for t in s.tables():
39
- t.drop()
40
- s.drop()
45
+ for top_schema in b.schemas():
46
+ for s in iter_schemas(top_schema):
47
+ for t in s.tables():
48
+ t.drop()
49
+ s.drop()
41
50
  return test_bucket_name
42
51
 
43
52
 
vastdb/errors.py CHANGED
@@ -89,7 +89,11 @@ class InvalidArgument(Exception):
89
89
  pass
90
90
 
91
91
 
92
- class TooWideRow(InvalidArgument):
92
+ class TooLargeRequest(InvalidArgument):
93
+ pass
94
+
95
+
96
+ class TooWideRow(TooLargeRequest):
93
97
  pass
94
98
 
95
99
 
@@ -6,7 +6,6 @@ import struct
6
6
  import urllib.parse
7
7
  from collections import defaultdict, namedtuple
8
8
  from enum import Enum
9
- from ipaddress import IPv4Address, IPv6Address
10
9
  from typing import Any, Dict, Iterator, List, Optional, Union
11
10
 
12
11
  import flatbuffers
@@ -1031,25 +1030,7 @@ class VastdbApi:
1031
1030
  num_rows = stats.NumRows()
1032
1031
  size_in_bytes = stats.SizeInBytes()
1033
1032
  is_external_rowid_alloc = stats.IsExternalRowidAlloc()
1034
- endpoints = []
1035
- if stats.VipsLength() == 0:
1036
- endpoints.append(self.url)
1037
- else:
1038
- url = urllib3.util.parse_url(self.url)
1039
-
1040
- ip_cls = IPv6Address if (stats.AddressType() == "ipv6") else IPv4Address
1041
- vips = [stats.Vips(i) for i in range(stats.VipsLength())]
1042
- ips = []
1043
- # extract the vips into list of IPs
1044
- for vip in vips:
1045
- start_ip = int(ip_cls(vip.StartAddress().decode()))
1046
- ips.extend(ip_cls(start_ip + i) for i in range(vip.AddressCount()))
1047
- # build a list of endpoint URLs, reusing schema and port (if specified when constructing the session).
1048
- # it is assumed that the client can access the returned IPs (e.g. if they are part of the VIP pool).
1049
- for ip in ips:
1050
- d = url._asdict()
1051
- d['host'] = str(ip)
1052
- endpoints.append(str(urllib3.util.Url(**d)))
1033
+ endpoints = [self.url] # we cannot replace the host by a VIP address in HTTPS-based URLs
1053
1034
  return TableStatsResult(num_rows, size_in_bytes, is_external_rowid_alloc, tuple(endpoints))
1054
1035
 
1055
1036
  def alter_table(self, bucket, schema, name, txid=0, client_tags=[], table_properties="",
vastdb/schema.py CHANGED
@@ -31,6 +31,51 @@ class Schema:
31
31
  """VAST transaction used for this schema."""
32
32
  return self.bucket.tx
33
33
 
34
+ def _subschema_full_name(self, name: str) -> str:
35
+ return f"{self.name}/{name}" if self.name else name
36
+
37
+ def create_schema(self, name: str, fail_if_exists=True) -> "Schema":
38
+ """Create a new schema (a container of tables) under this schema."""
39
+ if current := self.schema(name, fail_if_missing=False):
40
+ if fail_if_exists:
41
+ raise errors.SchemaExists(self.bucket.name, name)
42
+ else:
43
+ return current
44
+ full_name = self._subschema_full_name(name)
45
+ self.tx._rpc.api.create_schema(self.bucket.name, full_name, txid=self.tx.txid)
46
+ log.info("Created schema: %s", full_name)
47
+ return self.schema(name) # type: ignore[return-value]
48
+
49
+ def schema(self, name: str, fail_if_missing=True) -> Optional["Schema"]:
50
+ """Get a specific schema (a container of tables) under this schema."""
51
+ _bucket_name, schemas, _next_key, _is_truncated, _ = \
52
+ self.tx._rpc.api.list_schemas(bucket=self.bucket.name, schema=self.name, next_key=0, txid=self.tx.txid,
53
+ name_prefix=name, exact_match=True, max_keys=1)
54
+ names = [name for name, *_ in schemas]
55
+ log.debug("Found schemas: %s", names)
56
+ if not names:
57
+ if fail_if_missing:
58
+ raise errors.MissingSchema(self.bucket.name, self._subschema_full_name(name))
59
+ else:
60
+ return None
61
+
62
+ assert len(names) == 1, f"Expected to receive only a single schema, but got {len(schemas)}: ({schemas})"
63
+ return schema.Schema(name=self._subschema_full_name(names[0]), bucket=self.bucket)
64
+
65
+ def schemas(self, batch_size=None) -> List["Schema"]:
66
+ """List child schemas."""
67
+ next_key = 0
68
+ if not batch_size:
69
+ batch_size = 1000
70
+ result: List["Schema"] = []
71
+ while True:
72
+ _bucket_name, curr_schemas, next_key, is_truncated, _ = \
73
+ self.tx._rpc.api.list_schemas(bucket=self.bucket.name, schema=self.name, next_key=next_key, max_keys=batch_size, txid=self.tx.txid)
74
+ result.extend(schema.Schema(name=self._subschema_full_name(name), bucket=self.bucket) for name, *_ in curr_schemas)
75
+ if not is_truncated:
76
+ break
77
+ return result
78
+
34
79
  def create_table(self, table_name: str, columns: pa.Schema, fail_if_exists=True) -> "Table":
35
80
  """Create a new table under this schema."""
36
81
  if current := self.table(table_name, fail_if_missing=False):
vastdb/session.py CHANGED
@@ -7,12 +7,15 @@ For more details see:
7
7
  - [Tabular identity policy with the proper permissions](https://support.vastdata.com/s/article/UUID-14322b60-d6a2-89ac-3df0-3dfbb6974182)
8
8
  """
9
9
 
10
+ import logging
10
11
  import os
11
12
 
12
13
  import boto3
13
14
 
14
15
  from . import errors, internal_commands, transaction
15
16
 
17
+ log = logging.getLogger()
18
+
16
19
 
17
20
  class Features:
18
21
  """VAST database features - check if server is already support a feature."""
@@ -21,15 +24,28 @@ class Features:
21
24
  """Save the server version."""
22
25
  self.vast_version = vast_version
23
26
 
24
- def check_imports_table(self):
25
- """Check if the feature that support imports table is supported."""
26
- if self.vast_version < (5, 2):
27
- raise errors.NotSupportedVersion("import_table requires 5.2+", self.vast_version)
27
+ self.check_imports_table = self._check(
28
+ "Imported objects' table feature requires 5.2+ VAST release",
29
+ vast_version >= (5, 2))
30
+
31
+ self.check_return_row_ids = self._check(
32
+ "Returning row IDs requires 5.1+ VAST release",
33
+ vast_version >= (5, 1))
34
+
35
+ self.check_enforce_semisorted_projection = self._check(
36
+ "Semi-sorted projection enforcement requires 5.1+ VAST release",
37
+ vast_version >= (5, 1))
38
+
39
+ def _check(self, msg, supported):
40
+ log.debug("%s (current version is %s): supported=%s", msg, self.vast_version, supported)
41
+ if not supported:
42
+ def fail():
43
+ raise errors.NotSupportedVersion(msg, self.vast_version)
44
+ return fail
28
45
 
29
- def check_return_row_ids(self):
30
- """Check if insert/update/delete can return the row_ids."""
31
- if self.vast_version < (5, 1):
32
- raise errors.NotSupportedVersion("return_row_ids requires 5.1+", self.vast_version)
46
+ def noop():
47
+ pass
48
+ return noop
33
49
 
34
50
 
35
51
  class Session:
vastdb/table.py CHANGED
@@ -54,7 +54,8 @@ class QueryConfig:
54
54
  num_sub_splits: int = 4
55
55
 
56
56
  # used to split the table into disjoint subsets of rows, to be processed concurrently using multiple RPCs
57
- num_splits: int = 1
57
+ # will be estimated from the table's row count, if not explicitly set
58
+ num_splits: Optional[int] = None
58
59
 
59
60
  # each endpoint will be handled by a separate worker thread
60
61
  # a single endpoint can be specified more than once to benefit from multithreaded execution
@@ -64,12 +65,15 @@ class QueryConfig:
64
65
  limit_rows_per_sub_split: int = 128 * 1024
65
66
 
66
67
  # each fiber will read the following number of rowgroups coninuously before skipping
67
- # in order to use semi-sorted projections this value must be 8
68
+ # in order to use semi-sorted projections this value must be 8 (this is the hard coded size of a row groups per row block).
68
69
  num_row_groups_per_sub_split: int = 8
69
70
 
70
71
  # can be disabled for benchmarking purposes
71
72
  use_semi_sorted_projections: bool = True
72
73
 
74
+ # enforce using a specific semi-sorted projection (if enabled above)
75
+ semi_sorted_projection_name: Optional[str] = None
76
+
73
77
  # used to estimate the number of splits, given the table rows' count
74
78
  rows_per_split: int = 4000000
75
79
 
@@ -117,7 +121,8 @@ class SelectSplitState:
117
121
  limit_rows=self.config.limit_rows_per_sub_split,
118
122
  sub_split_start_row_ids=self.subsplits_state.items(),
119
123
  enable_sorted_projections=self.config.use_semi_sorted_projections,
120
- query_imports_table=self.table._imports_table)
124
+ query_imports_table=self.table._imports_table,
125
+ projection=self.config.semi_sorted_projection_name)
121
126
  pages_iter = internal_commands.parse_query_data_response(
122
127
  conn=response.raw,
123
128
  schema=self.query_data_request.response_schema,
@@ -313,11 +318,16 @@ class Table:
313
318
 
314
319
  # Take a snapshot of enpoints
315
320
  stats = self.get_stats()
321
+ log.debug("stats: %s", stats)
316
322
  endpoints = stats.endpoints if config.data_endpoints is None else config.data_endpoints
323
+ log.debug("endpoints: %s", endpoints)
324
+
325
+ if config.num_splits is None:
326
+ config.num_splits = max(1, stats.num_rows // config.rows_per_split)
327
+ log.debug("config: %s", config)
317
328
 
318
- if stats.num_rows > config.rows_per_split and config.num_splits is None:
319
- config.num_splits = stats.num_rows // config.rows_per_split
320
- log.debug(f"num_rows={stats.num_rows} rows_per_splits={config.rows_per_split} num_splits={config.num_splits} ")
329
+ if config.semi_sorted_projection_name:
330
+ self.tx._rpc.features.check_enforce_semisorted_projection()
321
331
 
322
332
  if columns is None:
323
333
  columns = [f.name for f in self.arrow_schema]
@@ -342,6 +352,8 @@ class Table:
342
352
  schema=query_schema,
343
353
  predicate=predicate,
344
354
  field_names=columns)
355
+ if len(query_data_request.serialized) > util.MAX_QUERY_DATA_REQUEST_SIZE:
356
+ raise errors.TooLargeRequest(f"{len(query_data_request.serialized)} bytes")
345
357
 
346
358
  splits_queue: queue.Queue[int] = queue.Queue()
347
359
 
@@ -38,13 +38,13 @@ def test_parallel_imports(session, clean_bucket_name, s3):
38
38
  t.create_imports_table()
39
39
  log.info("Starting import of %d files", num_files)
40
40
  t.import_files(files)
41
- arrow_table = pa.Table.from_batches(t.select(columns=['num']))
41
+ arrow_table = t.select(columns=['num']).read_all()
42
42
  assert arrow_table.num_rows == num_rows * num_files
43
- arrow_table = pa.Table.from_batches(t.select(columns=['num'], predicate=t['num'] == 100))
43
+ arrow_table = t.select(columns=['num'], predicate=t['num'] == 100).read_all()
44
44
  assert arrow_table.num_rows == num_files
45
45
  import_table = t.imports_table()
46
46
  # checking all imports are on the imports table:
47
- objects_name = pa.Table.from_batches(import_table.select(columns=["ObjectName"]))
47
+ objects_name = import_table.select(columns=["ObjectName"]).read_all()
48
48
  objects_name = objects_name.to_pydict()
49
49
  object_names = set(objects_name['ObjectName'])
50
50
  prefix = 'prq'
@@ -22,13 +22,13 @@ def test_nested_select(session, clean_bucket_name):
22
22
  ])
23
23
 
24
24
  with prepare_data(session, clean_bucket_name, 's', 't', expected) as t:
25
- actual = pa.Table.from_batches(t.select())
25
+ actual = t.select().read_all()
26
26
  assert actual == expected
27
27
 
28
28
  names = [f.name for f in columns]
29
29
  for n in range(len(names) + 1):
30
30
  for cols in itertools.permutations(names, n):
31
- actual = pa.Table.from_batches(t.select(columns=cols))
31
+ actual = t.select(columns=cols).read_all()
32
32
  assert actual == expected.select(cols)
33
33
 
34
34
 
@@ -53,7 +53,7 @@ def test_nested_filter(session, clean_bucket_name):
53
53
  ])
54
54
 
55
55
  with prepare_data(session, clean_bucket_name, 's', 't', expected) as t:
56
- actual = pa.Table.from_batches(t.select())
56
+ actual = t.select().read_all()
57
57
  assert actual == expected
58
58
 
59
59
  names = list('xyzw')
@@ -62,7 +62,7 @@ def test_nested_filter(session, clean_bucket_name):
62
62
  ibis_predicate = functools.reduce(
63
63
  operator.and_,
64
64
  (t[col] > 2 for col in cols))
65
- actual = pa.Table.from_batches(t.select(predicate=ibis_predicate), t.arrow_schema)
65
+ actual = t.select(predicate=ibis_predicate).read_all()
66
66
 
67
67
  arrow_predicate = functools.reduce(
68
68
  operator.and_,
@@ -1,7 +1,10 @@
1
1
  import logging
2
+ import time
2
3
 
3
4
  import pyarrow as pa
4
5
 
6
+ from vastdb.table import QueryConfig
7
+
5
8
  log = logging.getLogger(__name__)
6
9
 
7
10
 
@@ -41,3 +44,78 @@ def test_basic_projections(session, clean_bucket_name):
41
44
  projs = t.projections()
42
45
  assert len(projs) == 1
43
46
  assert projs[0].name == 'p_new'
47
+
48
+
49
+ def test_query_data_with_projection(session, clean_bucket_name):
50
+ columns = pa.schema([
51
+ ('a', pa.int64()),
52
+ ('b', pa.int64()),
53
+ ('s', pa.utf8()),
54
+ ])
55
+ # need to be large enough in order to consider as projection
56
+
57
+ GROUP_SIZE = 128 * 1024
58
+ expected = pa.table(schema=columns, data=[
59
+ [i for i in range(GROUP_SIZE)],
60
+ [i for i in reversed(range(GROUP_SIZE))],
61
+ [f's{i}' for i in range(GROUP_SIZE)],
62
+ ])
63
+
64
+ expected_projection_p1 = pa.table(schema=columns, data=[
65
+ [i for i in reversed(range(GROUP_SIZE - 5, GROUP_SIZE))],
66
+ [i for i in range(5)],
67
+ [f's{i}' for i in reversed(range(GROUP_SIZE - 5, GROUP_SIZE))],
68
+ ])
69
+
70
+ expected_projection_p2 = pa.table(schema=columns, data=[
71
+ [i for i in range(GROUP_SIZE - 5, GROUP_SIZE)],
72
+ [i for i in reversed(range(5))],
73
+ [f's{i}' for i in range(GROUP_SIZE - 5, GROUP_SIZE)],
74
+ ])
75
+
76
+ schema_name = "schema"
77
+ table_name = "table"
78
+ with session.transaction() as tx:
79
+ s = tx.bucket(clean_bucket_name).create_schema(schema_name)
80
+ t = s.create_table(table_name, expected.schema)
81
+
82
+ sorted_columns = ['b']
83
+ unsorted_columns = ['a', 's']
84
+ t.create_projection('p1', sorted_columns, unsorted_columns)
85
+
86
+ sorted_columns = ['a']
87
+ unsorted_columns = ['b', 's']
88
+ t.create_projection('p2', sorted_columns, unsorted_columns)
89
+
90
+ with session.transaction() as tx:
91
+ s = tx.bucket(clean_bucket_name).schema(schema_name)
92
+ t = s.table(table_name)
93
+ t.insert(expected)
94
+ actual = pa.Table.from_batches(t.select(columns=['a', 'b', 's']))
95
+ assert actual == expected
96
+
97
+ time.sleep(3)
98
+
99
+ with session.transaction() as tx:
100
+ config = QueryConfig()
101
+ # in nfs mock server num row groups per row block is 1 so need to change this in the config
102
+ config.num_row_groups_per_sub_split = 1
103
+
104
+ s = tx.bucket(clean_bucket_name).schema(schema_name)
105
+ t = s.table(table_name)
106
+ projection_actual = pa.Table.from_batches(t.select(columns=['a', 'b', 's'], predicate=(t['b'] < 5), config=config))
107
+ # no projection supply - need to be with p1 projeciton
108
+ assert expected_projection_p1 == projection_actual
109
+
110
+ config.semi_sorted_projection_name = 'p1'
111
+ projection_actual = pa.Table.from_batches(t.select(columns=['a', 'b', 's'], predicate=(t['b'] < 5), config=config))
112
+ # expecting results of projection p1 since we asked it specificaly
113
+ assert expected_projection_p1 == projection_actual
114
+
115
+ config.semi_sorted_projection_name = 'p2'
116
+ projection_actual = pa.Table.from_batches(t.select(columns=['a', 'b', 's'], predicate=(t['b'] < 5), config=config))
117
+ # expecting results of projection p2 since we asked it specificaly
118
+ assert expected_projection_p2 == projection_actual
119
+
120
+ t.drop()
121
+ s.drop()
@@ -61,3 +61,52 @@ def test_list_snapshots(session, clean_bucket_name):
61
61
  with session.transaction() as tx:
62
62
  b = tx.bucket(clean_bucket_name)
63
63
  b.snapshots() # VAST Catalog may create some snapshots
64
+
65
+
66
+ def test_nested_schemas(session, clean_bucket_name):
67
+ with session.transaction() as tx:
68
+ b = tx.bucket(clean_bucket_name)
69
+ s1 = b.create_schema('s1')
70
+ s1_s2 = s1.create_schema('s2')
71
+ s1_s3 = s1.create_schema('s3')
72
+ s1_s3_s4 = s1_s3.create_schema('s4')
73
+ s5 = b.create_schema('s5')
74
+
75
+ assert b.schema('s1') == s1
76
+ assert s1.schema('s2') == s1_s2
77
+ assert s1.schema('s3') == s1_s3
78
+ assert s1_s3.schema('s4') == s1_s3_s4
79
+ assert b.schema('s5') == s5
80
+
81
+ assert b.schemas() == [s1, s5]
82
+ assert s1.schemas() == [s1_s2, s1_s3]
83
+ assert s1_s2.schemas() == []
84
+ assert s1_s3.schemas() == [s1_s3_s4]
85
+ assert s1_s3_s4.schemas() == []
86
+ assert s5.schemas() == []
87
+
88
+ s1_s3_s4.drop()
89
+ assert s1_s3.schemas() == []
90
+ s1_s3.drop()
91
+ assert s1.schemas() == [s1_s2]
92
+ s1_s2.drop()
93
+ assert s1.schemas() == []
94
+
95
+ assert b.schemas() == [s1, s5]
96
+ s1.drop()
97
+ assert b.schemas() == [s5]
98
+ s5.drop()
99
+ assert b.schemas() == []
100
+
101
+
102
+ def test_schema_pagination(session, clean_bucket_name):
103
+ with session.transaction() as tx:
104
+ b = tx.bucket(clean_bucket_name)
105
+ names = [f's{i}' for i in range(10)]
106
+ schemas = [b.create_schema(name) for name in names]
107
+ assert b.schemas(batch_size=3) == schemas
108
+
109
+ s0 = b.schema('s0')
110
+ names = [f'q{i}' for i in range(10)]
111
+ subschemas = [s0.create_schema(name) for name in names]
112
+ assert s0.schemas(batch_size=3) == subschemas
@@ -3,7 +3,6 @@ import decimal
3
3
  import logging
4
4
  import random
5
5
  import threading
6
- import time
7
6
  from contextlib import closing
8
7
  from tempfile import NamedTemporaryFile
9
8
 
@@ -33,25 +32,25 @@ def test_tables(session, clean_bucket_name):
33
32
  ['a', 'bb', 'ccc'],
34
33
  ])
35
34
  with prepare_data(session, clean_bucket_name, 's', 't', expected) as t:
36
- actual = pa.Table.from_batches(t.select(columns=['a', 'b', 's']))
35
+ actual = t.select(columns=['a', 'b', 's']).read_all()
37
36
  assert actual == expected
38
37
 
39
- actual = pa.Table.from_batches(t.select())
38
+ actual = t.select().read_all()
40
39
  assert actual == expected
41
40
 
42
- actual = pa.Table.from_batches(t.select(columns=['a', 'b']))
41
+ actual = t.select(columns=['a', 'b']).read_all()
43
42
  assert actual == expected.select(['a', 'b'])
44
43
 
45
- actual = pa.Table.from_batches(t.select(columns=['b', 's', 'a']))
44
+ actual = t.select(columns=['b', 's', 'a']).read_all()
46
45
  assert actual == expected.select(['b', 's', 'a'])
47
46
 
48
- actual = pa.Table.from_batches(t.select(columns=['s']))
47
+ actual = t.select(columns=['s']).read_all()
49
48
  assert actual == expected.select(['s'])
50
49
 
51
- actual = pa.Table.from_batches(t.select(columns=[]))
50
+ actual = t.select(columns=[]).read_all()
52
51
  assert actual == expected.select([])
53
52
 
54
- actual = pa.Table.from_batches(t.select(columns=['s'], internal_row_id=True))
53
+ actual = t.select(columns=['s'], internal_row_id=True).read_all()
55
54
  log.debug("actual=%s", actual)
56
55
  assert actual.to_pydict() == {
57
56
  's': ['a', 'bb', 'ccc'],
@@ -62,9 +61,9 @@ def test_tables(session, clean_bucket_name):
62
61
  rb = pa.record_batch(schema=columns_to_delete, data=[[0]]) # delete rows 0,1
63
62
  t.delete(rb)
64
63
 
65
- selected_rows = pa.Table.from_batches(t.select(columns=['b'], predicate=(t['a'] == 222), internal_row_id=True))
64
+ selected_rows = t.select(columns=['b'], predicate=(t['a'] == 222), internal_row_id=True).read_all()
66
65
  t.delete(selected_rows)
67
- actual = pa.Table.from_batches(t.select(columns=['a', 'b', 's']))
66
+ actual = t.select(columns=['a', 'b', 's']).read_all()
68
67
  assert actual.to_pydict() == {
69
68
  'a': [333],
70
69
  'b': [2.5],
@@ -78,7 +77,7 @@ def test_insert_wide_row(session, clean_bucket_name):
78
77
  expected = pa.table(schema=columns, data=data)
79
78
 
80
79
  with prepare_data(session, clean_bucket_name, 's', 't', expected) as t:
81
- actual = pa.Table.from_batches(t.select())
80
+ actual = t.select().read_all()
82
81
  assert actual == expected
83
82
 
84
83
 
@@ -125,33 +124,33 @@ def test_update_table(session, clean_bucket_name):
125
124
  ])
126
125
 
127
126
  t.update(rb)
128
- actual = pa.Table.from_batches(t.select(columns=['a', 'b']))
127
+ actual = t.select(columns=['a', 'b']).read_all()
129
128
  assert actual.to_pydict() == {
130
129
  'a': [1110, 222, 3330],
131
130
  'b': [0.5, 1.5, 2.5]
132
131
  }
133
132
 
134
- actual = pa.Table.from_batches(t.select(columns=['a', 'b'], predicate=(t['a'] < 1000), internal_row_id=True))
133
+ actual = t.select(columns=['a', 'b'], predicate=(t['a'] < 1000), internal_row_id=True).read_all()
135
134
  column_index = actual.column_names.index('a')
136
135
  column_field = actual.field(column_index)
137
136
  new_data = pc.add(actual.column('a'), 2000)
138
137
  update_table = actual.set_column(column_index, column_field, new_data)
139
138
 
140
139
  t.update(update_table, columns=['a'])
141
- actual = pa.Table.from_batches(t.select(columns=['a', 'b']))
140
+ actual = t.select(columns=['a', 'b']).read_all()
142
141
  assert actual.to_pydict() == {
143
142
  'a': [1110, 2222, 3330],
144
143
  'b': [0.5, 1.5, 2.5]
145
144
  }
146
145
 
147
- actual = pa.Table.from_batches(t.select(columns=['a', 'b'], predicate=(t['a'] != 2222), internal_row_id=True))
146
+ actual = t.select(columns=['a', 'b'], predicate=(t['a'] != 2222), internal_row_id=True).read_all()
148
147
  column_index = actual.column_names.index('a')
149
148
  column_field = actual.field(column_index)
150
149
  new_data = pc.divide(actual.column('a'), 10)
151
150
  update_table = actual.set_column(column_index, column_field, new_data)
152
151
 
153
152
  t.update(update_table.to_batches()[0], columns=['a'])
154
- actual = pa.Table.from_batches(t.select(columns=['a', 'b']))
153
+ actual = t.select(columns=['a', 'b']).read_all()
155
154
  assert actual.to_pydict() == {
156
155
  'a': [111, 2222, 333],
157
156
  'b': [0.5, 1.5, 2.5]
@@ -171,7 +170,7 @@ def test_select_with_multisplits(session, clean_bucket_name):
171
170
  config.rows_per_split = 1000
172
171
 
173
172
  with prepare_data(session, clean_bucket_name, 's', 't', expected) as t:
174
- actual = pa.Table.from_batches(t.select(columns=['a'], config=config))
173
+ actual = t.select(columns=['a'], config=config).read_all()
175
174
  assert actual == expected
176
175
 
177
176
 
@@ -218,7 +217,7 @@ def test_types(session, clean_bucket_name):
218
217
 
219
218
  with prepare_data(session, clean_bucket_name, 's', 't', expected) as table:
220
219
  def select(predicate):
221
- return pa.Table.from_batches(table.select(predicate=predicate))
220
+ return table.select(predicate=predicate).read_all()
222
221
 
223
222
  assert select(None) == expected
224
223
  for t in [table, ibis._]:
@@ -274,13 +273,20 @@ def test_filters(session, clean_bucket_name):
274
273
 
275
274
  with prepare_data(session, clean_bucket_name, 's', 't', expected) as table:
276
275
  def select(predicate):
277
- return pa.Table.from_batches(table.select(predicate=predicate), table.arrow_schema)
276
+ return table.select(predicate=predicate).read_all()
278
277
 
279
278
  assert select(None) == expected
280
279
  assert select(True) == expected
281
280
  assert select(False) == pa.Table.from_batches([], schema=columns)
282
281
 
283
282
  for t in [table, ibis._]:
283
+
284
+ select(t['a'].isin(list(range(100))))
285
+ select(t['a'].isin(list(range(1000))))
286
+ select(t['a'].isin(list(range(10000))))
287
+ with pytest.raises(errors.TooLargeRequest):
288
+ select(t['a'].isin(list(range(100000))))
289
+
284
290
  assert select(t['a'].between(222, 444)) == expected.filter((pc.field('a') >= 222) & (pc.field('a') <= 444))
285
291
  assert select((t['a'].between(222, 444)) & (t['b'] > 2.5)) == expected.filter((pc.field('a') >= 222) & (pc.field('a') <= 444) & (pc.field('b') > 2.5))
286
292
 
@@ -351,7 +357,7 @@ def test_parquet_export(session, clean_bucket_name):
351
357
  expected = pa.Table.from_batches([rb])
352
358
  rb = t.insert(rb)
353
359
  assert rb.to_pylist() == [0, 1]
354
- actual = pa.Table.from_batches(t.select())
360
+ actual = t.select().read_all()
355
361
  assert actual == expected
356
362
 
357
363
  table_batches = t.select()
@@ -667,18 +673,37 @@ def test_select_stop(session, clean_bucket_name):
667
673
  assert active_threads() == 0
668
674
 
669
675
 
670
- def test_big_catalog_select(session, clean_bucket_name):
676
+ def test_catalog_select(session, clean_bucket_name):
671
677
  with session.transaction() as tx:
672
678
  bc = tx.catalog()
673
- actual = pa.Table.from_batches(bc.select(['name']))
674
- assert actual
675
- log.info("actual=%s", actual)
679
+ assert bc.columns()
680
+ rows = bc.select(['name']).read_all()
681
+ assert len(rows) > 0, rows
682
+
683
+
684
+ class NotReady(Exception):
685
+ pass
676
686
 
677
687
 
688
+ @pytest.mark.flaky(retries=30, delay=1, only_on=[NotReady])
678
689
  def test_audit_log_select(session, clean_bucket_name):
679
690
  with session.transaction() as tx:
680
691
  a = tx.audit_log()
681
- a.columns()
682
- time.sleep(1)
683
- actual = pa.Table.from_batches(a.select(), a.arrow_schema)
684
- log.info("actual=%s", actual)
692
+ assert a.columns()
693
+ rows = a.select().read_all()
694
+ if len(rows) == 0:
695
+ raise NotReady
696
+
697
+
698
+ @pytest.mark.flaky(retries=30, delay=1, only_on=[NotReady])
699
+ def test_catalog_snapshots_select(session, clean_bucket_name):
700
+ with session.transaction() as tx:
701
+ snaps = tx.catalog_snapshots()
702
+ if not snaps:
703
+ raise NotReady
704
+ latest = snaps[-1]
705
+ t = tx.catalog(latest)
706
+ assert t.columns()
707
+ rows = t.select().read_all()
708
+ if not rows:
709
+ raise NotReady
vastdb/transaction.py CHANGED
@@ -8,21 +8,26 @@ A transcation is used as a context manager, since every Database-related operati
8
8
 
9
9
  import logging
10
10
  from dataclasses import dataclass
11
- from typing import Optional
11
+ from typing import TYPE_CHECKING, List, Optional
12
12
 
13
13
  import botocore
14
14
 
15
- from . import bucket, errors, schema, session, table
15
+ from . import bucket, errors, schema, session
16
+
17
+ if TYPE_CHECKING:
18
+ from bucket import Bucket
19
+ from table import Table
20
+
16
21
 
17
22
  log = logging.getLogger(__name__)
18
23
 
19
- TABULAR_BC_BUCKET = "vast-big-catalog-bucket"
24
+ VAST_CATALOG_BUCKET_NAME = "vast-big-catalog-bucket"
20
25
  VAST_CATALOG_SCHEMA_NAME = 'vast_big_catalog_schema'
21
26
  VAST_CATALOG_TABLE_NAME = 'vast_big_catalog_table'
22
27
 
23
- TABULAR_AUDERY_BUCKET = "vast-audit-log-bucket"
24
- AUDERY_SCHEMA_NAME = 'vast_audit_log_schema'
25
- AUDERY_TABLE_NAME = 'vast_audit_log_table'
28
+ AUDIT_LOG_BUCKET_NAME = "vast-audit-log-bucket"
29
+ AUDIT_LOG_SCHEMA_NAME = 'vast_audit_log_schema'
30
+ AUDIT_LOG_TABLE_NAME = 'vast_audit_log_table'
26
31
 
27
32
 
28
33
  @dataclass
@@ -56,7 +61,7 @@ class Transaction:
56
61
  return 'InvalidTransaction'
57
62
  return f'Transaction(id=0x{self.txid:016x})'
58
63
 
59
- def bucket(self, name: str) -> "bucket.Bucket":
64
+ def bucket(self, name: str) -> "Bucket":
60
65
  """Return a VAST Bucket, if exists."""
61
66
  try:
62
67
  self._rpc.s3.head_bucket(Bucket=name)
@@ -67,14 +72,18 @@ class Transaction:
67
72
  raise
68
73
  return bucket.Bucket(name, self)
69
74
 
70
- def catalog(self, fail_if_missing=True) -> Optional["table.Table"]:
75
+ def catalog_snapshots(self) -> List["Bucket"]:
76
+ """Return VAST Catalog bucket snapshots."""
77
+ return bucket.Bucket(VAST_CATALOG_BUCKET_NAME, self).snapshots()
78
+
79
+ def catalog(self, snapshot: Optional["Bucket"] = None, fail_if_missing=True) -> Optional["Table"]:
71
80
  """Return VAST Catalog table."""
72
- b = bucket.Bucket(TABULAR_BC_BUCKET, self)
81
+ b = snapshot or bucket.Bucket(VAST_CATALOG_BUCKET_NAME, self)
73
82
  s = schema.Schema(VAST_CATALOG_SCHEMA_NAME, b)
74
83
  return s.table(name=VAST_CATALOG_TABLE_NAME, fail_if_missing=fail_if_missing)
75
84
 
76
- def audit_log(self, fail_if_missing=True) -> Optional["table.Table"]:
77
- """Return VAST AuditLog table."""
78
- b = bucket.Bucket(TABULAR_AUDERY_BUCKET, self)
79
- s = schema.Schema(AUDERY_SCHEMA_NAME, b)
80
- return s.table(name=AUDERY_TABLE_NAME, fail_if_missing=fail_if_missing)
85
+ def audit_log(self, fail_if_missing=True) -> Optional["Table"]:
86
+ """Return VAST Audit Log table."""
87
+ b = bucket.Bucket(AUDIT_LOG_BUCKET_NAME, self)
88
+ s = schema.Schema(AUDIT_LOG_SCHEMA_NAME, b)
89
+ return s.table(name=AUDIT_LOG_TABLE_NAME, fail_if_missing=fail_if_missing)
vastdb/util.py CHANGED
@@ -83,6 +83,7 @@ def union_schema_merge(current_schema: pa.Schema, new_schema: pa.Schema) -> pa.S
83
83
 
84
84
  MAX_TABULAR_REQUEST_SIZE = 5 << 20 # in bytes
85
85
  MAX_RECORD_BATCH_SLICE_SIZE = int(0.9 * MAX_TABULAR_REQUEST_SIZE)
86
+ MAX_QUERY_DATA_REQUEST_SIZE = int(0.9 * MAX_TABULAR_REQUEST_SIZE)
86
87
 
87
88
 
88
89
  def iter_serialized_slices(batch: Union[pa.RecordBatch, pa.Table], max_rows_per_slice=None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vastdb
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: VAST Data SDK
5
5
  Home-page: https://github.com/vast-data/vastdb_sdk
6
6
  Author: VAST DATA
@@ -149,29 +149,29 @@ vast_flatbuf/tabular/S3File.py,sha256=KC9c2oS5-JXwTTriUVFdjOvRG0B54Cq9kviSDZY3NI
149
149
  vast_flatbuf/tabular/VipRange.py,sha256=_BJd1RRZAcK76T9vlsHzXKYVsPVaz6WTEAqStMQCAUQ,2069
150
150
  vast_flatbuf/tabular/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
151
151
  vastdb/__init__.py,sha256=cMJtZuJ0IL9aKyM3DUWqTCzuP1H1MXXVivKKE1-q0DY,292
152
- vastdb/bucket.py,sha256=4rPEm9qlPTg7ccWO6VGmd4LKb8w-BDhJYwzXGjn03sc,3566
153
- vastdb/conftest.py,sha256=pKpo_46Vq4QHzTDQAFxasrVhnZ2V2L-y6IMLxojxaFM,2132
154
- vastdb/errors.py,sha256=fj8IlPnGi1lbJWIl1-8MSjLavL9bYQ-YUoboWbXCo54,4047
155
- vastdb/internal_commands.py,sha256=kIdkLHabW8r4-GSygGl1Gdrr4puxD79WPO8Jkx8aszg,98490
156
- vastdb/schema.py,sha256=ql4TPB1W_FQ_BHov3CKHI8JX3krXMlcKWz7dTrjpQ1w,3346
157
- vastdb/session.py,sha256=UTaz1Fh3u71Bnay2r6IyCHNMDrAszbzjnwylPURzhsk,2603
158
- vastdb/table.py,sha256=1ikj6toITImFowI2WHiimmqSiObmTfAohCdWC89q71Y,30031
159
- vastdb/transaction.py,sha256=u4pJBLooZQ_YGjsRgEWVL6RPAlt3lgm5oOpPHzPcayM,2852
160
- vastdb/util.py,sha256=rs7nLL2Qz-OVEZDSVIqAvS-uETMq-zxQs5jBksB5-JA,4276
152
+ vastdb/bucket.py,sha256=T0qX8efIJsQvK8Zn1_B-Np6BZqu_i9IuU3aN3JE7kyQ,2536
153
+ vastdb/conftest.py,sha256=D4RvOhGvMQy-JliKY-uyzcB-_mFBwI6aMF__xwHiwOM,2359
154
+ vastdb/errors.py,sha256=nC7d05xwe0WxMFyM3cEEqIvA09OXNqcxiUGsKov822I,4098
155
+ vastdb/internal_commands.py,sha256=r8EjueIaqSkdiqV6Cv7YsCiuuTO7rMCshyGExeBnXVw,97586
156
+ vastdb/schema.py,sha256=ro4GrVlhJEN4HT8qLdyPtiufVZrNBPGtej-z6Y-v2jg,5642
157
+ vastdb/session.py,sha256=zuy0wjKKB8d388KgRA77vQwxoraU0tWZacqVVMrU5dU,2984
158
+ vastdb/table.py,sha256=JZeAqww6dBlhXWW7J-LqC54a5IH2Dl8GTne3hU3wL2I,30646
159
+ vastdb/transaction.py,sha256=6mCnd43uF9AJyhhUZViD799Mde6AczBi5Cp7LGGDuoM,3178
160
+ vastdb/util.py,sha256=vt4LWROOFdZieJXLpQMlcnF7YWQFpPqQTVaRbmQ241o,4342
161
161
  vastdb/bench/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
- vastdb/bench/test_perf.py,sha256=iHE3E60fvyU5SBDHPi4h03Dj6QcY6VI9l9mMhgNMtPc,1117
162
+ vastdb/bench/test_perf.py,sha256=yn5gE7t_nzmJHBl9bCs1hxQOgzhvFphuYElsWGko8ts,1084
163
163
  vastdb/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
164
164
  vastdb/tests/test_duckdb.py,sha256=KDuv4PrjGEwChCGHG36xNT2JiFlBOt6K3DQ3L06Kq-A,1913
165
- vastdb/tests/test_imports.py,sha256=48kbJKsa_MrEXcBYQUbUDr1e9wzjG4FHQ7C3wUEQfXA,5705
166
- vastdb/tests/test_nested.py,sha256=FHYMmaKYvqVh0NvsocUFLr2LDVlSfXZYgqUSopWOSM0,3512
167
- vastdb/tests/test_projections.py,sha256=_cDNfD5zTwbCXLk6uGpPUWGN0P-4HElu5OjubWu-Jg0,1255
165
+ vastdb/tests/test_imports.py,sha256=xKub3-bisFjH0BsZM8COfiUWuMrtoOoQKprF6VQT9RI,5669
166
+ vastdb/tests/test_nested.py,sha256=22NAxBTm7Aq-Vn6AIYbi5Cb1ET8W0XeLK3pp4D8BYWI,3448
167
+ vastdb/tests/test_projections.py,sha256=11a-55VbJcqaFPkOKaKDEdM5nkeI0xtUhh6cQc1upSA,4223
168
168
  vastdb/tests/test_sanity.py,sha256=ixx0QPo73hLHjAa7bByFXjS1XST0WvmSwLEpgnHh_JY,2960
169
- vastdb/tests/test_schemas.py,sha256=qoHTLX51D-0S4bMxdCpRh9gaYQd-BkZdT_agGOwFwTM,1739
170
- vastdb/tests/test_tables.py,sha256=Q3N5P-7mOPVcfAFEfpAzomqkyCJ5gKZmfE4SUW5jehk,27859
169
+ vastdb/tests/test_schemas.py,sha256=l70YQMlx2UL1KRQhApriiG2ZM7GJF-IzWU31H3Yqn1U,3312
170
+ vastdb/tests/test_tables.py,sha256=V1-WOxCOD8ELJF6Ebj57Jwtum7Z6iYG9JKo89HxC7rM,28342
171
171
  vastdb/tests/test_util.py,sha256=owRAU3TCKMq-kz54NRdA5wX2O_bZIHqG5ucUR77jm5k,1046
172
172
  vastdb/tests/util.py,sha256=dpRJYbboDnlqL4qIdvScpp8--5fxRUBIcIYitrfcj9o,555
173
- vastdb-0.1.5.dist-info/LICENSE,sha256=obffan7LYrq7hLHNrY7vHcn2pKUTBUYXMKu-VOAvDxU,11333
174
- vastdb-0.1.5.dist-info/METADATA,sha256=NJzrnkyfPs4lliFamaEdJy2elLYLzYJtlCxEMRSiLtg,1350
175
- vastdb-0.1.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
176
- vastdb-0.1.5.dist-info/top_level.txt,sha256=Vsj2MKtlhPg0J4so64slQtnwjhgoPmJgcG-6YcVAwVc,20
177
- vastdb-0.1.5.dist-info/RECORD,,
173
+ vastdb-0.1.6.dist-info/LICENSE,sha256=obffan7LYrq7hLHNrY7vHcn2pKUTBUYXMKu-VOAvDxU,11333
174
+ vastdb-0.1.6.dist-info/METADATA,sha256=ibcsckhsDh4iGEN0xjK1_-FUrB24hNbwz9eAouRE6kY,1350
175
+ vastdb-0.1.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
176
+ vastdb-0.1.6.dist-info/top_level.txt,sha256=Vsj2MKtlhPg0J4so64slQtnwjhgoPmJgcG-6YcVAwVc,20
177
+ vastdb-0.1.6.dist-info/RECORD,,
File without changes