ClickHouse-Encoder
view release on metacpan or search on metacpan
lib/ClickHouse/Encoder.pm view on Meta::CPAN
$out = Compress::Zstd::decompress($payload);
die "decompress_native_block: zstd decompression failed\n"
unless defined $out;
} else {
die sprintf("decompress_native_block: unknown method tag 0x%02x "
. "(expected 0x02 NONE, 0x82 LZ4, or 0x90 ZSTD)\n", $tag);
}
die "decompress_native_block: decompressed size mismatch "
. "(header says $usize, got " . length($out) . ")\n"
unless length($out) == $usize;
return wantarray ? ($out, $end - $off) : $out;
}
# Expand any Nested(field T, ...) entries in a column list into the flat
# `name.field Array(T)` columns ClickHouse stores them as. Returns a new
# arrayref; non-Nested columns pass through unchanged. Pair with new():
# my $enc = ClickHouse::Encoder->new(
# columns => ClickHouse::Encoder->flatten_nested(\@user_columns));
sub flatten_nested {
my ($class, $cols) = @_;
my @out;
for my $c (@$cols) {
my ($name, $type) = @$c;
if ($type =~ /\ANested\((.+)\)\z/s) {
my @parts = _split_paren_list($1);
@parts or die "Nested($1) for column '$name' has no elements";
for my $part (@parts) {
$part =~ /\A([A-Za-z_][A-Za-z0-9_]*)\s+(.+?)\s*\z/s
or die "Nested element '$part' is not 'name Type'";
push @out, ["$name.$1", "Array($2)"];
}
} else {
push @out, [$name, $type];
}
}
return \@out;
}
# Split a comma-separated list at depth-0 commas (so Tuple(Int32, String)
# inside a Nested element stays one entry).
sub _split_paren_list {
my $body = shift;
my @parts;
my ($start, $depth, $len) = (0, 0, length $body);
for (my $i = 0; $i <= $len; $i++) {
my $c = $i < $len ? substr($body, $i, 1) : ',';
if ($c eq '(') { $depth++ }
elsif ($c eq ')') { $depth-- }
elsif ($c eq ',' && $depth == 0) {
(my $p = substr($body, $start, $i - $start)) =~ s/\A\s+|\s+\z//g;
push @parts, $p if length $p;
$start = $i + 1;
}
}
return @parts;
}
# Row-oriented decode: { ncols, nrows, names, types, rows } where
# rows is an arrayref of arrayrefs. Calls the XS decode_block_rows,
# which distributes column values into per-row arrayrefs as each
# column is decoded and frees the column AV eagerly. Peak memory
# is one column's AV plus the row AVs (vs a Perl-side transpose
# that holds ALL column AVs alongside the half-built row AVs).
# Throughput is similar to the column-major path; the win is the
# tighter memory profile on wide blocks.
sub decode_rows {
my ($class, $bytes, $offset) = @_;
return $class->decode_block_rows($bytes, $offset // 0);
}
# Decode a concatenated stream of Native blocks (the body of a
# `select ... format native` response). Returns an arrayref of the
# same hashref shape as decode_block. Stops cleanly when bytes are
# exhausted; partial trailing bytes raise an error from XS.
# Uses the 3-arg form of decode_block (with offset) to avoid O(N^2)
# substr copies on long streams.
sub decode_blocks {
my ($class, $bytes, $cb, %opts) = @_;
my $keep = $opts{keep};
my $off = 0;
my $len = length $bytes;
# Callback form: hand each block to $cb as it's decoded; never
# accumulate. Useful for streaming selects where the full block
# list would not fit comfortably in memory.
if ($cb) {
while ($off < $len) {
my $block = $class->decode_block($bytes, $off, $keep);
$off += $block->{consumed};
$cb->($block);
}
return;
}
my @blocks;
while ($off < $len) {
my $block = $class->decode_block($bytes, $off, $keep);
$off += $block->{consumed};
push @blocks, $block;
}
return \@blocks;
}
# Return a coderef that yields one block per call (undef when done).
# Holds a reference to $bytes; the closure is the only thing that
# survives between calls.
sub decode_blocks_iter {
my ($class, $bytes, %opts) = @_;
my $keep = $opts{keep};
my $off = 0;
my $len = length $bytes;
return sub {
return if $off >= $len;
my $block = $class->decode_block($bytes, $off, $keep);
$off += $block->{consumed};
return $block;
};
}
# Pull-style decoder that reads incrementally from a filehandle (or
# any IO::Handle-ish object). For each complete block, invokes $cb
# with the block hashref. Keeps a sliding buffer; on truncated decode
# it reads more bytes and retries. Useful when the response body is
# too large to fit in memory.
sub decode_stream {
my ($class, $fh, $cb, %opts) = @_;
my $chunk_size = $opts{chunk_size} // 64 * 1024;
my $keep = $opts{keep};
my $decompress = $opts{decompress};
my $buf = ''; # raw bytes from the filehandle
my $inner = ''; # decompressed Native bytes (== $buf when !decompress)
my $done = 0;
until ($done) {
# Phase 1: peel compressed-block frames out of $buf into $inner.
if ($decompress) {
while (length($buf) >= 25) {
my $csize = unpack 'V', substr($buf, 17, 4);
last if length($buf) < 16 + $csize;
my ($plain, $consumed) = eval {
$class->decompress_native_block($buf)
};
die $@ if $@;
$inner .= $plain;
substr($buf, 0, $consumed, '');
lib/ClickHouse/Encoder.pm view on Meta::CPAN
: $p <= 38 ? 16 : 32;
}
my $w = $RB_FIXED_WIDTH{$base};
die "decode_row_binary: cannot size unsupported type '$type'\n"
unless defined $w;
return $w;
}
# Decode one value of $type at $$posref, advancing $$posref past it.
sub _rb_decode_value {
my ($type, $bufref, $posref, $cache) = @_;
if ($type =~ /\AArray\((.+)\)\z/s) {
my $inner = $1;
my ($n, $after) = _rb_unpack_varint($$bufref, $$posref);
$$posref = $after;
return [ map { _rb_decode_value($inner, $bufref, $posref, $cache) }
1 .. $n ];
}
if ($type =~ /\ALowCardinality\((.+)\)\z/s) {
return _rb_decode_value($1, $bufref, $posref, $cache);
}
_rb_assert_scalar($type);
my $vlen = _rb_value_len($type, $bufref, $$posref);
my $vbytes = substr($$bufref, $$posref, $vlen);
die "decode_row_binary: truncated value for '$type' at offset $$posref\n"
if length($vbytes) != $vlen;
$$posref += $vlen;
my $prefix = $cache->{$type} ||=
"\x01\x01" . _rb_pack_string('c') . _rb_pack_string($type);
my $blk = ClickHouse::Encoder->decode_block($prefix . $vbytes);
return $blk->{columns}[0]{values}[0];
}
# Decode a RowBinary byte string into an arrayref of row arrayrefs.
# Call on an encoder instance whose column types match the producer.
sub decode_row_binary {
my ($self, $bytes) = @_;
die "decode_row_binary: must be called on an encoder instance\n"
unless ref $self;
die "decode_row_binary: input must be defined\n" unless defined $bytes;
my $cols = $self->columns;
my $pos = 0;
my $len = length $bytes;
# With zero columns the inner per-column loop is a no-op, so a
# non-empty buffer would never make $pos advance - guard explicitly
# rather than spin forever.
die "decode_row_binary: encoder has no columns but $len bytes given\n"
if !@$cols && $len;
my %cache;
my @rows;
while ($pos < $len) {
my @row;
for my $col (@$cols) {
push @row, _rb_decode_value($col->[1], \$bytes, \$pos, \%cache);
}
push @rows, \@row;
}
return \@rows;
}
# Post-process a decoded block (or any one column's values arrayref):
# rewrite Date / Date32 / DateTime / DateTime64 integer epochs into
# ISO 8601 strings or Time::Moment instances. Modifies the block in
# place AND returns it so the call can be chained. as => 'iso' (the
# default) emits UTC strings with a 'Z' suffix; as => 'datetime'
# returns Time::Moment objects (requires Time::Moment installed).
# DateTime64 precision is read from the column's type string so each
# tick converts to the correct number of fractional digits.
sub coerce_datetimes {
my ($class_or_self, $block, %opts) = @_;
my $as = $opts{as} // 'iso';
die "coerce_datetimes: 'as' must be 'iso' or 'datetime' (got '$as')\n"
unless $as eq 'iso' || $as eq 'datetime';
if ($as eq 'datetime') {
require Time::Moment;
} else {
require POSIX;
}
for my $col (@{ $block->{columns} }) {
next if $col->{skipped};
my $type = $col->{type};
my $vals = $col->{values};
# Strip Nullable() wrapping so the inner type matches below.
# Nullable values come through as undef in the values array;
# the loops already skip undef.
$type = $1 if $type =~ /^Nullable\((.*)\)\z/;
if ($type eq 'Date' || $type eq 'Date32') {
for my $v (@$vals) {
next unless defined $v;
$v = _epoch_to_string($v * 86400, 0, $as, 'Y-m-d');
}
}
elsif ($type eq 'DateTime' || $type =~ /^DateTime\(/) {
for my $v (@$vals) {
next unless defined $v;
$v = _epoch_to_string($v, 0, $as, 'iso');
}
}
elsif ($type =~ /^DateTime64\((\d+)/) {
my $precision = $1;
my $scale = 10 ** $precision;
for my $v (@$vals) {
next unless defined $v;
# Decoded DateTime64 is an integer count of (10^precision)
# ticks since the Unix epoch (a signed int64).
use integer;
my $secs = int($v / $scale);
no integer;
my $frac = $v - $secs * $scale;
# Normalize negative fractional tail to a positive frac
# below the integer epoch.
if ($frac < 0) { $frac += $scale; $secs -= 1 }
$v = _epoch_to_string($secs, $frac, $as,
'iso', $precision);
}
}
# other columns (non-time) untouched
lib/ClickHouse/Encoder.pm view on Meta::CPAN
my $block = ClickHouse::Encoder->decode_block(
$bytes, 0, { id => 1, ts => 1 });
Columns whose name isn't in the filter still consume their wire
bytes (so the cursor stays aligned) but their C<values> array is
replaced with N C<undef>s and the column hashref carries a
C<<< skipped => 1 >>> marker. Skips the SV-allocation cost for
unwanted columns; useful on wide C<select *> responses.
XS implementation: walks the Native byte stream using the same type
parser the encoder uses, so symmetric round-trips are guaranteed for
every type C<encode> handles (C<BFloat16>, alphabetical Variant
remapping, C<LowCardinality> dict indirection,
C<SimpleAggregateFunction> passthrough, JSON typed paths, etc.).
C<Decimal128> values come back as C<<< [$lo_uint64, $hi_int64] >>>;
C<Decimal256> as a 4-limb arrayref. Use L</decimal128_str> /
L</decimal256_str> to convert to scaled decimal strings.
=head2 decode_rows
my $r = ClickHouse::Encoder->decode_rows($bytes);
my $r = ClickHouse::Encoder->decode_rows($bytes, $offset);
Row-oriented convenience. Returns:
{
ncols => $n_columns,
nrows => $n_rows,
names => [...],
types => [...],
rows => [[...], [...], ...],
consumed => $bytes_used,
}
Calls an XS row-major decoder (C<decode_block_rows>) that walks each
column then immediately distributes its values into the per-row
arrayrefs and frees the column AV. Peak memory holds one column's
AV plus the row AVs (vs both column- and row-major representations
fully alive, which a Perl-side transpose would entail). Throughput
is similar to L</decode_block>; the win is the tighter peak memory
on wide blocks.
=head2 decode_block_rows
my $r = ClickHouse::Encoder->decode_block_rows($bytes, $offset);
XS row-major decoder; same return shape as L</decode_rows>. Direct
entry point if you want to avoid the L</decode_rows> Perl-side
trampoline.
=head2 decode_blocks
my $blocks = ClickHouse::Encoder->decode_blocks($bytes);
ClickHouse::Encoder->decode_blocks($bytes, sub { my $b = shift; ... });
A C<select ... format native> response is a concatenated stream of
blocks (one per granule of C<max_block_size> rows). With no callback,
C<decode_blocks> walks the stream and returns an arrayref of the same
hashref shape as L</decode_block>. With a callback, each block is
passed to the callback as it's decoded and no list is accumulated -
useful for very long selects where the full block list wouldn't fit
comfortably in memory.
Uses the 3-arg form of L</decode_block> (with explicit offset) to
keep total work O(N) regardless of block count. Stops cleanly when
bytes are exhausted; partial trailing bytes croak.
The optional C<keep =E<gt> \%names> hashref forwards a column filter
to L</decode_block> for every block in the stream, matching the
same semantics: present keys are decoded, absent ones still have
their bytes consumed (to keep the cursor aligned) but their values
are not materialized and their column hash carries C<skipped =E<gt> 1>.
Useful for big-fan-out select responses where only a few columns
of a wide row matter.
ClickHouse::Encoder->decode_blocks($bytes, $cb,
keep => { id => 1, event => 1 });
=head2 decode_blocks_iter
my $iter = ClickHouse::Encoder->decode_blocks_iter($bytes);
while (my $block = $iter->()) { ... }
my $iter = ClickHouse::Encoder->decode_blocks_iter($bytes,
keep => { id => 1 });
Returns a coderef that yields one block per call (C<undef> when
exhausted). Same per-block payload as L</decode_block>; useful when
you want pull-style iteration without committing to a callback.
Accepts the same C<keep> filter as L</decode_blocks>.
=head2 decode_stream
ClickHouse::Encoder->decode_stream($fh, sub { my $block = shift; ... },
chunk_size => 65536);
ClickHouse::Encoder->decode_stream($fh, $cb,
keep => { id => 1, event => 1 });
Pull bytes incrementally from a filehandle (or any read-able IO
handle), yielding each complete block to the callback as it
arrives. Uses a sliding buffer; on a truncated decode it reads more
bytes and retries. Memory stays bounded by C<chunk_size> + one
block, so this is the right entry point for select responses too
large to buffer in full. Croaks on partial trailing bytes.
The C<keep> filter is the same one L</decode_block> accepts:
unwanted columns still have their bytes consumed (so the cursor
stays aligned) but their values are not materialized into an SV
array, so peak memory stays bounded by the kept columns.
With C<decompress =E<gt> 1>, C<$fh> is expected to deliver a stream
of compressed-block-framed Native blocks (the format CH's HTTP
C<?compress=1> response uses, or a captured native-TCP Data stream
under compression). C<decode_stream> peels each compressed block
via L</decompress_native_block> before feeding the resulting raw
Native bytes into L</decode_block>.
C<$fh> must support Perl's C<read()> builtin (any plain filehandle
or L<IO::Handle> subclass). Raw socket descriptors that only
support C<sysread> need to be wrapped via L<IO::Socket> or read
into a buffer that is then fed to L</decode_block> directly.
=head2 ping
ClickHouse::Encoder->ping(host => 'db', port => 8123);
ClickHouse::Encoder->ping(scheme => 'https', host => 'db', port => 8443);
Liveness check via CH's C</ping> endpoint. Returns C<1> on success;
croaks on connection refused, timeout, or any non-2xx HTTP status.
lib/ClickHouse/Encoder.pm view on Meta::CPAN
$diff, table => 'events');
# $stmts = [ 'alter table `events` drop column `old_col`',
# 'alter table `events` modify column `x` UInt32',
# 'alter table `events` add column `new_col` Int64' ]
Translates a L</schema_diff> hashref into a list of C<alter table>
statements. Returns an arrayref of SQL strings; the caller decides
whether to apply them transactionally or one at a time. Ordering is
deterministic: drops first, then modifies, then adds.
=head2 for_native_bytes
my $enc = ClickHouse::Encoder->for_native_bytes($captured_bytes);
Inspect a captured Native block and return a fresh encoder
configured for that exact column shape. Zero-row blocks work fine
(the column headers are still on the wire); the typical use case
is round-tripping captured payloads through a transform / filter
step where you need an encoder matching the input's schema but
don't have access to the source server.
=head2 encode_row_binary
my $body = $enc->encode_row_binary(\@rows);
# POST as: insert into t format RowBinary
Encode rows into ClickHouse's row-major C<RowBinary> format (the
request body for C<insert ... format RowBinary>). Native is the
preferred format and is what the rest of this module uses;
C<RowBinary> is offered for interoperability with producers and
pipelines that speak it. Call on an encoder instance - its column
types drive serialisation.
Supported column types: every scalar type
(C<Int*>/C<UInt*>/C<Float*>/C<Bool>/C<Date*>/C<DateTime*>/
C<Decimal*>/C<UUID>/C<IPv4>/C<IPv6>/C<Enum*>), C<String>,
C<FixedString(N)>, C<Nullable(...)> of any of those, C<Array(...)>
nesting, and C<LowCardinality(...)> (encoded as its inner type, as
RowBinary represents it). C<Map>, C<Tuple>, C<Variant>, C<JSON>,
C<Dynamic>, Geo and C<Nested> columns croak - their Native and
RowBinary framings differ. Use Native (L</encode>) for those.
=head2 decode_row_binary
my $rows = $enc->decode_row_binary($bytes);
Decode a C<RowBinary> byte string into an arrayref of row
arrayrefs. Call on an encoder instance whose column types match the
producer (RowBinary carries no schema, so the types must be known
out of band). The supported type surface and per-type value
semantics are identical to L</encode_row_binary> and to the Native
decoder - C<decode_row_binary> yields the same Perl values
L</decode_block> would for the same data.
=head2 coerce_datetimes
my $blk = ClickHouse::Encoder->decode_block($bytes);
ClickHouse::Encoder->coerce_datetimes($blk); # ISO strings
ClickHouse::Encoder->coerce_datetimes($blk, as => 'datetime'); # Time::Moment
Post-process a decoded block: rewrite every C<Date> / C<Date32> /
C<DateTime> / C<DateTime(tz)> / C<DateTime64(p)> column's values
from raw epoch integers into either ISO 8601 strings (the default;
UTC with a C<Z> suffix) or L<Time::Moment> instances
(C<as =E<gt> 'datetime'>). The block is mutated in place and also
returned, so the call can be chained.
C<Nullable()>-wrapped time columns are handled too; C<undef>
values pass through unchanged. Non-time columns are untouched.
C<DateTime64(p)> precision is honored - the fractional part is
emitted as exactly C<p> digits in ISO form, or as nanoseconds
widened from C<p> ticks in C<Time::Moment> form.
This is a separate post-decode step (rather than an option on
L</decode_block>) so the cost is only paid when the caller wants
formatted values. Raw integer epochs are still cheaper to compare,
filter, or pass back into a re-encode.
=head2 parse_wkt
my $point = ClickHouse::Encoder->parse_wkt('POINT(1.5 2.5)');
my $poly = ClickHouse::Encoder->parse_wkt(
'POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))');
# round-trip into a CH Geo column:
my $enc = ClickHouse::Encoder->new(columns => [
['p', 'Point'], ['poly', 'Polygon']]);
$enc->encode([[ $point, $poly ]]);
Parse a Well-Known-Text geometry string into the nested-arrayref
shape that the Geo column encoders accept. Supports C<POINT>,
C<LINESTRING>, C<MULTILINESTRING>, C<POLYGON>, and C<MULTIPOLYGON>.
The CH C<Ring> type has no WKT name; feed a C<LINESTRING> result
into a Ring column directly. Geometry names are case-insensitive
and surrounding whitespace is tolerated. Malformed input croaks
with a message identifying the offending geometry.
=head2 estimate_size
my $bytes = $enc->estimate_size(\@rows);
my $bytes = $enc->estimate_size($n_rows,
avg_string_size => 64);
Coarse byte-size estimate for an encoded block, parameterized on row
count (an integer or arrayref-of-rows; only the count is used).
Returns an order-of-magnitude figure for batch-split decisions
("should I split this into two POSTs?") without paying the encode
cost. Fixed-width types are byte-exact; variable types
(C<String>, C<Array>, etc.) use a configurable 16-byte average
heuristic. For byte-exact size, call C<length($enc-E<gt>encode(...))>.
=head2 select_blocks
ClickHouse::Encoder->select_blocks(
'select id, event, ts from events where date = today()',
host => 'db.example', port => 8123,
database => 'default', user => 'default',
on_block => sub { my $block = shift; ... },
keep => { id => 1, event => 1 }, # optional projection
);
Streaming counterpart to L</insert_http>: POSTs C<$sql> to the
lib/ClickHouse/Encoder.pm view on Meta::CPAN
C<scheme =E<gt> 'https'> enables TLS via L<HTTP::Tiny> (install
L<IO::Socket::SSL> and L<Net::SSLeay>). C<ssl_options> and
C<verify_SSL> pass through to L<HTTP::Tiny>.
=item C<settings>
Hashref of per-query CH settings (C<max_execution_time>,
C<max_memory_usage>, ...) appended to every flush as URL params.
=item C<dedup_token>
Stamps every POST with C<insert_deduplication_token>. Identical
retries are rejected server-side, making the inserter
transactionally idempotent.
=back
Methods on the returned object:
=over 4
=item C<<< $bi->push($row) >>>
Append a single row. Auto-flushes if buffer crosses C<batch_size>.
=item C<<< $bi->push_many(\@rows) >>>
Append many rows in one call.
=item C<<< $bi->flush >>>
Flush whatever is in the buffer (idempotent: no-op when empty).
=item C<<< $bi->finish >>>
Flush and return C<<< { rows => $sent_total, batches => $batches } >>>.
=item C<<< $bi->buffered_count >>> / C<<< $bi->sent_rows >>> / C<<< $bi->sent_batches >>>
Instrumentation accessors.
=item C<<< $bi->summary >>>
Hashref of cumulative C<X-ClickHouse-Summary> stats rolled up across
batches (C<written_rows>, C<written_bytes>, C<elapsed_ns>, ...).
=item C<<< $bi->last_response >>>
The L<HTTP::Tiny> response hashref from the most recent flush, with
the parsed C<ch =E<gt> { query-id, server, format, exception-code,
summary, progress, ... }>
slot attached (whichever C<X-ClickHouse-*> headers the server sent).
C<undef> until the first flush succeeds.
=back
=head2 decimal128_str
my $s = ClickHouse::Encoder->decimal128_str($lo, $hi, $scale);
Convert a decoded C<Decimal128> low/high uint64 pair (as returned
by L</decode_block>) into a signed decimal string with the given
fractional scale. Uses L<Math::BigInt> for the 128-bit arithmetic.
=head2 decimal256_str
my $s = ClickHouse::Encoder->decimal256_str(\@limbs, $scale);
Convert a decoded C<Decimal256> 4-limb arrayref (low-to-high
uint64s, as returned by L</decode_block>) into a signed decimal
string with the given fractional scale. Uses L<Math::BigInt> for
the 256-bit arithmetic.
=head2 compressed_writer
my $w = ClickHouse::Encoder->compressed_writer('zstd', \&raw_writer);
my $st = $enc->streamer($w, batch_size => 1000);
Class method that wraps a writer coderef so each emitted block is
compressed before being forwarded. Modes: C<'zstd'> (requires
L<Compress::Zstd>), C<'gzip'> (uses core L<IO::Compress::Gzip>),
C<'raw'> / C<undef> (pass-through). Compose with L</stream> or
L</streamer>; the wrapper handles one block at a time so memory stays
proportional to a single batch.
=head2 compress_native_block
my $framed = ClickHouse::Encoder->compress_native_block(
$native_bytes,
mode => 'lz4', # 'lz4' | 'zstd' | 'auto' | 'none'
# hasher => \&my_cityhash128, # optional; default = bundled
);
Wraps an encoded Native block in ClickHouse's CompressedReadBuffer
framing: a 16-byte checksum, then a 9-byte header (1-byte method
tag + LE UInt32 compressed_size + LE UInt32 uncompressed_size),
then the LZ4 (tag C<0x82>), ZSTD (tag C<0x90>), or uncompressed
(tag C<0x02>) payload. This is the framing used by the native TCP
protocol when compression is negotiated and by Native-over-HTTP
with C<&compress=1> / C<&decompress=1>.
Modes:
=over 4
=item C<'lz4'>
LZ4-compressed via L<Compress::LZ4>'s raw form (no length prefix).
=item C<'zstd'>
ZSTD-compressed via L<Compress::Zstd>.
=item C<'auto'>
Try LZ4 first; if the result is C<E<gt>=> the input, fall back to
C<'none'>. Mirrors CH's own C<CompressedWriteBuffer> behavior for
incompressible payloads.
=item C<'none'>
No compression but still wrapped in the framing (method tag C<0x02>).
Useful when the wire context requires compressed-block framing but
the payload doesn't benefit from compression.
=back
The checksum is CityHash128 in the "cityhash102" variant
(ClickHouse's namespace fork of Google CityHash v1.0.2). This
( run in 0.949 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )