Skip to content

Inserting timestamps loses precision down to second #1545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
9 tasks done
txomon opened this issue Apr 21, 2025 · 2 comments
Open
9 tasks done

Inserting timestamps loses precision down to second #1545

txomon opened this issue Apr 21, 2025 · 2 comments

Comments

@txomon
Copy link

txomon commented Apr 21, 2025

Observed

  1. Created a table with DateTime64(6) type
  2. Inserted timestamps with nanosecond precision using InsertAsync()
  3. Queries returned timestamps with second precision

Expected behaviour

When inserting timestamps, using Insert into testtable values (?,?) format the timestamps lose precision.

This fact is not documented nor explicit, causing likely bugs.

Code example

import (
	"context"
	"testing"
	"time"

	"github.com/ClickHouse/clickhouse-go/v2"
	"github.com/stretchr/testify/require"
)

func TestAsyncInsertQuestionmarkLosesPrecision(t *testing.T) {
	t.Parallel()
	ctx := context.Background()

	// Parse DSN and create connection
	opt, err := clickhouse.ParseDSN("clickhouse://user:password@clickhouse:9000/default?secure=false&skip_verify=true")
	require.NoError(t, err)

	conn, err := clickhouse.Open(opt)
	require.NoError(t, err)
	defer conn.Close()

	// Create test table
	err = conn.Exec(ctx, `
		CREATE TABLE IF NOT EXISTS test_async_insert_questionmark_loses_precision (
			id Int32,
			created_at DateTime64(6),
		) ENGINE = MergeTree()
		ORDER BY id
	`)
	require.NoError(t, err)

	// Clean up table after test
	defer func() {
		_ = conn.Exec(ctx, "DROP TABLE IF EXISTS test_async_insert_questionmark_loses_precision")
	}()

	// Insert data with multiple values
	now := time.Now().UTC().Truncate(time.Microsecond)
	err = conn.AsyncInsert(ctx, `
		INSERT INTO test_async_insert_questionmark_loses_precision (id, created_at)
		VALUES (?, ?)
	`, true, int32(1), now)
	require.NoError(t, err)

	// Query the data back
	rows, err := conn.Query(ctx, "SELECT id, created_at FROM test_async_insert_questionmark_loses_precision WHERE id = 1")
	require.NoError(t, err)
	defer rows.Close()

	// Read the results
	var (
		id        int32
		createdAt time.Time
	)

	require.True(t, rows.Next())
	err = rows.Scan(&id, &createdAt)
	require.NoError(t, err)

	// Verify the data matches what we inserted
	require.Equal(t, int32(1), id)
	require.Equal(t, now, createdAt)
}

Error log


Expected :time.Date(2025, time.April, 21, 11, 43, 4, 352652000, time.UTC)
Actual   :time.Date(2025, time.April, 21, 11, 43, 4, 0, time.UTC)

Details

Environment

  • clickhouse-go version: master
  • Interface: ClickHouse API
  • Go version: 1.24
  • Operating system: linux
  • ClickHouse version: doesn't matter
  • Is it a ClickHouse Cloud? no
  • ClickHouse Server non-default settings, if any: none
  • CREATE TABLE statements for tables involved: provided in the testcase
  • Sample data for all these tables, use clickhouse-obfuscator if necessary
@txomon
Copy link
Author

txomon commented Apr 21, 2025

For completeness, I have tried to see if there was the same issue when using parameters or batch insert but both were good:

func TestAsyncInsertPlaceholderLosesPrecision(t *testing.T) {
	t.Parallel()
	ctx := context.Background()

	// Parse DSN and create connection
	opt, err := clickhouse.ParseDSN("clickhouse://user:password@clickhouse:9000/default?secure=false&skip_verify=true")
	require.NoError(t, err)

	conn, err := clickhouse.Open(opt)
	require.NoError(t, err)
	defer conn.Close()

	// Create test table
	err = conn.Exec(ctx, `
		CREATE TABLE IF NOT EXISTS test_async_insert_placeholder_loses_precision (
			id Int32,
			created_at DateTime64(6),
		) ENGINE = MergeTree()
		ORDER BY id
	`)
	require.NoError(t, err)

	// Clean up table after test
	defer func() {
		_ = conn.Exec(ctx, "DROP TABLE IF EXISTS test_async_insert_placeholder_loses_precision")
	}()

	// Insert data with multiple values
	now := time.Now().UTC().Truncate(time.Microsecond)
	ctx = clickhouse.Context(ctx, clickhouse.WithParameters(clickhouse.Parameters{
		"id":         "1",
		"created_at": now.Format("2006-01-02 15:04:05.999999"),
	}))
	err = conn.AsyncInsert(ctx, `
		INSERT INTO test_async_insert_placeholder_loses_precision (id, created_at)
		VALUES ({id: Int32}, {created_at: DateTime64(6)})
	`, true)
	require.NoError(t, err)

	// Query the data back
	rows, err := conn.Query(ctx, "SELECT id, created_at FROM test_async_insert_placeholder_loses_precision WHERE id = 1")
	require.NoError(t, err)
	defer rows.Close()

	// Read the results
	var (
		id        int32
		createdAt time.Time
	)

	require.True(t, rows.Next())
	err = rows.Scan(&id, &createdAt)
	require.NoError(t, err)

	// Verify the data matches what we inserted
	require.Equal(t, int32(1), id)
	require.Equal(t, now, createdAt)
}

func TestBatchInsertLosesPrecision(t *testing.T) {
	t.Parallel()
	ctx := context.Background()

	// Parse DSN and create connection
	opt, err := clickhouse.ParseDSN("clickhouse://user:password@clickhouse:9000/default?secure=false&skip_verify=true")
	require.NoError(t, err)

	conn, err := clickhouse.Open(opt)
	require.NoError(t, err)
	defer conn.Close()

	// Create test table
	err = conn.Exec(ctx, `
		CREATE TABLE IF NOT EXISTS test_batch_insert_loses_precision (
			id Int32,
			created_at DateTime64(6)
		) ENGINE = MergeTree()
		ORDER BY id
	`)
	require.NoError(t, err)

	// Clean up table after test
	defer func() {
		_ = conn.Exec(ctx, "DROP TABLE IF EXISTS test_batch_insert_loses_precision")
	}()

	// Insert data with multiple values
	now := time.Now().UTC().Truncate(time.Microsecond)

	batch, err := conn.PrepareBatch(ctx, `INSERT INTO test_batch_insert_loses_precision (id, created_at)`)
	require.NoError(t, err)
	err = batch.Append(int32(1), now)
	require.NoError(t, err)

	err = batch.Send()
	require.NoError(t, err)

	// Query the data back
	rows, err := conn.Query(ctx, "SELECT id, created_at FROM test_batch_insert_loses_precision WHERE id = 1")
	require.NoError(t, err)
	defer rows.Close()

	// Read the results
	var (
		id        int32
		createdAt time.Time
	)

	require.True(t, rows.Next())
	err = rows.Scan(&id, &createdAt)
	require.NoError(t, err)

	// Verify the data matches what we inserted
	require.Equal(t, int32(1), id)
	require.Equal(t, now, createdAt)
}

@titanproger
Copy link

Try this:

now := time.Now().UTC().Truncate(time.Microsecond) err = conn.AsyncInsert(ctx,
INSERT INTO test_async_insert_questionmark_loses_precision (id, created_at)
VALUES (@param_id, @param_created_at)
, true, clickhouse.Named("param_id", 1), clickhouse.DateNamed("param_created_at", now, clickhouse.MicroSeconds)) require.NoError(t, err)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants