Skip to content

Commit 43b6f37

Browse files
committed
Enable stats on disk connector #2697
1 parent 6aa0496 commit 43b6f37

4 files changed

Lines changed: 144 additions & 60 deletions

File tree

opteryx/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__build__ = 1385
1+
__build__ = 1386
22

33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.

opteryx/connectors/capabilities/statistics.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
# See the License at http://www.apache.org/licenses/LICENSE-2.0
44
# Distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
55

6-
76
from typing import Any
87
from typing import Dict
98
from typing import Optional
109

10+
import numpy
1111
from orso.schema import RelationSchema
1212

1313
from opteryx.managers.expression import NodeType
@@ -66,6 +66,8 @@ def prune_blobs(self, blob_names: list[str], query_statistics, selection) -> lis
6666
for condition in valid_conditions:
6767
column_name = condition.left.source_column
6868
literal_value = condition.right.value
69+
if type(literal_value) is numpy.datetime64:
70+
literal_value = str(literal_value.astype("M8[ms]"))
6971
max_value = cached_stats.upper_bounds.get(column_name)
7072
min_value = cached_stats.lower_bounds.get(column_name)
7173

opteryx/connectors/disk_connector.py

Lines changed: 71 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515

1616
import pyarrow
1717
from orso.schema import RelationSchema
18+
from orso.tools import single_item_cache
1819
from orso.types import OrsoTypes
1920

2021
from opteryx.connectors.base.base_connector import BaseConnector
2122
from opteryx.connectors.capabilities import LimitPushable
2223
from opteryx.connectors.capabilities import Partitionable
2324
from opteryx.connectors.capabilities import PredicatePushable
25+
from opteryx.connectors.capabilities import Statistics
2426
from opteryx.exceptions import DataError
2527
from opteryx.exceptions import DatasetNotFoundError
2628
from opteryx.exceptions import EmptyDatasetError
@@ -46,59 +48,7 @@
4648
mmap_config["access"] = mmap.ACCESS_READ
4749

4850

49-
def read_blob(
50-
*, blob_name: str, decoder, statistics, just_schema=False, projection=None, selection=None
51-
):
52-
"""
53-
Read a blob (binary large object) from disk using memory-mapped file access.
54-
55-
This method uses low-level file reading with memory-mapped files to
56-
improve performance. It reads the entire file into memory and then
57-
decodes it using the provided decoder function.
58-
59-
Parameters:
60-
blob_name (str):
61-
The name of the blob file to read.
62-
decoder (callable):
63-
A function to decode the memory-mapped file content.
64-
just_schema (bool, optional):
65-
If True, only the schema of the data is returned. Defaults to False.
66-
projection (list, optional):
67-
A list of fields to project. Defaults to None.
68-
selection (dict, optional):
69-
A dictionary of selection criteria. Defaults to None.
70-
**kwargs:
71-
Additional keyword arguments.
72-
73-
Returns:
74-
The decoded blob content.
75-
76-
Raises:
77-
FileNotFoundError:
78-
If the blob file does not exist.
79-
OSError:
80-
If an I/O error occurs while reading the file.
81-
"""
82-
try:
83-
file_descriptor = os.open(blob_name, os.O_RDONLY | os.O_BINARY)
84-
if hasattr(os, "posix_fadvise"):
85-
os.posix_fadvise(file_descriptor, 0, 0, os.POSIX_FADV_WILLNEED)
86-
size = os.fstat(file_descriptor).st_size
87-
_map = mmap.mmap(file_descriptor, length=size, **mmap_config)
88-
result = decoder(
89-
_map,
90-
just_schema=just_schema,
91-
projection=projection,
92-
selection=selection,
93-
use_threads=True,
94-
)
95-
statistics.bytes_read += size
96-
return result
97-
finally:
98-
os.close(file_descriptor)
99-
100-
101-
class DiskConnector(BaseConnector, Partitionable, PredicatePushable, LimitPushable):
51+
class DiskConnector(BaseConnector, Partitionable, PredicatePushable, LimitPushable, Statistics):
10252
"""
10353
Connector for reading datasets from files on local storage.
10454
"""
@@ -137,13 +87,74 @@ def __init__(self, **kwargs):
13787
Partitionable.__init__(self, **kwargs)
13888
PredicatePushable.__init__(self, **kwargs)
13989
LimitPushable.__init__(self, **kwargs)
90+
Statistics.__init__(self, **kwargs)
14091

14192
self.dataset = self.dataset.replace(".", OS_SEP)
14293
self.cached_first_blob = None # Cache for the first blob in the dataset
14394
self.blob_list = {}
14495
self.rows_seen = 0
14596
self.blobs_seen = 0
14697

98+
def read_blob(
99+
self, *, blob_name: str, decoder, just_schema=False, projection=None, selection=None
100+
):
101+
"""
102+
Read a blob (binary large object) from disk using memory-mapped file access.
103+
104+
This method uses low-level file reading with memory-mapped files to
105+
improve performance. It reads the entire file into memory and then
106+
decodes it using the provided decoder function.
107+
108+
Parameters:
109+
blob_name (str):
110+
The name of the blob file to read.
111+
decoder (callable):
112+
A function to decode the memory-mapped file content.
113+
just_schema (bool, optional):
114+
If True, only the schema of the data is returned. Defaults to False.
115+
projection (list, optional):
116+
A list of fields to project. Defaults to None.
117+
selection (dict, optional):
118+
A dictionary of selection criteria. Defaults to None.
119+
**kwargs:
120+
Additional keyword arguments.
121+
122+
Returns:
123+
The decoded blob content.
124+
125+
Raises:
126+
FileNotFoundError:
127+
If the blob file does not exist.
128+
OSError:
129+
If an I/O error occurs while reading the file.
130+
"""
131+
try:
132+
file_descriptor = os.open(blob_name, os.O_RDONLY | os.O_BINARY)
133+
if hasattr(os, "posix_fadvise"):
134+
os.posix_fadvise(file_descriptor, 0, 0, os.POSIX_FADV_WILLNEED)
135+
size = os.fstat(file_descriptor).st_size
136+
_map = mmap.mmap(file_descriptor, length=size, **mmap_config)
137+
result = decoder(
138+
_map,
139+
just_schema=just_schema,
140+
projection=projection,
141+
selection=selection,
142+
use_threads=True,
143+
)
144+
self.statistics.bytes_read += size
145+
146+
if not just_schema:
147+
stats = self.read_blob_statistics(
148+
blob_name=blob_name, blob_bytes=_map, decoder=decoder
149+
)
150+
if self.relation_statistics is None:
151+
self.relation_statistics = stats
152+
153+
return result
154+
finally:
155+
os.close(file_descriptor)
156+
157+
@single_item_cache
147158
def get_list_of_blob_names(self, *, prefix: str) -> List[str]:
148159
"""
149160
List all blob files in the given directory path.
@@ -190,15 +201,19 @@ def read_dataset(
190201
prefix=self.dataset,
191202
)
192203

204+
if predicates is not None:
205+
blob_names = self.prune_blobs(
206+
blob_names=blob_names, query_statistics=self.statistics, selection=predicates
207+
)
208+
193209
remaining_rows = limit if limit is not None else float("inf")
194210

195211
for blob_name in blob_names:
196212
decoder = get_decoder(blob_name)
197213
try:
198214
if not just_schema:
199-
num_rows, _, decoded = read_blob(
215+
num_rows, _, decoded = self.read_blob(
200216
blob_name=blob_name,
201-
statistics=self.statistics,
202217
decoder=decoder,
203218
just_schema=False,
204219
projection=columns,
@@ -219,9 +234,8 @@ def read_dataset(
219234
if remaining_rows <= 0:
220235
break
221236
else:
222-
schema = read_blob(
237+
schema = self.read_blob(
223238
blob_name=blob_name,
224-
statistics=self.statistics,
225239
decoder=decoder,
226240
just_schema=True,
227241
)

opteryx/connectors/file_connector.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
dataset name in a query.
99
"""
1010

11+
import mmap
1112
import os
1213
from typing import Dict
1314
from typing import Optional
@@ -20,10 +21,77 @@
2021
from opteryx.connectors.capabilities import LimitPushable
2122
from opteryx.connectors.capabilities import PredicatePushable
2223
from opteryx.connectors.capabilities import Statistics
23-
from opteryx.connectors.disk_connector import read_blob
2424
from opteryx.exceptions import DatasetNotFoundError
25+
from opteryx.utils import is_windows
2526
from opteryx.utils.file_decoders import get_decoder
2627

28+
IS_WINDOWS = is_windows()
29+
# Define os.O_BINARY for non-Windows platforms if it's not already defined
30+
if not hasattr(os, "O_BINARY"):
31+
os.O_BINARY = 0 # Value has no effect on non-Windows platforms
32+
if not hasattr(os, "O_DIRECT"):
33+
os.O_DIRECT = 0 # Value has no effect on non-Windows platforms
34+
35+
mmap_config = {}
36+
if not IS_WINDOWS:
37+
mmap_config["flags"] = mmap.MAP_PRIVATE
38+
mmap_config["prot"] = mmap.PROT_READ
39+
else:
40+
mmap_config["access"] = mmap.ACCESS_READ
41+
42+
43+
def read_blob(
44+
*, blob_name: str, decoder, just_schema=False, statistics=None, projection=None, selection=None
45+
):
46+
"""
47+
Read a blob (binary large object) from disk using memory-mapped file access.
48+
49+
This method uses low-level file reading with memory-mapped files to
50+
improve performance. It reads the entire file into memory and then
51+
decodes it using the provided decoder function.
52+
53+
Parameters:
54+
blob_name (str):
55+
The name of the blob file to read.
56+
decoder (callable):
57+
A function to decode the memory-mapped file content.
58+
just_schema (bool, optional):
59+
If True, only the schema of the data is returned. Defaults to False.
60+
projection (list, optional):
61+
A list of fields to project. Defaults to None.
62+
selection (dict, optional):
63+
A dictionary of selection criteria. Defaults to None.
64+
**kwargs:
65+
Additional keyword arguments.
66+
67+
Returns:
68+
The decoded blob content.
69+
70+
Raises:
71+
FileNotFoundError:
72+
If the blob file does not exist.
73+
OSError:
74+
If an I/O error occurs while reading the file.
75+
"""
76+
try:
77+
file_descriptor = os.open(blob_name, os.O_RDONLY | os.O_BINARY)
78+
if hasattr(os, "posix_fadvise"):
79+
os.posix_fadvise(file_descriptor, 0, 0, os.POSIX_FADV_WILLNEED)
80+
size = os.fstat(file_descriptor).st_size
81+
_map = mmap.mmap(file_descriptor, length=size, **mmap_config)
82+
result = decoder(
83+
_map,
84+
just_schema=just_schema,
85+
projection=projection,
86+
selection=selection,
87+
use_threads=True,
88+
)
89+
statistics.bytes_read += size
90+
91+
return result
92+
finally:
93+
os.close(file_descriptor)
94+
2795

2896
class FileConnector(BaseConnector, PredicatePushable, Statistics, LimitPushable):
2997
"""

0 commit comments

Comments
 (0)