blob: c909ae1bd7f754ceed9eccb4da9d3b6341508239 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Prepared data and dedicated files use Java DataFileMeta's TIMESTAMP(3)."""
from datetime import datetime, timedelta
from unittest.mock import patch
import pyarrow as pa
import pyarrow.parquet as pq
import pytest
from pypaimon import CatalogFactory, Schema
from pypaimon.data.timestamp import Timestamp
from pypaimon.manifest.manifest_file_manager import ManifestFileManager
from pypaimon.manifest.manifest_list_manager import ManifestListManager
from pypaimon.write.write_dispatch import NativeTableWrite
pytestmark = [pytest.mark.python_write, pytest.mark.python_commit]
@pytest.mark.parametrize('stream', [False, True], ids=['batch', 'stream'])
@pytest.mark.parametrize('mode', ['append', 'pk-input', 'postpone', 'blob', 'vector'])
def test_prepared_file_creation_times_have_millisecond_precision(tmp_path, stream, mode):
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
schema = pa.schema([('id', pa.int32()), ('value', pa.int32())])
rows = [dict(id=1, value=10), dict(id=2, value=20)]
options = {'file.format': 'parquet', 'write.native.enabled': 'false',
'commit.native.enabled': 'false', 'read.native.enabled': 'false',
'scan.native-plan.enabled': 'false'}
if mode in ('pk-input', 'postpone'):
options.update({'bucket': '-2' if mode == 'postpone' else '1',
'changelog-producer': 'input'})
if mode in ('blob', 'vector'):
options.update({'data-evolution.enabled': 'true', 'row-tracking.enabled': 'true'})
if mode == 'blob':
schema = schema.append(pa.field('payload', pa.large_binary()))
for row in rows:
row['payload'] = ('blob-%d' % row['id']).encode()
else:
options['vector.file.format'] = 'parquet'
schema = schema.append(pa.field('embedding', pa.list_(pa.float32(), 2)))
for row in rows:
row['embedding'] = [float(row['id']), 2.0]
catalog.create_table('default.t', Schema.from_pyarrow_schema(
schema, primary_keys=['id'] if mode in ('pk-input', 'postpone') else [], options=options), False)
table = catalog.get_table('default.t')
builder = table.new_stream_write_builder() if stream else table.new_batch_write_builder()
writer, commit = builder.new_write(), builder.new_commit()
assert not isinstance(writer, NativeTableWrite)
now = Timestamp.from_local_date_time(datetime(2024, 6, 1, 12, 30, 40, 123456))
expected = now.to_millis_timestamp()
try:
with patch.object(Timestamp, 'now', return_value=now):
writer.write_arrow(pa.Table.from_pylist(rows, schema=schema))
messages = writer.prepare_commit(1) if stream else writer.prepare_commit()
files = [file for message in messages for file in message.new_files + message.changelog_files]
assert files
assert all(file.creation_time.get_nano_of_millisecond() == 0 for file in files)
assert all(file.creation_time == expected for file in files)
assert now.get_nano_of_millisecond() == 456000
if mode == 'blob':
assert any(file.file_name.endswith('.blob') for file in files)
assert any(file.file_name.endswith('.parquet') for file in files)
if mode == 'vector':
assert any('.vector.parquet' in file.file_name for file in files)
if mode == 'pk-input':
assert any(message.changelog_files for message in messages)
if mode == 'postpone':
assert all(message.bucket == -2 and not message.changelog_files for message in messages)
# Pending events are invisible to normal scans until assigned real
# buckets; verify their physical Parquet rows before publication.
with table.file_io.new_input_stream(files[0].file_path) as stream_:
physical = pq.ParquetFile(stream_).read().select(schema.names).to_pylist()
assert physical == rows
commit.commit(messages, 1) if stream else commit.commit(messages)
finally:
writer.close()
commit.close()
snapshot = table.snapshot_manager().get_latest_snapshot()
entries = ManifestFileManager(table).read_entries_parallel(ManifestListManager(table).read_all(snapshot))
assert entries and all(entry.file.creation_time == expected for entry in entries)
read = table.new_read_builder()
actual = read.new_read().to_arrow(read.new_scan().plan().splits()).sort_by('id').to_pylist()
assert actual == ([] if mode == 'postpone' else rows)
@pytest.mark.parametrize('stream', [False, True], ids=['batch', 'stream'])
def test_input_changelog_creation_time_is_taken_after_its_write(tmp_path, stream):
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
schema = pa.schema([('id', pa.int32()), ('value', pa.int32())])
catalog.create_table('default.t', Schema.from_pyarrow_schema(
schema, primary_keys=['id'], options={
'bucket': '1', 'changelog-producer': 'input',
'write.native.enabled': 'false', 'commit.native.enabled': 'false',
'read.native.enabled': 'false', 'scan.native-plan.enabled': 'false'}), False)
table = catalog.get_table('default.t')
builder = table.new_stream_write_builder() if stream else table.new_batch_write_builder()
writer, commit = builder.new_write(), builder.new_commit()
clock = [datetime(2024, 6, 1, 12, 30, 40, 123456)]
completed = {}
write_parquet = table.file_io.write_parquet
def write_and_advance(path, *args, **kwargs):
result = write_parquet(path, *args, **kwargs)
# Advance a deterministic clock only after a real file was written.
# Each file's result must use its own completion time, including the
# changelog whose statistics were collected before the I/O.
clock[0] += timedelta(seconds=1)
completed[path] = Timestamp.from_local_date_time(clock[0]).to_millis_timestamp()
return result
try:
with patch.object(table.file_io, 'write_parquet', side_effect=write_and_advance), \
patch.object(Timestamp, 'now', side_effect=lambda: Timestamp.from_local_date_time(clock[0])):
writer.write_arrow(pa.Table.from_pylist([
{'id': 1, 'value': 10}, {'id': 1, 'value': 20}], schema=schema))
messages = writer.prepare_commit(1) if stream else writer.prepare_commit()
data_files = [file for message in messages for file in message.new_files]
changelogs = [file for message in messages for file in message.changelog_files]
assert data_files and changelogs
assert sum(file.row_count for file in data_files) == 1
assert sum(file.row_count for file in changelogs) == 2
for file in data_files + changelogs:
assert file.creation_time == completed[file.file_path]
commit.commit(messages, 1) if stream else commit.commit(messages)
finally:
writer.close()
commit.close()
read = table.new_read_builder()
assert read.new_read().to_arrow(read.new_scan().plan().splits()).to_pylist() == [{'id': 1, 'value': 20}]