Skip to content

PrepareBatch parses the optional TABLE keyword and table-function targets as part of the table name #1952

Description

@claude

Description

extractInsertQueryComponents (batch.go:25, regex normalizeInsertQueryMatch at batch.go:9) captures the insert target as ([^(]+)everything between INSERT INTO and the first (. Two valid ClickHouse INSERT forms are therefore misparsed:

  1. The optional TABLE keywordINSERT INTO TABLE tbl (col1, col2) is valid SQL. The parser returns tableName = "TABLE tbl".
  2. A table-function targetINSERT INTO FUNCTION null('id UInt32') / INSERT INTO TABLE FUNCTION ... are valid. The parser returns tableName = "FUNCTION null" / "TABLE FUNCTION null" (the argument list is dropped, since it lands in the optional column-list group).

The returned tableName is discarded on the native protocol (conn_batch.go:23 uses _), so native PrepareBatch still works — the server resolves the target and returns the block header. On the HTTP protocol it is interpolated straight into a DESCRIBE statement at conn_http_batch.go:17:

describeTableQuery := fmt.Sprintf("DESCRIBE TABLE %s", tableName)

which yields DESCRIBE TABLE TABLE tbl (a syntax error) and DESCRIBE TABLE FUNCTION null (target loses its arguments), so PrepareBatch fails for statements the server accepts. The only workaround is to pre-supply columns through the context (opt.columnNamesAndTypes).

Note that the other halves of the upstream report do not apply here: multi-line INSERTs parse correctly (Go's \s and negated character classes cross newlines), and double-quoted column names are already normalized by strings.Trim(..., "\"") at batch.go:45.

ClickHouse server version

Code analysis + unit test only; not verified against a running server (no ClickHouse instance was reachable in the investigation environment). The DESCRIBE TABLE TABLE tbl / DESCRIBE TABLE FUNCTION null failure on the HTTP path is inferred from the fmt.Sprintf above, not observed end to end. Upstream reports these INSERT forms accepted by ClickHouse 26.5.1.882.

Reproduction

batch_parse_repro_test.go (package clickhouse):

package clickhouse

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestInsertTargetParsing(t *testing.T) {
	cases := []struct {
		query             string
		expectedTableName string
		expectedColumns   []string
	}{
		{
			query:             "INSERT INTO TABLE table_name (col1, col2) VALUES (1, 2)",
			expectedTableName: "table_name",
			expectedColumns:   []string{"col1", "col2"},
		},
		{
			query:             "INSERT INTO TABLE table_name VALUES (1, 2)",
			expectedTableName: "table_name",
			expectedColumns:   []string{},
		},
		{
			query:             "INSERT INTO FUNCTION null('id UInt32') VALUES (1)",
			expectedTableName: "null('id UInt32')",
			expectedColumns:   []string{},
		},
	}

	for _, tc := range cases {
		t.Run(tc.query, func(t *testing.T) {
			_, tableName, columns, err := extractNormalizedInsertQueryAndColumns(tc.query)
			assert.NoError(t, err)
			fmt.Printf("query=%q -> tableName=%q columns=%v (DESCRIBE would be: %q)\n",
				tc.query, tableName, columns, fmt.Sprintf("DESCRIBE TABLE %s", tableName))
			assert.Equal(t, tc.expectedTableName, tableName)
			assert.Equal(t, tc.expectedColumns, columns)
		})
	}
}

go test -run TestInsertTargetParsing -v . — all three subtests fail:

query="INSERT INTO TABLE table_name (col1, col2) VALUES (1, 2)" -> tableName="TABLE table_name" columns=[col1 col2] (DESCRIBE would be: "DESCRIBE TABLE TABLE table_name")
    expected: "table_name"
    actual  : "TABLE table_name"

query="INSERT INTO TABLE table_name VALUES (1, 2)" -> tableName="TABLE table_name" columns=[] (DESCRIBE would be: "DESCRIBE TABLE TABLE table_name")
    expected: "table_name"
    actual  : "TABLE table_name"

query="INSERT INTO FUNCTION null('id UInt32') VALUES (1)" -> tableName="FUNCTION null" columns=[] (DESCRIBE would be: "DESCRIBE TABLE FUNCTION null")
    expected: "null('id UInt32')"
    actual  : "FUNCTION null"

Expected: the target name resolves to table_name in the first two cases (with the column list intact), and the table-function expression is either carried through whole (DESCRIBE TABLE null('id UInt32') is valid for table functions) or rejected with an actionable error rather than a mangled DESCRIBE.

Suggested fix

In normalizeInsertQueryMatch (batch.go:9), consume the optional TABLE keyword after INSERT INTO instead of letting it fall into the table-name group, and stop the table-name group at whitespace/( rather than at ( alone so a stray keyword cannot be absorbed. Table-function targets ([TABLE] FUNCTION <fn>(...)) need to be recognized as such: either preserve the full expression for the HTTP DESCRIBE (conn_http_batch.go:17), or return an explicit error explaining that HTTP batches into table functions require columnNamesAndTypes in the context.

Link

Relayed from ClickHouse/clickhouse-connect#932

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions