Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/chdb_hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ those you need.
| bytea | String | |
| date | Date32 | |
| time | Time64(6) | Override with `String` for formats that don't support times. |
| timestamp | DateTime64(6, 'UTC') | Converted from session time zone. |
| timestamp | DateTime64(6) | Converted from session time zone. |
| timestamptz | DateTime64(6, 'UTC') | |
| interval | String | Override with an `Interval` unit such as `IntervalDay`. |
| uuid | UUID | |
Expand Down
4 changes: 4 additions & 0 deletions src/helper.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
#include "utils/palloc.h"
#include "utils/wait_event.h"

#include "pg-clickhouse.h"

#include "helper.h"
#include "setup.h"

Expand Down Expand Up @@ -367,6 +369,8 @@ build_setup(
appendBinaryStringInfo(buf, (char*)&ctx->max_threads, sizeof(ctx->max_threads));
appendBinaryStringInfo(buf, (char*)&ctx->max_parsers, sizeof(ctx->max_parsers));

/* timestamp maps to DateTime64 without time zone, read in this zone */
append_string(buf, pgch_session_timezone());
append_string(buf, query);

uint16_t count = (uint16_t)nparams;
Expand Down
12 changes: 8 additions & 4 deletions src/helper/chdb_helper.c
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ setup_session(
chdb_connection conn,
uint16_t max_mem,
uint16_t max_threads,
uint16_t max_parsers
uint16_t max_parsers,
str timezone
) {
char settings[1024];
snprintf(
Expand All @@ -321,10 +322,12 @@ setup_session(
"output_format_native_encode_types_in_binary_format=0,"
"date_time_output_format='iso',"
"max_threads=%" PRIu16 ",max_parsing_threads=%" PRIu16
",max_memory_usage=%" PRIu64,
",max_memory_usage=%" PRIu64 ",session_timezone='%.*s'",
max_threads,
max_parsers,
(uint64_t)max_mem * 1024 * 1024
(uint64_t)max_mem * 1024 * 1024,
(int)timezone.len,
timezone.data
);
chdb_result* res = chdb_query(conn, settings, native_format);
const char* err = chdb_result_error(res);
Expand All @@ -346,6 +349,7 @@ main(void) {
uint16_t max_mem = take2(&cur);
uint16_t max_threads = take2(&cur);
uint16_t max_parsers = take2(&cur);
str timezone = take_str(&cur);
str query = take_str(&cur);
uint16_t npar = take2(&cur);

Expand Down Expand Up @@ -380,7 +384,7 @@ main(void) {
* Unfortunately, setting via argv doesn't work, so we have to set them a
* an initial query. https://github.com/chdb-io/chdb-core/issues/191
*/
int status = setup_session(*conn, max_mem, max_threads, max_parsers);
int status = setup_session(*conn, max_mem, max_threads, max_parsers, timezone);

if (!status) {
switch (cmd_type) {
Expand Down
25 changes: 15 additions & 10 deletions src/hook/copy.c
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ structure_for_attnums(TupleDesc desc, List* attnums) {
}

/*
* Return a copy of `structure` with every bare `type` clause replaced with
* `String`.
* Return a copy of `structure` with every bare `type` clause, along with any
* parameters it takes, replaced with `String`.
*/
static char*
structure_as_string(const char* structure, const char* type) {
Expand All @@ -99,20 +99,25 @@ structure_as_string(const char* structure, const char* type) {
quoted = !quoted;
}
if (!quoted && strncmp(pos, type, len) == 0 && pos > structure &&
(pos[-1] == ' ' || pos[-1] == '(') &&
(pos[len] == '\0' || pos[len] == ',' || pos[len] == ')')) {
appendStringInfoString(&buf, "String");
pos += len;
} else {
appendStringInfoChar(&buf, *pos++);
(pos[-1] == ' ' || pos[-1] == '(')) {
const char* end = pos + len;
if (*end == '(' && strchr(end, ')')) {
end = strchr(end, ')') + 1;
}
if (*end == '\0' || *end == ',' || *end == ')') {
appendStringInfoString(&buf, "String");
pos = end;
continue;
}
}
appendStringInfoChar(&buf, *pos++);
}
return buf.data;
}

/*
* Returns true if `format` is one of the formats lacking Time64 support.
* In such cases, `Time64(6)` should be replaced with `String`.
* In such cases, `Time64` should be replaced with `String`.
*/
static bool
format_lacks_time64(const char* format) {
Expand Down Expand Up @@ -147,7 +152,7 @@ chdb_copy(chdbCopyContext* ctx) {
ctx->structure = structure_as_string(ctx->structure, "UUID");
}
if (format_lacks_time64(ctx->format)) {
ctx->structure = structure_as_string(ctx->structure, "Time64(6)");
ctx->structure = structure_as_string(ctx->structure, "Time64");
}
}

Expand Down
1 change: 1 addition & 0 deletions src/setup.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ typedef struct chdbHelperContext {
* Fields are native endian.
*
* chdbHelperContext command type, settings
* string session_timezone
* string query
* uint16 parameter count
* string parameter name and value, repeated
Expand Down
4 changes: 2 additions & 2 deletions t/structure.pl
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@
qq{COPY datetime FROM 'file://$dir/nonesuch.csv'},
qr/nonesuch.csv doesn't exist/,
qr[\QSELECT * FROM file],
qr[\Q structure: "ts Nullable(DateTime64(6, 'UTC')), tsn Nullable(DateTime64(6, 'UTC')), tstz DateTime64(6, 'UTC'), tstzn DateTime64(6, 'UTC'), date Date32, time Time64(6), timen Time64(6), ttz String, ttzn String, ival String" }],
qr[\Q structure: "ts Nullable(DateTime64(6)), tsn Nullable(DateTime64(3)), tstz DateTime64(6, 'UTC'), tstzn DateTime64(4, 'UTC'), date Date32, time Time64(6), timen Time64(3), ttz String, ttzn String, ival String" }],
);

$node->psql(postgres => q{
Expand All @@ -324,7 +324,7 @@
qq{COPY datetime_arrays FROM 'file://$dir/nonesuch.csv'},
qr/nonesuch.csv doesn't exist/,
qr[\QSELECT * FROM file],
qr[\Q structure: "ts Array(Nullable(DateTime64(6, 'UTC'))), tsn Array(Nullable(DateTime64(6, 'UTC'))), tstz Array(Nullable(DateTime64(6, 'UTC'))), tstzn Array(Nullable(DateTime64(6, 'UTC'))), date Array(Nullable(Date32)), time Array(Nullable(Time64(6))), timen Array(Nullable(Time64(6))), ttz Array(Nullable(String)), ttzn Array(Nullable(String)), ival Array(Nullable(String))" }],
qr[\Q structure: "ts Array(Nullable(DateTime64(6))), tsn Array(Nullable(DateTime64(3))), tstz Array(Nullable(DateTime64(6, 'UTC'))), tstzn Array(Nullable(DateTime64(4, 'UTC'))), date Array(Nullable(Date32)), time Array(Nullable(Time64(6))), timen Array(Nullable(Time64(3))), ttz Array(Nullable(String)), ttzn Array(Nullable(String)), ival Array(Nullable(String))" }],
);
}

Expand Down
37 changes: 37 additions & 0 deletions test/expected/timestamp.out
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,43 @@ SELECT pg_read_file(:'tsv_path');

(1 row)

/************************ No offset ************************/
-- Text without offset reads as wall clock in Postgres timezone for timestamp,
-- UTC for timestamptz. ts stays 12:00 under every timezone.
\set naive_path :temp_dir /naive.tsv
\set naive_url file:// :naive_path
CREATE TABLE naive (id INT, ts TEXT, tstz TEXT);
INSERT INTO naive VALUES (1, '2026-08-28 12:00:00', '2026-08-28 12:00:00');
COPY naive TO :'naive_url';
SET timezone TO 'America/Los_Angeles';
TRUNCATE dt2;
COPY dt2 FROM :'naive_url';
SELECT * FROM dt2;
id | ts | tstz
----+---------------------+------------------------
1 | 2026-08-28 12:00:00 | 2026-08-28 05:00:00-07
(1 row)

SET timezone TO 'Asia/Tokyo';
TRUNCATE dt2;
COPY dt2 FROM :'naive_url';
SELECT * FROM dt2;
id | ts | tstz
----+---------------------+------------------------
1 | 2026-08-28 12:00:00 | 2026-08-28 21:00:00+09
(1 row)

-- Fixed offset, which chDB spells Fixed/UTC-07:00:00
SET TIME ZONE -7;
TRUNCATE dt2;
COPY dt2 FROM :'naive_url';
SELECT * FROM dt2;
id | ts | tstz
----+---------------------+------------------------
1 | 2026-08-28 12:00:00 | 2026-08-28 05:00:00-07
(1 row)

SET timezone TO 'UTC';
/************************ Default scales ************************/
-- A DateTime64 or Time64 named without a scale takes chDB's default of
-- milliseconds, truncating the microseconds Postgres holds.
Expand Down
26 changes: 26 additions & 0 deletions test/sql/timestamp.sql
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,32 @@ SELECT pg_read_file(:'tsv_path');
COPY date_times TO :'tsv_url' (structure 'id Int8, ts DateTime64(3, ''Japan''), tstz DateTime64(3, ''America/New_York'')');
SELECT pg_read_file(:'tsv_path');

/************************ No offset ************************/
-- Text without offset reads as wall clock in Postgres timezone for timestamp,
-- UTC for timestamptz. ts stays 12:00 under every timezone.
\set naive_path :temp_dir /naive.tsv
\set naive_url file:// :naive_path
CREATE TABLE naive (id INT, ts TEXT, tstz TEXT);
INSERT INTO naive VALUES (1, '2026-08-28 12:00:00', '2026-08-28 12:00:00');
COPY naive TO :'naive_url';

SET timezone TO 'America/Los_Angeles';
TRUNCATE dt2;
COPY dt2 FROM :'naive_url';
SELECT * FROM dt2;

SET timezone TO 'Asia/Tokyo';
TRUNCATE dt2;
COPY dt2 FROM :'naive_url';
SELECT * FROM dt2;

-- Fixed offset, which chDB spells Fixed/UTC-07:00:00
SET TIME ZONE -7;
TRUNCATE dt2;
COPY dt2 FROM :'naive_url';
SELECT * FROM dt2;
SET timezone TO 'UTC';

/************************ Default scales ************************/
-- A DateTime64 or Time64 named without a scale takes chDB's default of
-- milliseconds, truncating the microseconds Postgres holds.
Expand Down
Loading