This is an automated email from the ASF dual-hosted git repository.
kszucs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/master by this push:
new 0894045 ARROW-7670: [Python][Dataset] More ergonomical API
0894045 is described below
commit 089404571bcfb66e079ec70b8984a402e0c08b04
Author: Krisztián Szűcs <[email protected]>
AuthorDate: Sun Jan 26 19:43:31 2020 +0100
ARROW-7670: [Python][Dataset] More ergonomical API
- introduced `ds.field()`, `ds.scalar()`
- overloaded comparison and binary operators
- added `Expression.isin()` and `Expression.is_valid()`
- removed ScannerBuilder in favor of direct `Scanner` instantiation using
keyword arguments
- added convenience methods for Dataset: `to_table()`, `to_batches()` and
`scan()`
- added support for string scalar
- removed `InMemoryScanTask` and `DefaultPartitioning`
Closes #6268 from kszucs/filter-expr and squashes the following commits:
2bcf0554f <Krisztián Szűcs> fix review issues
706f3cc25 <Krisztián Szűcs> add to apidocs
a54a7c806 <Krisztián Szűcs> docstring
0966b8afa <Krisztián Szűcs> flake8
a950c975b <Krisztián Szűcs> flake8
9af49ce29 <Krisztián Szűcs> revert abbreviations
0170362cf <Krisztián Szűcs> cleanup
31cd5c60b <Krisztián Szűcs> remove ScannerBuilder
b0b4cd9b8 <Krisztián Szűcs> fix rename issue
b3438dfb2 <Krisztián Szűcs> cython specific overloading
dcab6b069 <Krisztián Szűcs> rename Expression to Expr
757343c13 <Krisztián Szűcs> string scalar
9c7ee5a03 <Krisztián Szűcs> improve the filter expression ergonomics
Authored-by: Krisztián Szűcs <[email protected]>
Signed-off-by: Krisztián Szűcs <[email protected]>
---
docs/source/python/api/dataset.rst | 3 +
python/pyarrow/_dataset.pyx | 553 +++++++++++++++------------
python/pyarrow/dataset.py | 37 +-
python/pyarrow/includes/libarrow.pxd | 4 +
python/pyarrow/includes/libarrow_dataset.pxd | 16 +-
python/pyarrow/tests/test_dataset.py | 164 ++++----
6 files changed, 450 insertions(+), 327 deletions(-)
diff --git a/docs/source/python/api/dataset.rst
b/docs/source/python/api/dataset.rst
index c2d37d0..e7dea33 100644
--- a/docs/source/python/api/dataset.rst
+++ b/docs/source/python/api/dataset.rst
@@ -36,6 +36,8 @@ Factory functions
source
dataset
partitioning
+ field
+ scalar
Classes
-------
@@ -56,3 +58,4 @@ Classes
Dataset
ScannerBuilder
Scanner
+ Expression
diff --git a/python/pyarrow/_dataset.pyx b/python/pyarrow/_dataset.pyx
index 4cd0cd2..7dbc971 100644
--- a/python/pyarrow/_dataset.pyx
+++ b/python/pyarrow/_dataset.pyx
@@ -21,8 +21,11 @@
from __future__ import absolute_import
+import six
+from cpython.object cimport Py_LT, Py_EQ, Py_GT, Py_LE, Py_NE, Py_GE
from cython.operator cimport dereference as deref
+import pyarrow as pa
from pyarrow.lib cimport *
from pyarrow.includes.libarrow_dataset cimport *
from pyarrow.compat import frombytes, tobytes
@@ -95,9 +98,7 @@ cdef class Partitioning:
cdef Partitioning self
typ = frombytes(sp.get().type_name())
- if typ == 'default':
- self = DefaultPartitioning.__new__(DefaultPartitioning)
- elif typ == 'schema':
+ if typ == 'schema':
self = DirectoryPartitioning.__new__(DirectoryPartitioning)
elif typ == 'hive':
self = HivePartitioning.__new__(HivePartitioning)
@@ -146,21 +147,6 @@ cdef class PartitioningFactory:
return self.wrapped
-cdef class DefaultPartitioning(Partitioning):
-
- cdef:
- CDefaultPartitioning* default_partitioning
-
- def __init__(self):
- cdef shared_ptr[CDefaultPartitioning] partitioning
- partitioning = make_shared[CDefaultPartitioning]()
- self.init(<shared_ptr[CPartitioning]> partitioning)
-
- cdef init(self, const shared_ptr[CPartitioning]& sp):
- Partitioning.init(self, sp)
- self.default_partitioning = <CDefaultPartitioning*> sp.get()
-
-
cdef class DirectoryPartitioning(Partitioning):
"""
A Partitioning based on a specified Schema.
@@ -292,7 +278,7 @@ cdef class HivePartitioning(Partitioning):
cdef class FileSystemFactoryOptions:
"""
- Options for FileSystemFactoryOptions.
+ Influences the discovery of filesystem paths.
Parameters
----------
@@ -492,10 +478,13 @@ cdef class FileSystemSourceFactory(SourceFactory):
Parameters
----------
filesystem : pyarrow.fs.FileSystem
+ Filesystem to discover.
paths_or_selector: pyarrow.fs.Selector or list of path-likes
Either a Selector object or a list of path-like objects.
format : FileFormat
+ Currently only ParquetFileFormat is supported.
options : FileSystemFactoryOptions, optional
+ Various flags influencing the discovery of filesystem paths.
"""
cdef:
@@ -513,7 +502,6 @@ cdef class FileSystemSourceFactory(SourceFactory):
CFileSystemFactoryOptions c_options
c_filesystem = filesystem.unwrap()
-
c_format = format.unwrap()
options = options or FileSystemFactoryOptions()
@@ -589,15 +577,15 @@ cdef class Source:
@property
def partition_expression(self):
"""
- An expression which evaluates to true for all data viewed by this
+ An Expression which evaluates to true for all data viewed by this
Source.
"""
- cdef shared_ptr[CExpression] expression
- expression = self.source.partition_expression()
- if expression.get() == nullptr:
+ cdef shared_ptr[CExpression] expr
+ expr = self.source.partition_expression()
+ if expr.get() == nullptr:
return None
else:
- return Expression.wrap(expression)
+ return Expression.wrap(expr)
cdef class TreeSource(Source):
@@ -630,8 +618,7 @@ cdef class FileSystemSource(Source):
cdef:
CFileSystemSource* filesystem_source
- def __init__(self, Schema schema not None,
- Expression source_partition,
+ def __init__(self, Schema schema not None, Expression root_partition,
FileFormat file_format not None,
FileSystem filesystem not None,
paths_or_selector, partitions):
@@ -640,20 +627,22 @@ cdef class FileSystemSource(Source):
Parameters
----------
schema : Schema
- Schema for resulting Source
- source_partition : Expression
+ The top-level schema of the DataSource.
+ root_partition : Expression
+ The top-level partition of the DataSource.
file_format : FileFormat
+ File format to create fragments from, currently only
+ ParquetFileFormat is supported.
filesystem : FileSystem
- FileSystem which will be explored to discover data files.
+ The filesystem which files are from.
paths_or_selector : Union[FileSelector, List[FileStats]]
- The file stats object can be queried by the
- filesystem.get_target_stats method.
+ List of files/directories to consume.
partitions : List[Expression]
+ Attach aditional partition information for the file paths.
"""
cdef:
- shared_ptr[CExpression] c_source_partition
FileStats stats
- Expression expression
+ Expression expr
vector[CFileStats] c_file_stats
vector[shared_ptr[CExpression]] c_partitions
CResult[shared_ptr[CSource]] result
@@ -661,8 +650,8 @@ cdef class FileSystemSource(Source):
for stats in filesystem.get_target_stats(paths_or_selector):
c_file_stats.push_back(stats.unwrap())
- for expression in partitions:
- c_partitions.push_back(expression.unwrap())
+ for expr in partitions:
+ c_partitions.push_back(expr.unwrap())
if c_file_stats.size() != c_partitions.size():
raise ValueError(
@@ -670,13 +659,12 @@ cdef class FileSystemSource(Source):
'equal to the number of partitions.'
)
- if source_partition is None:
- source_partition = ScalarExpression(True)
- c_source_partition = source_partition.unwrap()
+ if root_partition is None:
+ root_partition = ScalarExpression(True)
result = CFileSystemSource.Make(
pyarrow_unwrap_schema(schema),
- c_source_partition,
+ root_partition.unwrap(),
file_format.unwrap(),
filesystem.unwrap(),
c_file_stats,
@@ -805,30 +793,120 @@ cdef class Dataset:
cdef inline shared_ptr[CDataset] unwrap(self) nogil:
return self.wrapped
- def new_scan(self, MemoryPool memory_pool=None):
+ def scan(self, columns=None, filter=None, MemoryPool memory_pool=None):
+ """Builds a scan operation against the dataset.
+
+ It poduces a stream of ScanTasks which is meant to be a unit of work to
+ be dispatched. The tasks are not executed automatically, the user is
+ responsible to execute and dispatch the individual tasks, so custom
+ local task scheduling can be implemented.
+
+ Parameters
+ ----------
+ columns : list of str, default None
+ List of columns to project. Order and duplicates will be preserved.
+ The columns will be passed down to Sources and corresponding data
+ fragments to avoid loading, copying, and deserializing columns
+ that will not be required further down the compute chain.
+ By default all of the available columns are projected. Raises
+ an exception if any of the referenced column names does not exist
+ in the dataset's Schema.
+ filter : Expression, default None
+ Scan will return only the rows matching the filter.
+ If possible the predicate will be pushed down to exploit the
+ partition information or internal metadata found in the data
+ source, e.g. Parquet statistics. Otherwise filters the loaded
+ RecordBatches before yielding them.
+ memory_pool : MemoryPool, default None
+ For memory allocations, if required. If not specified, uses the
+ default pool.
+
+ Returns
+ -------
+ scan_tasks : iterator of ScanTask
"""
- Begin to build a new Scan operation against this Dataset.
+ scanner = Scanner(self, columns=columns, filter=filter,
+ memory_pool=memory_pool)
+ return scanner.scan()
+
+ def to_batches(self, columns=None, filter=None,
+ MemoryPool memory_pool=None):
+ """Read the dataset as materialized record batches.
+
+ Builds a scan operation against the dataset and sequentially executes
+ the ScanTasks as the returned generator gets consumed.
Parameters
----------
+ columns : list of str, default None
+ List of columns to project. Order and duplicates will be preserved.
+ The columns will be passed down to Sources and corresponding data
+ fragments to avoid loading, copying, and deserializing columns
+ that will not be required further down the compute chain.
+ By default all of the available columns are projected. Raises
+ an exception if any of the referenced column names does not exist
+ in the dataset's Schema.
+ filter : Expression, default None
+ Scan will return only the rows matching the filter.
+ If possible the predicate will be pushed down to exploit the
+ partition information or internal metadata found in the data
+ source, e.g. Parquet statistics. Otherwise filters the loaded
+ RecordBatches before yielding them.
memory_pool : MemoryPool, default None
For memory allocations, if required. If not specified, uses the
default pool.
Returns
-------
- ScannerBuilder
+ record_batches : iterator of RecordBatch
"""
- cdef:
- shared_ptr[CScanContext] context = make_shared[CScanContext]()
- CResult[shared_ptr[CScannerBuilder]] result
- context.get().pool = maybe_unbox_memory_pool(memory_pool)
- result = self.dataset.NewScanWithContext(context)
- return ScannerBuilder.wrap(GetResultValue(result))
+ scanner = Scanner(self, columns=columns, filter=filter,
+ memory_pool=memory_pool)
+ for task in scanner.scan():
+ for batch in task.execute():
+ yield batch
+
+ def to_table(self, columns=None, filter=None, use_threads=True,
+ MemoryPool memory_pool=None):
+ """Read the dataset to an arrow table.
+
+ Note that this method reads all the selected data from the dataset
+ into memory.
+
+ Parameters
+ ----------
+ columns : list of str, default None
+ List of columns to project. Order and duplicates will be preserved.
+ The columns will be passed down to Sources and corresponding data
+ fragments to avoid loading, copying, and deserializing columns
+ that will not be required further down the compute chain.
+ By default all of the available columns are projected. Raises
+ an exception if any of the referenced column names does not exist
+ in the dataset's Schema.
+ filter : Expression, default None
+ Scan will return only the rows matching the filter.
+ If possible the predicate will be pushed down to exploit the
+ partition information or internal metadata found in the data
+ source, e.g. Parquet statistics. Otherwise filters the loaded
+ RecordBatches before yielding them.
+ use_threads : boolean, default True
+ If enabled, then maximum paralellism will be used determined by
+ the number of available CPU cores.
+ memory_pool : MemoryPool, default None
+ For memory allocations, if required. If not specified, uses the
+ default pool.
+
+ Returns
+ -------
+ table : Table instance
+ """
+ scanner = Scanner(self, columns=columns, filter=filter,
+ use_threads=use_threads, memory_pool=memory_pool)
+ return scanner.to_table()
@property
def sources(self):
- """List of the sources"""
+ """List of the data sources"""
cdef vector[shared_ptr[CSource]] sources = self.dataset.sources()
return [Source.wrap(source) for source in sources]
@@ -857,7 +935,7 @@ cdef class ScanTask:
@staticmethod
cdef wrap(shared_ptr[CScanTask]& sp):
- cdef InMemoryScanTask self = InMemoryScanTask.__new__(InMemoryScanTask)
+ cdef ScanTask self = ScanTask.__new__(ScanTask)
self.init(sp)
return self
@@ -878,173 +956,90 @@ cdef class ScanTask:
CRecordBatchIterator iterator
shared_ptr[CRecordBatch] record_batch
- iterator = move(GetResultValue(move(self.task.Execute())))
-
- while True:
- record_batch = GetResultValue(iterator.Next())
- if record_batch.get() == nullptr:
- raise StopIteration()
- else:
- yield pyarrow_wrap_batch(record_batch)
-
+ with nogil:
+ iterator = move(GetResultValue(move(self.task.Execute())))
-cdef class InMemoryScanTask(ScanTask):
- """A trivial ScanTask that yields the RecordBatch of an array."""
+ while True:
+ record_batch = GetResultValue(iterator.Next())
+ if record_batch.get() == nullptr:
+ raise StopIteration()
+ else:
+ with gil:
+ yield pyarrow_wrap_batch(record_batch)
- cdef:
- CInMemoryScanTask* in_memory_task
-
- cdef init(self, shared_ptr[CScanTask]& sp):
- ScanTask.init(self, sp)
- self.in_memory_task = <CInMemoryScanTask*> sp.get()
+cdef class Scanner:
+ """A materialized scan operation with context and options bound.
-cdef class ScannerBuilder:
- """Factory class to construct a Scanner.
-
- It is used to pass information, notably a potential filter expression and a
- subset of columns to materialize.
+ A scanner is the class that glues the scan tasks, data fragments and data
+ sources together.
Parameters
----------
dataset : Dataset
- The dataset to scan.
+ Dataset to scan.
+ columns : list of str, default None
+ List of columns to project. Order and duplicates will be preserved.
+ The columns will be passed down to Sources and corresponding data
+ fragments to avoid loading, copying, and deserializing columns
+ that will not be required further down the compute chain.
+ By default all of the available columns are projected. Raises
+ an exception if any of the referenced column names does not exist
+ in the dataset's Schema.
+ filter : Expression, default None
+ Scan will return only the rows matching the filter.
+ If possible the predicate will be pushed down to exploit the
+ partition information or internal metadata found in the data
+ source, e.g. Parquet statistics. Otherwise filters the loaded
+ RecordBatches before yielding them.
+ use_threads : boolean, default True
+ If enabled, then maximum paralellism will be used determined by
+ the number of available CPU cores.
memory_pool : MemoryPool, default None
For memory allocations, if required. If not specified, uses the
default pool.
"""
cdef:
- shared_ptr[CScannerBuilder] wrapped
- CScannerBuilder* builder
+ shared_ptr[CScanner] wrapped
+ CScanner* scanner
- def __init__(self, Dataset dataset not None, MemoryPool memory_pool=None):
+ def __init__(self, Dataset dataset, list columns=None,
+ Expression filter=None, bint use_threads=True,
+ MemoryPool memory_pool=None):
cdef:
+ shared_ptr[CScanContext] context
shared_ptr[CScannerBuilder] builder
- shared_ptr[CScanContext] context = make_shared[CScanContext]()
- context.get().pool = maybe_unbox_memory_pool(memory_pool)
- builder = make_shared[CScannerBuilder](dataset.unwrap(), context)
- self.init(builder)
-
- cdef void init(self, shared_ptr[CScannerBuilder]& sp):
- self.wrapped = sp
- self.builder = sp.get()
-
- @staticmethod
- cdef wrap(shared_ptr[CScannerBuilder]& sp):
- cdef ScannerBuilder self = ScannerBuilder.__new__(ScannerBuilder)
- self.init(sp)
- return self
-
- cdef inline shared_ptr[CScannerBuilder] unwrap(self) nogil:
- return self.wrapped
-
- def project(self, columns):
- """Set the subset of columns to materialize.
-
- This subset will be passed down to Sources and corresponding
- data fragments. The goal is to avoid loading, copying, and
- deserializing columns that will not be required further down the
- compute chain.
-
- It alters the object in place and returns the object itself enabling
- method chaining. Raises exception if any of the referenced column names
- does not exists in the dataset's Schema.
-
- Parameters
- ----------
- columns : list of str
- List of columns to project. Order and duplicates will be preserved.
+ shared_ptr[CExpression] filter_expression
+ vector[c_string] columns_to_project
- Returns
- -------
- self : ScannerBuilder
- """
- cdef vector[c_string] cols = [tobytes(c) for c in columns]
- check_status(self.builder.Project(cols))
-
- return self
-
- def finish(self):
- """Return the constructed now-immutable Scanner object
-
- Returns
- -------
- Scanner
- """
- return Scanner.wrap(GetResultValue(self.builder.Finish()))
-
- def filter(self, Expression filter_expression not None):
- """Set the filter expression to return only rows matching the filter.
-
- The predicate will be passed down to Sources and corresponding
- data fragments to exploit predicate pushdown if possible using
- partition information or internal metadata, e.g. Parquet statistics.
- Otherwise filters the loaded RecordBatches before yielding them.
-
- It alters the object in place and returns the object itself enabling
- method chaining. Raises exception if any of the referenced column names
- does not exists in the dataset's Schema.
-
- Parameters
- ----------
- filter_expression : Expression
- Boolean expression to filter rows with.
-
- Returns
- -------
- self : ScannerBuilder
- """
- cdef:
- shared_ptr[CExpression] c_casting_filter_expression
+ # create scan context
+ context = make_shared[CScanContext]()
+ context.get().pool = maybe_unbox_memory_pool(memory_pool)
- c_casting_filter_expression = GetResultValue(
- CInsertImplicitCasts(
- deref(filter_expression.unwrap().get()),
- deref(self.builder.schema().get())
- )
+ # create scanner builder
+ builder = GetResultValue(
+ dataset.unwrap().get().NewScanWithContext(context)
)
- check_status(self.builder.Filter(c_casting_filter_expression))
- return self
-
- def use_threads(self, bint value):
- """Set whether the Scanner should make use of the thread pool.
-
- It alters the object in place and returns with the object itself
- enabling method chaining.
-
- Parameters
- ----------
- value : boolean
-
- Returns
- -------
- self : ScannerBuilder
- """
- check_status(self.builder.UseThreads(value))
- return self
-
- @property
- def schema(self):
- return pyarrow_wrap_schema(self.builder.schema())
-
-cdef class Scanner:
- """A materialized scan operation with context and options bound.
-
- Create this using the ScannerBuilder factory class.
-
- A scanner is the class that glues the scan tasks, data fragments and data
- sources together.
- """
-
- cdef:
- shared_ptr[CScanner] wrapped
- CScanner* scanner
+ # set the builder's properties
+ if columns is not None:
+ columns_to_project = [tobytes(c) for c in columns]
+ check_status(builder.get().Project(columns_to_project))
+ if filter is not None:
+ filter_expression = GetResultValue(
+ CInsertImplicitCasts(
+ deref(filter.unwrap().get()),
+ deref(builder.get().schema().get())
+ )
+ )
+ check_status(builder.get().Filter(filter_expression))
+ if use_threads is not None:
+ check_status(builder.get().UseThreads(use_threads))
- def __init__(self):
- raise TypeError('Scanner cannot be initialized directly, use '
- 'ScannerBuilder instead')
+ # instantiate the scanner object
+ scanner = GetResultValue(builder.get().Finish())
+ self.init(scanner)
cdef void init(self, shared_ptr[CScanner]& sp):
self.wrapped = sp
@@ -1097,18 +1092,41 @@ cdef class Scanner:
return pyarrow_wrap_table(GetResultValue(result))
+def _binop(fn, left, right):
+ # cython doesn't support reverse operands like __radd__ just passes the
+ # arguments in the same order as the binary operator called
+
+ if isinstance(left, Expression) and isinstance(right, Expression):
+ pass
+ elif isinstance(left, Expression):
+ try:
+ right = ScalarExpression(right)
+ except TypeError:
+ return NotImplemented
+
+ elif isinstance(right, Expression):
+ try:
+ left = ScalarExpression(left)
+ except TypeError:
+ return NotImplemented
+ else:
+ raise TypeError('Neither left nor right arguments are Expressions')
+
+ return fn(left, right)
+
+
cdef class Expression:
cdef:
shared_ptr[CExpression] wrapped
- CExpression* expression
+ CExpression* expr
def __init__(self):
_forbid_instantiation(self.__class__)
cdef void init(self, const shared_ptr[CExpression]& sp):
self.wrapped = sp
- self.expression = sp.get()
+ self.expr = sp.get()
@staticmethod
cdef wrap(const shared_ptr[CExpression]& sp):
@@ -1143,21 +1161,76 @@ cdef class Expression:
return self.wrapped
def equals(self, Expression other):
- return self.expression.Equals(other.unwrap())
+ return self.expr.Equals(other.unwrap())
def __str__(self):
- return frombytes(self.expression.ToString())
+ return frombytes(self.expr.ToString())
def validate(self, Schema schema not None):
+ """Validate this expression for execution against a schema.
+
+ This will check that all reference fields are present (fields not in
+ the schema will be replaced with null) and all subexpressions are
+ executable. Returns the type to which this expression will evaluate.
+
+ Parameters
+ ----------
+ schema : Schema
+ Schema to execute the expression on.
+
+ Returns
+ -------
+ type : DataType
+ """
cdef:
shared_ptr[CSchema] sp_schema
CResult[shared_ptr[CDataType]] result
sp_schema = pyarrow_unwrap_schema(schema)
- result = self.expression.Validate(deref(sp_schema))
+ result = self.expr.Validate(deref(sp_schema))
return pyarrow_wrap_data_type(GetResultValue(result))
def assume(self, Expression given):
- return Expression.wrap(self.expression.Assume(given.unwrap()))
+ """Simplify to an equivalent Expression given assumed constraints."""
+ return Expression.wrap(self.expr.Assume(given.unwrap()))
+
+ def __invert__(self):
+ return NotExpression(self)
+
+ def __richcmp__(self, other, int op):
+ operator_mapping = {
+ Py_EQ: CompareOperator.Equal,
+ Py_NE: CompareOperator.NotEqual,
+ Py_GT: CompareOperator.Greater,
+ Py_GE: CompareOperator.GreaterEqual,
+ Py_LT: CompareOperator.Less,
+ Py_LE: CompareOperator.LessEqual
+ }
+
+ if not isinstance(other, Expression):
+ try:
+ other = ScalarExpression(other)
+ except TypeError:
+ return NotImplemented
+
+ return ComparisonExpression(operator_mapping[op], self, other)
+
+ def __and__(self, other):
+ return _binop(AndExpression, self, other)
+
+ def __or__(self, other):
+ return _binop(OrExpression, self, other)
+
+ def is_valid(self):
+ """Checks whether the expression is not-null (valid)"""
+ return IsValidExpression(self)
+
+ def cast(self, type, bint safe=True):
+ """Explicitly change the expression's data type"""
+ return CastExpression(self, to=ensure_type(type), safe=safe)
+
+ def isin(self, values):
+ """Checks whether the expression is contained in values"""
+ return InExpression(self, pa.array(values))
cdef class UnaryExpression(Expression):
@@ -1193,7 +1266,7 @@ cdef class ScalarExpression(Expression):
def __init__(self, value):
cdef:
shared_ptr[CScalar] scalar
- shared_ptr[CScalarExpression] expression
+ shared_ptr[CScalarExpression] expr
if isinstance(value, bool):
scalar = MakeScalar(<c_bool>value)
@@ -1201,21 +1274,18 @@ cdef class ScalarExpression(Expression):
scalar = MakeScalar(<double>value)
elif isinstance(value, int):
scalar = MakeScalar(<int64_t>value)
+ elif isinstance(value, six.string_types):
+ scalar = MakeStringScalar(tobytes(value))
else:
raise TypeError('Not yet supported scalar value: {}'.format(value))
- expression.reset(new CScalarExpression(scalar))
- self.init(<shared_ptr[CExpression]> expression)
+ expr.reset(new CScalarExpression(scalar))
+ self.init(<shared_ptr[CExpression]> expr)
cdef void init(self, const shared_ptr[CExpression]& sp):
Expression.init(self, sp)
self.scalar = <CScalarExpression*> sp.get()
- # TODO(kszucs): implement once we have proper Scalar bindings
- # @property
- # def value(self):
- # return pyarrow_wrap_scalar(self.scalar.value())
-
cdef class FieldExpression(Expression):
@@ -1224,9 +1294,9 @@ cdef class FieldExpression(Expression):
def __init__(self, name):
cdef:
c_string field_name = tobytes(name)
- shared_ptr[CExpression] expression
- expression.reset(new CFieldExpression(field_name))
- self.init(expression)
+ shared_ptr[CExpression] expr
+ expr.reset(new CFieldExpression(field_name))
+ self.init(expr)
cdef void init(self, const shared_ptr[CExpression]& sp):
Expression.init(self, sp)
@@ -1249,18 +1319,17 @@ cdef class ComparisonExpression(BinaryExpression):
cdef CComparisonExpression* comparison
- def __init__(self, CompareOperator op,
- Expression left_operand not None,
- Expression right_operand not None):
- cdef shared_ptr[CComparisonExpression] expression
- expression.reset(
+ def __init__(self, CompareOperator op, Expression left not None,
+ Expression right not None):
+ cdef shared_ptr[CComparisonExpression] expr
+ expr.reset(
new CComparisonExpression(
<CCompareOperator>op,
- left_operand.unwrap(),
- right_operand.unwrap()
+ left.unwrap(),
+ right.unwrap()
)
)
- self.init(<shared_ptr[CExpression]> expression)
+ self.init(<shared_ptr[CExpression]> expr)
cdef void init(self, const shared_ptr[CExpression]& sp):
BinaryExpression.init(self, sp)
@@ -1273,9 +1342,9 @@ cdef class ComparisonExpression(BinaryExpression):
cdef class IsValidExpression(UnaryExpression):
def __init__(self, Expression operand not None):
- cdef shared_ptr[CIsValidExpression] expression
- expression = make_shared[CIsValidExpression](operand.unwrap())
- self.init(<shared_ptr[CExpression]> expression)
+ cdef shared_ptr[CIsValidExpression] expr
+ expr = make_shared[CIsValidExpression](operand.unwrap())
+ self.init(<shared_ptr[CExpression]> expr)
cdef class CastExpression(UnaryExpression):
@@ -1286,59 +1355,57 @@ cdef class CastExpression(UnaryExpression):
# term we should expose the CastOptions object
cdef:
CastOptions options
- shared_ptr[CExpression] expression
+ shared_ptr[CExpression] expr
options = CastOptions.safe() if safe else CastOptions.unsafe()
- expression.reset(new CCastExpression(
+ expr.reset(new CCastExpression(
operand.unwrap(),
pyarrow_unwrap_data_type(to),
options.unwrap()
))
- self.init(expression)
+ self.init(expr)
cdef class InExpression(UnaryExpression):
def __init__(self, Expression operand not None, Array haystack not None):
- cdef shared_ptr[CExpression] expression
- expression.reset(
+ cdef shared_ptr[CExpression] expr
+ expr.reset(
new CInExpression(operand.unwrap(), pyarrow_unwrap_array(haystack))
)
- self.init(expression)
+ self.init(expr)
cdef class NotExpression(UnaryExpression):
def __init__(self, Expression operand not None):
- cdef shared_ptr[CNotExpression] expression
- expression = MakeNotExpression(operand.unwrap())
- self.init(<shared_ptr[CExpression]> expression)
+ cdef shared_ptr[CNotExpression] expr
+ expr = CMakeNotExpression(operand.unwrap())
+ self.init(<shared_ptr[CExpression]> expr)
cdef class AndExpression(BinaryExpression):
- def __init__(self, Expression left_operand not None,
- Expression right_operand not None,
+ def __init__(self, Expression left not None, Expression right not None,
*additional_operands):
cdef:
Expression operand
vector[shared_ptr[CExpression]] exprs
- exprs.push_back(left_operand.unwrap())
- exprs.push_back(right_operand.unwrap())
+ exprs.push_back(left.unwrap())
+ exprs.push_back(right.unwrap())
for operand in additional_operands:
exprs.push_back(operand.unwrap())
- self.init(MakeAndExpression(exprs))
+ self.init(CMakeAndExpression(exprs))
cdef class OrExpression(BinaryExpression):
- def __init__(self, Expression left_operand not None,
- Expression right_operand not None,
+ def __init__(self, Expression left not None, Expression right not None,
*additional_operands):
cdef:
Expression operand
vector[shared_ptr[CExpression]] exprs
- exprs.push_back(left_operand.unwrap())
- exprs.push_back(right_operand.unwrap())
+ exprs.push_back(left.unwrap())
+ exprs.push_back(right.unwrap())
for operand in additional_operands:
exprs.push_back(operand.unwrap())
- self.init(MakeOrExpression(exprs))
+ self.init(CMakeOrExpression(exprs))
diff --git a/python/pyarrow/dataset.py b/python/pyarrow/dataset.py
index 2d8a458..1aa0dfb 100644
--- a/python/pyarrow/dataset.py
+++ b/python/pyarrow/dataset.py
@@ -34,7 +34,6 @@ from pyarrow._dataset import ( # noqa
ComparisonExpression,
Dataset,
DatasetFactory,
- DefaultPartitioning,
DirectoryPartitioning,
Expression,
FieldExpression,
@@ -52,7 +51,6 @@ from pyarrow._dataset import ( # noqa
PartitioningFactory,
ScalarExpression,
Scanner,
- ScannerBuilder,
ScanTask,
Source,
TreeSource,
@@ -332,5 +330,38 @@ def dataset(sources, filesystem=None, partitioning=None,
format=None):
format=format)
for src in sources
]
-
return DatasetFactory(sources).finish()
+
+
+def field(name):
+ """References a named column of the dataset.
+
+ Stores only the field's name. Type and other information is known only when
+ the expression is applied on a dataset having an explicit scheme.
+
+ Parameters
+ ----------
+ name : string
+ The name of the field the expression references to.
+
+ Returns
+ -------
+ field_expr : FieldExpression
+ """
+ return FieldExpression(name)
+
+
+def scalar(value):
+ """Expression representing a scalar value.
+
+ Parameters
+ ----------
+ value : bool, int, float or string
+ Python value of the scalar. Note that only a subset of types are
+ currently supported.
+
+ Returns
+ -------
+ scalar_expr : ScalarExpression
+ """
+ return ScalarExpression(value)
diff --git a/python/pyarrow/includes/libarrow.pxd
b/python/pyarrow/includes/libarrow.pxd
index d4cd5f2..7212896 100644
--- a/python/pyarrow/includes/libarrow.pxd
+++ b/python/pyarrow/includes/libarrow.pxd
@@ -800,7 +800,11 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
cdef cppclass CDoubleScalar" arrow::DoubleScalar"(CScalar):
double value
+ cdef cppclass CStringScalar" arrow::StringScalar"(CScalar):
+ pass
+
shared_ptr[CScalar] MakeScalar[Value](Value value)
+ shared_ptr[CScalar] MakeStringScalar" arrow::MakeScalar"(c_string value)
cdef cppclass CConcatenateTablesOptions" arrow::ConcatenateTablesOptions":
c_bool unify_schemas
diff --git a/python/pyarrow/includes/libarrow_dataset.pxd
b/python/pyarrow/includes/libarrow_dataset.pxd
index 0c07aaa..10e3793 100644
--- a/python/pyarrow/includes/libarrow_dataset.pxd
+++ b/python/pyarrow/includes/libarrow_dataset.pxd
@@ -120,11 +120,11 @@ cdef extern from "arrow/dataset/api.h" namespace
"arrow::dataset" nogil:
CUnaryExpression):
CInExpression(shared_ptr[CExpression] operand, shared_ptr[CArray] set)
- cdef shared_ptr[CNotExpression] MakeNotExpression "arrow::dataset::not_"(
+ cdef shared_ptr[CNotExpression] CMakeNotExpression "arrow::dataset::not_"(
shared_ptr[CExpression] operand)
- cdef shared_ptr[CExpression] MakeAndExpression "arrow::dataset::and_"(
+ cdef shared_ptr[CExpression] CMakeAndExpression "arrow::dataset::and_"(
const CExpressionVector& subexpressions)
- cdef shared_ptr[CExpression] MakeOrExpression "arrow::dataset::or_"(
+ cdef shared_ptr[CExpression] CMakeOrExpression "arrow::dataset::or_"(
const CExpressionVector& subexpressions)
cdef CResult[shared_ptr[CExpression]] CInsertImplicitCasts \
@@ -141,8 +141,6 @@ cdef extern from "arrow/dataset/api.h" namespace
"arrow::dataset" nogil:
shared_ptr[CExpression] filter
shared_ptr[CSchema] schema
c_bool use_threads
- # shared_ptr[CExpressionEvaluator] evaluator
- # shared_ptr[CRecordBatchProjector] projector
@staticmethod
shared_ptr[CScanOptions] Defaults()
@@ -153,10 +151,6 @@ cdef extern from "arrow/dataset/api.h" namespace
"arrow::dataset" nogil:
cdef cppclass CScanTask" arrow::dataset::ScanTask":
CResult[CRecordBatchIterator] Execute()
- cdef cppclass CInMemoryScanTask "arrow::dataset::InMemoryScanTask"(
- CScanTask):
- pass
-
ctypedef CIterator[shared_ptr[CScanTask]] CScanTaskIterator \
"arrow::dataset::ScanTaskIterator"
@@ -311,10 +305,6 @@ cdef extern from "arrow/dataset/api.h" namespace
"arrow::dataset" nogil:
cdef cppclass CPartitioningFactory "arrow::dataset::PartitioningFactory":
pass
- cdef cppclass CDefaultPartitioning \
- "arrow::dataset::DefaultPartitioning"(CPartitioning):
- CDefaultPartitioning()
-
cdef cppclass CDirectoryPartitioning \
"arrow::dataset::DirectoryPartitioning"(CPartitioning):
CDirectoryPartitioning(shared_ptr[CSchema] schema)
diff --git a/python/pyarrow/tests/test_dataset.py
b/python/pyarrow/tests/test_dataset.py
index fe3eb22..d5f40cb 100644
--- a/python/pyarrow/tests/test_dataset.py
+++ b/python/pyarrow/tests/test_dataset.py
@@ -16,9 +16,9 @@
# under the License.
import sys
+import operator
import numpy as np
-
import pytest
import pyarrow as pa
@@ -175,13 +175,13 @@ def test_filesystem_source(mockfs):
partitions = [ds.ScalarExpression(True), ds.ScalarExpression(True)]
source = ds.FileSystemSource(schema,
- source_partition=None,
+ root_partition=None,
file_format=file_format,
filesystem=mockfs,
paths_or_selector=paths,
partitions=partitions)
- source_partition = ds.ComparisonExpression(
+ root_partition = ds.ComparisonExpression(
ds.CompareOperator.Equal,
ds.FieldExpression('source'),
ds.ScalarExpression(1337)
@@ -199,42 +199,35 @@ def test_filesystem_source(mockfs):
)
]
source = ds.FileSystemSource(paths_or_selector=paths, schema=schema,
- source_partition=source_partition,
+ root_partition=root_partition,
filesystem=mockfs, partitions=partitions,
file_format=file_format)
- assert source.partition_expression.equals(source_partition)
+ assert source.partition_expression.equals(root_partition)
def test_dataset(dataset):
assert isinstance(dataset, ds.Dataset)
assert isinstance(dataset.schema, pa.Schema)
- # TODO(kszucs): test non-boolean expressions for filter do raise
- builder = dataset.new_scan()
- assert isinstance(builder, ds.ScannerBuilder)
-
- scanner = builder.finish()
- assert isinstance(scanner, ds.Scanner)
- assert len(list(scanner.scan())) == 2
+ # TODO(kszucs): test non-boolean Exprs for filter do raise
expected_i64 = pa.array([0, 1, 2, 3, 4], type=pa.int64())
expected_f64 = pa.array([0, 1, 2, 3, 4], type=pa.float64())
- for task in scanner.scan():
+ for task in dataset.scan():
assert isinstance(task, ds.ScanTask)
for batch in task.execute():
assert batch.column(0).equals(expected_i64)
assert batch.column(1).equals(expected_f64)
- table = scanner.to_table()
+ batches = dataset.to_batches()
+ assert all(isinstance(batch, pa.RecordBatch) for batch in batches)
+
+ table = dataset.to_table()
assert isinstance(table, pa.Table)
assert len(table) == 10
- condition = ds.ComparisonExpression(
- ds.CompareOperator.Equal,
- ds.FieldExpression('i64'),
- ds.ScalarExpression(1)
- )
- scanner = dataset.new_scan().use_threads(True).filter(condition).finish()
+ condition = ds.field('i64') == 1
+ scanner = ds.Scanner(dataset, use_threads=True, filter=condition)
result = scanner.to_table().to_pydict()
# don't rely on the scanning order
@@ -244,17 +237,16 @@ def test_dataset(dataset):
assert sorted(result['key']) == ['xxx', 'yyy']
-def test_scanner_builder(dataset):
- builder = ds.ScannerBuilder(dataset, memory_pool=pa.default_memory_pool())
- scanner = builder.finish()
+def test_scanner(dataset):
+ scanner = ds.Scanner(dataset, memory_pool=pa.default_memory_pool())
assert isinstance(scanner, ds.Scanner)
assert len(list(scanner.scan())) == 2
with pytest.raises(pa.ArrowInvalid):
- dataset.new_scan().project(['unknown'])
+ dataset.scan(columns=['unknown'])
- builder = dataset.new_scan(memory_pool=pa.default_memory_pool())
- scanner = builder.project(['i64']).finish()
+ scanner = ds.Scanner(dataset, columns=['i64'],
+ memory_pool=pa.default_memory_pool())
assert isinstance(scanner, ds.Scanner)
assert len(list(scanner.scan())) == 2
@@ -294,18 +286,7 @@ def test_partitioning():
expr = partitioning.parse('/3/3.14')
assert isinstance(expr, ds.Expression)
- expected = ds.AndExpression(
- ds.ComparisonExpression(
- ds.CompareOperator.Equal,
- ds.FieldExpression('group'),
- ds.ScalarExpression(3)
- ),
- ds.ComparisonExpression(
- ds.CompareOperator.Equal,
- ds.FieldExpression('key'),
- ds.ScalarExpression(3.14)
- )
- )
+ expected = (ds.field('group') == 3) & (ds.field('key') == 3.14)
assert expr.equals(expected)
with pytest.raises(pa.ArrowInvalid):
@@ -318,17 +299,9 @@ def test_partitioning():
])
)
expr = partitioning.parse('/alpha=0/beta=3')
- expected = ds.AndExpression(
- ds.ComparisonExpression(
- ds.CompareOperator.Equal,
- ds.FieldExpression('alpha'),
- ds.ScalarExpression(0)
- ),
- ds.ComparisonExpression(
- ds.CompareOperator.Equal,
- ds.FieldExpression('beta'),
- ds.ScalarExpression(3)
- )
+ expected = (
+ (ds.field('alpha') == ds.scalar(0)) &
+ (ds.field('beta') == ds.scalar(3))
)
assert expr.equals(expected)
@@ -337,6 +310,7 @@ def test_expression():
a = ds.ScalarExpression(1)
b = ds.ScalarExpression(1.1)
c = ds.ScalarExpression(True)
+ d = ds.ScalarExpression("string")
equal = ds.ComparisonExpression(ds.CompareOperator.Equal, a, b)
assert equal.op() == ds.CompareOperator.Equal
@@ -349,7 +323,7 @@ def test_expression():
ds.AndExpression(a, b, c)
ds.OrExpression(a, b)
- ds.OrExpression(a, b, c)
+ ds.OrExpression(a, b, c, d)
ds.NotExpression(ds.OrExpression(a, b, c))
ds.IsValidExpression(a)
ds.CastExpression(a, pa.int32())
@@ -382,6 +356,67 @@ def test_expression():
assert str(condition) == "(i64 > 5:int64)"
+def test_expression_ergonomics():
+ zero = ds.scalar(0)
+ one = ds.scalar(1)
+ true = ds.scalar(True)
+ false = ds.scalar(False)
+ string = ds.scalar("string")
+ field = ds.field("field")
+
+ assert one.equals(ds.ScalarExpression(1))
+ assert zero.equals(ds.ScalarExpression(0))
+ assert true.equals(ds.ScalarExpression(True))
+ assert false.equals(ds.ScalarExpression(False))
+ assert string.equals(ds.ScalarExpression("string"))
+ assert field.equals(ds.FieldExpression("field"))
+
+ expected = ds.AndExpression(ds.ScalarExpression(1), ds.ScalarExpression(0))
+ for expr in [one & zero, 1 & zero, one & 0]:
+ assert expr.equals(expected)
+
+ expected = ds.OrExpression(ds.ScalarExpression(1), ds.ScalarExpression(0))
+ for expr in [one | zero, 1 | zero, one | 0]:
+ assert expr.equals(expected)
+
+ comparison_ops = [
+ (operator.eq, ds.CompareOperator.Equal),
+ (operator.ne, ds.CompareOperator.NotEqual),
+ (operator.ge, ds.CompareOperator.GreaterEqual),
+ (operator.le, ds.CompareOperator.LessEqual),
+ (operator.lt, ds.CompareOperator.Less),
+ (operator.gt, ds.CompareOperator.Greater),
+ ]
+ for op, compare_op in comparison_ops:
+ expr = op(zero, one)
+ expected = ds.ComparisonExpression(compare_op, zero, one)
+ assert expr.equals(expected)
+
+ expr = ~true == false
+ expected = ds.ComparisonExpression(
+ ds.CompareOperator.Equal,
+ ds.NotExpression(ds.ScalarExpression(True)),
+ ds.ScalarExpression(False)
+ )
+ assert expr.equals(expected)
+
+ for typ in ("bool", pa.bool_()):
+ expr = field.cast(typ) == true
+ expected = ds.ComparisonExpression(
+ ds.CompareOperator.Equal,
+ ds.CastExpression(ds.FieldExpression("field"), pa.bool_()),
+ ds.ScalarExpression(True)
+ )
+ assert expr.equals(expected)
+
+ expr = field.isin([1, 2])
+ expected = ds.InExpression(ds.FieldExpression("field"), pa.array([1, 2]))
+ assert expr.equals(expected)
+
+ with pytest.raises(TypeError):
+ field.isin(1)
+
+
@pytest.mark.parametrize('paths_or_selector', [
fs.FileSelector('subdir', recursive=True),
[
@@ -423,10 +458,9 @@ def test_file_system_factory(mockfs, paths_or_selector):
assert isinstance(source, ds.Source)
dataset = ds.Dataset([source], inspected_schema)
+ assert len(list(dataset.scan())) == 2
- scanner = dataset.new_scan().finish()
- assert len(list(scanner.scan())) == 2
-
+ scanner = ds.Scanner(dataset)
expected_i64 = pa.array([0, 1, 2, 3, 4], type=pa.int64())
expected_f64 = pa.array([0, 1, 2, 3, 4], type=pa.float64())
for task, group, key in zip(scanner.scan(), [1, 2], ['xxx', 'yyy']):
@@ -439,7 +473,7 @@ def test_file_system_factory(mockfs, paths_or_selector):
assert batch[2].equals(expected_group_column)
assert batch[3].equals(expected_key_column)
- table = scanner.to_table()
+ table = dataset.to_table()
assert isinstance(table, pa.Table)
assert len(table) == 10
assert table.num_columns == 4
@@ -533,19 +567,19 @@ def _check_dataset_from_path(path, table, **kwargs):
assert isinstance(path, pathlib.Path)
dataset = ds.dataset(ds.source(path, **kwargs))
assert dataset.schema.equals(table.schema, check_metadata=False)
- result = dataset.new_scan().finish().to_table()
+ result = dataset.to_table()
assert result.replace_schema_metadata().equals(table)
# string path
dataset = ds.dataset(ds.source(str(path), **kwargs))
assert dataset.schema.equals(table.schema, check_metadata=False)
- result = dataset.new_scan().finish().to_table()
+ result = dataset.to_table()
assert result.replace_schema_metadata().equals(table)
# passing directly to dataset
dataset = ds.dataset(str(path), **kwargs)
assert dataset.schema.equals(table.schema, check_metadata=False)
- result = dataset.new_scan().finish().to_table()
+ result = dataset.to_table()
assert result.replace_schema_metadata().equals(table)
@@ -573,7 +607,7 @@ def test_open_dataset_list_of_files(tempdir):
ds.dataset(ds.source([path1, path2])),
ds.dataset(ds.source([str(path1), str(path2)]))]:
assert dataset.schema.equals(table.schema, check_metadata=False)
- result = dataset.new_scan().finish().to_table()
+ result = dataset.to_table()
assert result.replace_schema_metadata().equals(table)
@@ -609,7 +643,7 @@ def test_open_dataset_partitioned_directory(tempdir):
expected_schema = table.schema.append(pa.field("part", pa.int8()))
assert dataset.schema.equals(expected_schema, check_metadata=False)
- result = dataset.new_scan().finish().to_table()
+ result = dataset.to_table()
expected = full_table.append_column(
"part", pa.array(np.repeat([0, 1, 2], 9), type=pa.int8()))
assert result.replace_schema_metadata().equals(expected)
@@ -662,15 +696,9 @@ def test_filter_implicit_cast(tempdir):
_, path = _create_single_file(tempdir, table)
dataset = ds.dataset(str(path))
- filter_ = ds.ComparisonExpression(
- ds.CompareOperator.Greater,
- ds.FieldExpression('a'),
- ds.ScalarExpression(2)
- )
-
- scanner_builder = dataset.new_scan()
- scanner_builder.filter(filter_)
- result = scanner_builder.finish().to_table()
+ filter_ = ds.field('a') > 2
+ scanner = ds.Scanner(dataset, filter=filter_)
+ result = scanner.to_table()
assert len(result) == 3