Skip to content

Commit 71e356b

Browse files
committed
bundle-jemalloc: Bundle jemalloc with JDBC java library, Fixes duckdb#63
1 parent 7319174 commit 71e356b

File tree

60 files changed

+471
-261
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+471
-261
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
build
22
.idea
3+
*.iml
34
cmake-build-debug

CMakeLists.txt

+1-1
Large diffs are not rendered by default.

src/duckdb/extension/core_functions/scalar/random/random.cpp

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
namespace duckdb {
1010

1111
struct RandomLocalState : public FunctionLocalState {
12-
explicit RandomLocalState(uint32_t seed) : random_engine(seed) {
12+
explicit RandomLocalState(uint64_t seed) : random_engine(0) {
13+
random_engine.SetSeed(seed);
1314
}
1415

1516
RandomEngine random_engine;
@@ -30,7 +31,7 @@ static unique_ptr<FunctionLocalState> RandomInitLocalState(ExpressionState &stat
3031
FunctionData *bind_data) {
3132
auto &random_engine = RandomEngine::Get(state.GetContext());
3233
lock_guard<mutex> guard(random_engine.lock);
33-
return make_uniq<RandomLocalState>(random_engine.NextRandomInteger());
34+
return make_uniq<RandomLocalState>(random_engine.NextRandomInteger64());
3435
}
3536

3637
ScalarFunction RandomFun::GetFunction() {

src/duckdb/extension/json/json_functions/json_structure.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,8 @@ static double CalculateTypeSimilarity(const LogicalType &merged, const LogicalTy
626626
// This can happen for empty structs/maps ("{}"), or in rare cases where an inconsistent struct becomes
627627
// consistent when merged, but does not have enough children to be considered a map.
628628
return CalculateMapAndStructSimilarity(type, merged, true, max_depth, depth);
629+
} else if (type.id() != LogicalTypeId::STRUCT) {
630+
return -1;
629631
}
630632

631633
// Only structs can be merged into a struct

src/duckdb/src/common/adbc/adbc.cpp

+18
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,15 @@ AdbcStatusCode StatementSetSubstraitPlan(struct AdbcStatement *statement, const
172172
return ADBC_STATUS_INVALID_ARGUMENT;
173173
}
174174
auto wrapper = static_cast<DuckDBAdbcStatementWrapper *>(statement->private_data);
175+
if (wrapper->ingestion_stream.release) {
176+
// Release any resources currently held by the ingestion stream before we overwrite it
177+
wrapper->ingestion_stream.release(&wrapper->ingestion_stream);
178+
wrapper->ingestion_stream.release = nullptr;
179+
}
180+
if (wrapper->statement) {
181+
duckdb_destroy_prepare(&wrapper->statement);
182+
wrapper->statement = nullptr;
183+
}
175184
wrapper->substrait_plan = static_cast<uint8_t *>(malloc(sizeof(uint8_t) * length));
176185
wrapper->plan_length = length;
177186
memcpy(wrapper->substrait_plan, plan, length);
@@ -912,6 +921,15 @@ AdbcStatusCode StatementSetSqlQuery(struct AdbcStatement *statement, const char
912921
}
913922

914923
auto wrapper = static_cast<DuckDBAdbcStatementWrapper *>(statement->private_data);
924+
if (wrapper->ingestion_stream.release) {
925+
// Release any resources currently held by the ingestion stream before we overwrite it
926+
wrapper->ingestion_stream.release(&wrapper->ingestion_stream);
927+
wrapper->ingestion_stream.release = nullptr;
928+
}
929+
if (wrapper->statement) {
930+
duckdb_destroy_prepare(&wrapper->statement);
931+
wrapper->statement = nullptr;
932+
}
915933
auto res = duckdb_prepare(wrapper->connection, query, &wrapper->statement);
916934
auto error_msg = duckdb_prepare_error(wrapper->statement);
917935
return CheckResult(res, error, error_msg);

src/duckdb/src/common/file_buffer.cpp

+5-2
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,22 @@ FileBuffer::~FileBuffer() {
4242
allocator.FreeData(internal_buffer, internal_size);
4343
}
4444

45-
void FileBuffer::ReallocBuffer(size_t new_size) {
45+
void FileBuffer::ReallocBuffer(idx_t new_size) {
4646
data_ptr_t new_buffer;
4747
if (internal_buffer) {
4848
new_buffer = allocator.ReallocateData(internal_buffer, internal_size, new_size);
4949
} else {
5050
new_buffer = allocator.AllocateData(new_size);
5151
}
52+
53+
// FIXME: should we throw one of our exceptions here?
5254
if (!new_buffer) {
5355
throw std::bad_alloc();
5456
}
5557
internal_buffer = new_buffer;
5658
internal_size = new_size;
57-
// Caller must update these.
59+
60+
// The caller must update these.
5861
buffer = nullptr;
5962
size = 0;
6063
}

src/duckdb/src/common/random_engine.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ uint32_t RandomEngine::NextRandomInteger32(uint32_t min, uint32_t max) {
5959
return min + static_cast<uint32_t>(NextRandom32() * double(max - min));
6060
}
6161

62-
void RandomEngine::SetSeed(uint32_t seed) {
62+
void RandomEngine::SetSeed(uint64_t seed) {
6363
random_state->pcg.seed(seed);
6464
}
6565

src/duckdb/src/execution/join_hashtable.cpp

+14-4
Original file line numberDiff line numberDiff line change
@@ -1528,18 +1528,28 @@ bool JoinHashTable::PrepareExternalFinalize(const idx_t max_ht_size) {
15281528

15291529
// Create vector with unfinished partition indices
15301530
auto &partitions = sink_collection->GetPartitions();
1531+
auto min_partition_size = NumericLimits<idx_t>::Maximum();
15311532
vector<idx_t> partition_indices;
15321533
partition_indices.reserve(num_partitions);
15331534
for (idx_t partition_idx = 0; partition_idx < num_partitions; partition_idx++) {
1534-
if (!completed_partitions.RowIsValidUnsafe(partition_idx)) {
1535-
partition_indices.push_back(partition_idx);
1535+
if (completed_partitions.RowIsValidUnsafe(partition_idx)) {
1536+
continue;
15361537
}
1538+
partition_indices.push_back(partition_idx);
1539+
// Keep track of min partition size
1540+
const auto size =
1541+
partitions[partition_idx]->SizeInBytes() + PointerTableSize(partitions[partition_idx]->Count());
1542+
min_partition_size = MinValue(min_partition_size, size);
15371543
}
1544+
15381545
// Sort partitions by size, from small to large
1539-
std::sort(partition_indices.begin(), partition_indices.end(), [&](const idx_t &lhs, const idx_t &rhs) {
1546+
std::stable_sort(partition_indices.begin(), partition_indices.end(), [&](const idx_t &lhs, const idx_t &rhs) {
15401547
const auto lhs_size = partitions[lhs]->SizeInBytes() + PointerTableSize(partitions[lhs]->Count());
15411548
const auto rhs_size = partitions[rhs]->SizeInBytes() + PointerTableSize(partitions[rhs]->Count());
1542-
return lhs_size < rhs_size;
1549+
// We divide by min_partition_size, effectively rouding everything down to a multiple of min_partition_size
1550+
// Makes it so minor differences in partition sizes don't mess up the original order
1551+
// Retaining as much of the original order as possible reduces I/O (partition idx determines eviction queue idx)
1552+
return lhs_size / min_partition_size < rhs_size / min_partition_size;
15431553
});
15441554

15451555
// Determine which partitions should go next

src/duckdb/src/execution/operator/aggregate/physical_window.cpp

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
#include "duckdb/common/sort/partition_state.hpp"
44
#include "duckdb/function/window/window_aggregate_function.hpp"
5-
#include "duckdb/function/window/window_cumedist_function.hpp"
65
#include "duckdb/function/window/window_executor.hpp"
76
#include "duckdb/function/window/window_rank_function.hpp"
87
#include "duckdb/function/window/window_rownumber_function.hpp"

src/duckdb/src/execution/operator/csv_scanner/sniffer/csv_sniffer.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ AdaptiveSnifferResult CSVSniffer::MinimalSniff() {
133133
vector<HeaderValue> potential_header;
134134
for (idx_t col_idx = 0; col_idx < data_chunk.ColumnCount(); col_idx++) {
135135
auto &cur_vector = data_chunk.data[col_idx];
136-
auto vector_data = FlatVector::GetData<string_t>(cur_vector);
136+
const auto vector_data = FlatVector::GetData<string_t>(cur_vector);
137137
auto &validity = FlatVector::Validity(cur_vector);
138138
HeaderValue val;
139139
if (validity.RowIsValid(0)) {
@@ -181,7 +181,7 @@ SnifferResult CSVSniffer::AdaptiveSniff(const CSVSchema &file_schema) {
181181
return min_sniff_res.ToSnifferResult();
182182
}
183183

184-
SnifferResult CSVSniffer::SniffCSV(bool force_match) {
184+
SnifferResult CSVSniffer::SniffCSV(const bool force_match) {
185185
buffer_manager->sniffing = true;
186186
// 1. Dialect Detection
187187
DetectDialect();

src/duckdb/src/execution/operator/csv_scanner/sniffer/dialect_detection.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,8 @@ void CSVSniffer::AnalyzeDialectCandidate(unique_ptr<ColumnCountScanner> scanner,
362362
(single_column_before || ((more_values || more_columns) && !require_more_padding) ||
363363
(more_than_one_column && require_less_padding) || quoted) &&
364364
!invalid_padding && comments_are_acceptable) {
365-
if (!candidates.empty() && set_columns.IsSet() && max_columns_found == set_columns.Size()) {
365+
if (!candidates.empty() && set_columns.IsSet() && max_columns_found == set_columns.Size() &&
366+
consistent_rows <= best_consistent_rows) {
366367
// We have a candidate that fits our requirements better
367368
if (candidates.front()->ever_quoted || !scanner->ever_quoted) {
368369
return;

src/duckdb/src/execution/operator/csv_scanner/sniffer/header_detection.cpp

+50-17
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ static string GenerateColumnName(const idx_t total_cols, const idx_t col_number,
1717
// Helper function for UTF-8 aware space trimming
1818
static string TrimWhitespace(const string &col_name) {
1919
utf8proc_int32_t codepoint;
20-
auto str = reinterpret_cast<const utf8proc_uint8_t *>(col_name.c_str());
21-
idx_t size = col_name.size();
20+
const auto str = reinterpret_cast<const utf8proc_uint8_t *>(col_name.c_str());
21+
const idx_t size = col_name.size();
2222
// Find the first character that is not left trimmed
2323
idx_t begin = 0;
2424
while (begin < size) {
@@ -96,6 +96,44 @@ static string NormalizeColumnName(const string &col_name) {
9696
return col_name_cleaned;
9797
}
9898

99+
static void ReplaceNames(vector<string> &detected_names, CSVStateMachine &state_machine,
100+
unordered_map<idx_t, vector<LogicalType>> &best_sql_types_candidates_per_column_idx,
101+
CSVReaderOptions &options, const vector<HeaderValue> &best_header_row,
102+
CSVErrorHandler &error_handler) {
103+
auto &dialect_options = state_machine.dialect_options;
104+
if (!options.columns_set) {
105+
if (options.file_options.hive_partitioning || options.file_options.union_by_name || options.multi_file_reader) {
106+
// Just do the replacement
107+
for (idx_t i = 0; i < MinValue<idx_t>(detected_names.size(), options.name_list.size()); i++) {
108+
detected_names[i] = options.name_list[i];
109+
}
110+
return;
111+
}
112+
if (options.name_list.size() > dialect_options.num_cols) {
113+
if (options.null_padding) {
114+
// we increase our types
115+
idx_t col = 0;
116+
for (idx_t i = dialect_options.num_cols; i < options.name_list.size(); i++) {
117+
detected_names.push_back(GenerateColumnName(options.name_list.size(), col++));
118+
best_sql_types_candidates_per_column_idx[i] = {LogicalType::VARCHAR};
119+
}
120+
121+
dialect_options.num_cols = options.name_list.size();
122+
123+
} else {
124+
// we throw an error
125+
const auto error = CSVError::HeaderSniffingError(
126+
options, best_header_row, options.name_list.size(),
127+
state_machine.dialect_options.state_machine_options.delimiter.GetValue());
128+
error_handler.Error(error);
129+
}
130+
}
131+
for (idx_t i = 0; i < options.name_list.size(); i++) {
132+
detected_names[i] = options.name_list[i];
133+
}
134+
}
135+
}
136+
99137
// If our columns were set by the user, we verify if their names match with the first row
100138
bool CSVSniffer::DetectHeaderWithSetColumn(ClientContext &context, vector<HeaderValue> &best_header_row,
101139
const SetColumns &set_columns, CSVReaderOptions &options) {
@@ -181,11 +219,8 @@ CSVSniffer::DetectHeaderInternal(ClientContext &context, vector<HeaderValue> &be
181219
detected_names.push_back(GenerateColumnName(dialect_options.num_cols, col));
182220
}
183221
// If the user provided names, we must replace our header with the user provided names
184-
if (!options.columns_set) {
185-
for (idx_t i = 0; i < MinValue<idx_t>(best_header_row.size(), options.name_list.size()); i++) {
186-
detected_names[i] = options.name_list[i];
187-
}
188-
}
222+
ReplaceNames(detected_names, state_machine, best_sql_types_candidates_per_column_idx, options, best_header_row,
223+
error_handler);
189224
return detected_names;
190225
}
191226
// information for header detection
@@ -199,11 +234,8 @@ CSVSniffer::DetectHeaderInternal(ClientContext &context, vector<HeaderValue> &be
199234
detected_names.push_back(GenerateColumnName(dialect_options.num_cols, col));
200235
}
201236
dialect_options.rows_until_header += 1;
202-
if (!options.columns_set) {
203-
for (idx_t i = 0; i < MinValue<idx_t>(detected_names.size(), options.name_list.size()); i++) {
204-
detected_names[i] = options.name_list[i];
205-
}
206-
}
237+
ReplaceNames(detected_names, state_machine, best_sql_types_candidates_per_column_idx, options,
238+
best_header_row, error_handler);
207239
return detected_names;
208240
}
209241
auto error =
@@ -295,16 +327,17 @@ CSVSniffer::DetectHeaderInternal(ClientContext &context, vector<HeaderValue> &be
295327
}
296328

297329
// If the user provided names, we must replace our header with the user provided names
298-
if (!options.columns_set) {
299-
for (idx_t i = 0; i < MinValue<idx_t>(detected_names.size(), options.name_list.size()); i++) {
300-
detected_names[i] = options.name_list[i];
301-
}
302-
}
330+
ReplaceNames(detected_names, state_machine, best_sql_types_candidates_per_column_idx, options, best_header_row,
331+
error_handler);
303332
return detected_names;
304333
}
305334
void CSVSniffer::DetectHeader() {
306335
auto &sniffer_state_machine = best_candidate->GetStateMachine();
307336
names = DetectHeaderInternal(buffer_manager->context, best_header_row, sniffer_state_machine, set_columns,
308337
best_sql_types_candidates_per_column_idx, options, *error_handler);
338+
for (idx_t i = max_columns_found; i < names.size(); i++) {
339+
detected_types.push_back(LogicalType::VARCHAR);
340+
}
341+
max_columns_found = names.size();
309342
}
310343
} // namespace duckdb

src/duckdb/src/execution/operator/csv_scanner/state_machine/csv_state_machine_cache.cpp

+10-8
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ void CSVStateMachineCache::Insert(const CSVStateMachineOptions &state_machine_op
5757

5858
const bool multi_byte_delimiter = delimiter_value.size() != 1;
5959

60-
bool enable_unquoted_escape = state_machine_options.rfc_4180.GetValue() == false &&
61-
state_machine_options.quote != state_machine_options.escape &&
62-
state_machine_options.escape != '\0';
60+
const bool enable_unquoted_escape = state_machine_options.rfc_4180.GetValue() == false &&
61+
state_machine_options.quote != state_machine_options.escape &&
62+
state_machine_options.escape != '\0';
6363
// Now set values depending on configuration
6464
// 1) Standard/Invalid State
65-
vector<uint8_t> std_inv {static_cast<uint8_t>(CSVState::STANDARD), static_cast<uint8_t>(CSVState::INVALID),
66-
static_cast<uint8_t>(CSVState::STANDARD_NEWLINE)};
67-
for (auto &state : std_inv) {
65+
const vector<uint8_t> std_inv {static_cast<uint8_t>(CSVState::STANDARD), static_cast<uint8_t>(CSVState::INVALID),
66+
static_cast<uint8_t>(CSVState::STANDARD_NEWLINE)};
67+
for (const auto &state : std_inv) {
6868
if (multi_byte_delimiter) {
6969
transition_array[delimiter_first_byte][state] = CSVState::DELIMITER_FIRST_BYTE;
7070
} else {
@@ -75,7 +75,9 @@ void CSVStateMachineCache::Insert(const CSVStateMachineOptions &state_machine_op
7575
if (state == static_cast<uint8_t>(CSVState::STANDARD_NEWLINE)) {
7676
transition_array[static_cast<uint8_t>('\n')][state] = CSVState::STANDARD;
7777
} else {
78-
transition_array[static_cast<uint8_t>('\n')][state] = CSVState::RECORD_SEPARATOR;
78+
if (!state_machine_options.rfc_4180.GetValue()) {
79+
transition_array[static_cast<uint8_t>('\n')][state] = CSVState::RECORD_SEPARATOR;
80+
}
7981
}
8082
} else {
8183
transition_array[static_cast<uint8_t>('\r')][state] = CSVState::RECORD_SEPARATOR;
@@ -96,7 +98,7 @@ void CSVStateMachineCache::Insert(const CSVStateMachineOptions &state_machine_op
9698
transition_array[' '][static_cast<uint8_t>(CSVState::DELIMITER)] = CSVState::EMPTY_SPACE;
9799
}
98100

99-
vector<uint8_t> delimiter_states {
101+
const vector<uint8_t> delimiter_states {
100102
static_cast<uint8_t>(CSVState::DELIMITER), static_cast<uint8_t>(CSVState::DELIMITER_FIRST_BYTE),
101103
static_cast<uint8_t>(CSVState::DELIMITER_SECOND_BYTE), static_cast<uint8_t>(CSVState::DELIMITER_THIRD_BYTE)};
102104

src/duckdb/src/execution/operator/csv_scanner/table_function/csv_file_scanner.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ void CSVFileScan::SetStart() {
6060
}
6161

6262
CSVFileScan::CSVFileScan(ClientContext &context, const string &file_path_p, const CSVReaderOptions &options_p,
63-
const idx_t file_idx_p, const ReadCSVData &bind_data, const vector<ColumnIndex> &column_ids,
63+
idx_t file_idx_p, const ReadCSVData &bind_data, const vector<ColumnIndex> &column_ids,
6464
CSVSchema &file_schema, bool per_file_single_threaded)
6565
: file_path(file_path_p), file_idx(file_idx_p),
6666
error_handler(make_shared_ptr<CSVErrorHandler>(options_p.ignore_errors.GetValue())), options(options_p) {

src/duckdb/src/execution/operator/csv_scanner/table_function/global_csv_state.cpp

+12-6
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ unique_ptr<StringValueScanner> CSVGlobalState::Next(optional_ptr<StringValueScan
8787
previous_scanner->GetValidationLine());
8888
}
8989
if (single_threaded) {
90+
{
91+
lock_guard<mutex> parallel_lock(main_mutex);
92+
if (previous_scanner) {
93+
// Cleanup previous scanner.
94+
previous_scanner->buffer_tracker.reset();
95+
current_buffer_in_use.reset();
96+
previous_scanner->csv_file_scan->Finish();
97+
}
98+
}
9099
idx_t cur_idx;
91100
bool empty_file = false;
92101
do {
@@ -108,6 +117,7 @@ unique_ptr<StringValueScanner> CSVGlobalState::Next(optional_ptr<StringValueScan
108117
auto file_scan = make_shared_ptr<CSVFileScan>(context, bind_data.files[cur_idx], bind_data.options, cur_idx,
109118
bind_data, column_ids, file_schema, true);
110119
empty_file = file_scan->file_size == 0;
120+
111121
if (!empty_file) {
112122
lock_guard<mutex> parallel_lock(main_mutex);
113123
file_scans.emplace_back(std::move(file_scan));
@@ -116,11 +126,7 @@ unique_ptr<StringValueScanner> CSVGlobalState::Next(optional_ptr<StringValueScan
116126
current_boundary.SetCurrentBoundaryToPosition(single_threaded);
117127
current_buffer_in_use = make_shared_ptr<CSVBufferUsage>(*file_scans.back()->buffer_manager,
118128
current_boundary.GetBufferIdx());
119-
if (previous_scanner) {
120-
previous_scanner->buffer_tracker.reset();
121-
current_buffer_in_use.reset();
122-
previous_scanner->csv_file_scan->Finish();
123-
}
129+
124130
return make_uniq<StringValueScanner>(scanner_idx++, current_file->buffer_manager,
125131
current_file->state_machine, current_file->error_handler,
126132
current_file, false, current_boundary);
@@ -178,7 +184,7 @@ unique_ptr<StringValueScanner> CSVGlobalState::Next(optional_ptr<StringValueScan
178184

179185
idx_t CSVGlobalState::MaxThreads() const {
180186
// We initialize max one thread per our set bytes per thread limit
181-
if (single_threaded) {
187+
if (single_threaded || !file_scans.front()->on_disk_file) {
182188
return system_threads;
183189
}
184190
idx_t total_threads = file_scans.front()->file_size / CSVIterator::BYTES_PER_THREAD + 1;

src/duckdb/src/execution/sample/reservoir_sample.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ void ReservoirSample::EvictOverBudgetSamples() {
505505
D_ASSERT(num_samples_to_keep <= sample_count);
506506
D_ASSERT(stats_sample);
507507
D_ASSERT(sample_count == FIXED_SAMPLE_SIZE);
508-
auto new_reservoir_chunk = CreateNewSampleChunk(types, FIXED_SAMPLE_SIZE);
508+
auto new_reservoir_chunk = CreateNewSampleChunk(types, sample_count);
509509

510510
// The current selection vector can potentially have 2048 valid mappings.
511511
// If we need to save a sample with less rows than that, we need to do the following

src/duckdb/src/function/table/read_csv.cpp

+3-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ static unique_ptr<FunctionData> ReadCSVBind(ClientContext &context, TableFunctio
126126
auto &options = result->options;
127127
auto multi_file_reader = MultiFileReader::Create(input.table_function);
128128
auto multi_file_list = multi_file_reader->CreateFileList(context, input.inputs[0]);
129-
129+
if (multi_file_list->GetTotalFileCount() > 1) {
130+
options.multi_file_reader = true;
131+
}
130132
options.FromNamedParameters(input.named_parameters, context);
131133

132134
options.file_options.AutoDetectHivePartitioning(*multi_file_list, context);

src/duckdb/src/function/table/system/duckdb_memory.cpp

-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ static unique_ptr<FunctionData> DuckDBMemoryBind(ClientContext &context, TableFu
2727

2828
unique_ptr<GlobalTableFunctionState> DuckDBMemoryInit(ClientContext &context, TableFunctionInitInput &input) {
2929
auto result = make_uniq<DuckDBMemoryData>();
30-
3130
result->entries = BufferManager::GetBufferManager(context).GetMemoryUsageInfo();
3231
return std::move(result);
3332
}

0 commit comments

Comments
 (0)