Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ All notable changes to this project will be documented in this file. It uses the

## Unreleased

### ⚡ Improvements

* Preserve declared fractional-second precision when mapping `time`,
`timestamp`, and `timestamptz` columns to chDB types ([#101]).
Comment thread
serprex marked this conversation as resolved.
Resolved

### 🐞 Bug Fixes

* Use PostgreSQL session `TimeZone` in chDB so `COPY FROM` preserves
`timestamp` wall-clock values when input omits time zone ([#101]).
* Preserve Azure SAS tokens between schema inference and data loading in
`CREATE TABLE ... COPY FROM` ([#103]).
* Fix GCC 15 build failures caused by discarded `const` qualifiers ([#103]).

[#101]: https://github.com/ClickHouse/pg_chdb/pull/101
"ClickHouse/pg_chdb#101 Have chDB use postgres session's TimeZone"
[#103]: https://github.com/ClickHouse/pg_chdb/pull/103
"ClickHouse/pg_chdb#103 Use const more"

Comment thread
serprex marked this conversation as resolved.
Resolved
Expand Down
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 @@ -89,8 +89,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 @@ -104,20 +104,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 @@ -152,7 +157,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
66 changes: 66 additions & 0 deletions test/expected/timestamp.out
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,72 @@ 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';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the default in pg_regress, FWIW.

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
Comment thread
theory marked this conversation as resolved.
Resolved
(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';
/************************ Declared precision ************************/
-- Inferred structure takes fractional digits from column typmod.
CREATE TABLE precisions (
ts TIMESTAMP(3) NOT NULL,
tstz TIMESTAMPTZ(1) NOT NULL,
t TIME(4) NOT NULL,
tn TIME(2) NULL
);
INSERT INTO precisions
VALUES ('2026-07-23 20:43:27.363', '2026-07-23 20:43:50.9Z', '13:45:24.2821', '13:45:24.28');
COPY precisions TO :'tsv_url';
SELECT pg_read_file(:'tsv_path');
pg_read_file
-------------------------------------------------------------------------------------
2026-07-23T20:43:27.363Z 2026-07-23T20:43:50.9Z 13:45:24.2821 13:45:24.28+
Comment thread
theory marked this conversation as resolved.
Resolved

(1 row)

-- Parquet replaces parameterized Time64 with String.
\set parquet_url file:// :temp_dir /precisions.parquet
COPY precisions TO :'parquet_url' (format 'Parquet');
TRUNCATE precisions;
COPY precisions FROM :'parquet_url' (format 'Parquet');
SELECT * FROM precisions;
ts | tstz | t | tn
-------------------------+--------------------------+---------------+-------------
2026-07-23 20:43:27.363 | 2026-07-23 20:43:50.9+00 | 13:45:24.2821 | 13:45:24.28
(1 row)

/************************ Default scales ************************/
-- A DateTime64 or Time64 named without a scale takes chDB's default of
-- milliseconds, truncating the microseconds Postgres holds.
Expand Down
48 changes: 48 additions & 0 deletions test/sql/timestamp.sql
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,54 @@ 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';

/************************ Declared precision ************************/
-- Inferred structure takes fractional digits from column typmod.
CREATE TABLE precisions (
ts TIMESTAMP(3) NOT NULL,
tstz TIMESTAMPTZ(1) NOT NULL,
t TIME(4) NOT NULL,
tn TIME(2) NULL
);

INSERT INTO precisions
VALUES ('2026-07-23 20:43:27.363', '2026-07-23 20:43:50.9Z', '13:45:24.2821', '13:45:24.28');

COPY precisions TO :'tsv_url';
SELECT pg_read_file(:'tsv_path');

-- Parquet replaces parameterized Time64 with String.
\set parquet_url file:// :temp_dir /precisions.parquet
COPY precisions TO :'parquet_url' (format 'Parquet');
TRUNCATE precisions;
COPY precisions FROM :'parquet_url' (format 'Parquet');
SELECT * FROM precisions;

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