@@ -55,20 +55,76 @@ var StringsRE = regexp.MustCompile(`(?i)'[^']+'`)
5555var SpecialCharsRE = regexp .MustCompile (`(?i)[)(*+\-/\\,]+` )
5656var FieldsNamesRE = regexp .MustCompile ("(?i)\\ w+|`[^`]+`\\ .`[^`]+`|\" [^\" ]+\" " )
5757
58+ func extractPartitionComponents (partitionExpr string ) []string {
59+ partitionExpr = strings .TrimSpace (partitionExpr )
60+ if strings .HasPrefix (partitionExpr , "(" ) && strings .HasSuffix (partitionExpr , ")" ) {
61+ inner := strings .TrimSpace (partitionExpr [1 : len (partitionExpr )- 1 ])
62+ return splitPartitionTuple (inner )
63+ }
64+ return []string {partitionExpr }
65+ }
66+
67+ func splitPartitionTuple (s string ) []string {
68+ var components []string
69+ var current strings.Builder
70+ depth := 0
71+ inQuote := false
72+ var quoteChar rune
73+
74+ for _ , ch := range s {
75+ switch ch {
76+ case '\'' , '"' :
77+ if ! inQuote {
78+ inQuote = true
79+ quoteChar = ch
80+ } else if ch == quoteChar {
81+ inQuote = false
82+ }
83+ current .WriteRune (ch )
84+ case '(' :
85+ if ! inQuote {
86+ depth ++
87+ }
88+ current .WriteRune (ch )
89+ case ')' :
90+ if ! inQuote {
91+ depth --
92+ }
93+ current .WriteRune (ch )
94+ case ',' :
95+ if ! inQuote && depth == 0 {
96+ // Top-level comma, split here
97+ components = append (components , strings .TrimSpace (current .String ()))
98+ current .Reset ()
99+ } else {
100+ current .WriteRune (ch )
101+ }
102+ default :
103+ current .WriteRune (ch )
104+ }
105+ }
106+
107+ if current .Len () > 0 {
108+ components = append (components , strings .TrimSpace (current .String ()))
109+ }
110+
111+ return components
112+ }
113+
58114func extractPartitionByFieldNames (s string ) []struct {
59115 Name string `ch:"name"`
60116} {
61117 s = SettingsRE .ReplaceAllString (s , "" )
62118 s = OrderByRE .ReplaceAllString (s , "" )
63119 s = FunctionsRE .ReplaceAllString (s , "" )
64120 s = StringsRE .ReplaceAllString (s , "" )
65- s = SpecialCharsRE .ReplaceAllString (s , "" )
121+ s = SpecialCharsRE .ReplaceAllString (s , " " )
66122 matches := FieldsNamesRE .FindAllString (s , - 1 )
67123 columns := make ([]struct {
68124 Name string `ch:"name"`
69125 }, len (matches ))
70126 for i := range matches {
71- columns [i ].Name = matches [i ]
127+ columns [i ].Name = strings . TrimSpace ( matches [i ])
72128 }
73129 return columns
74130}
@@ -77,6 +133,148 @@ func GetPartitionIdAndName(ctx context.Context, ch *clickhouse.ClickHouse, datab
77133 if ! strings .Contains (createQuery , "MergeTree" ) || ! PartitionByRE .MatchString (createQuery ) {
78134 return "" , "" , nil
79135 }
136+
137+ partitionByMatches := PartitionByRE .FindStringSubmatch (createQuery )
138+ if len (partitionByMatches ) < 2 || partitionByMatches [1 ] == "" {
139+ return "" , "" , nil
140+ }
141+ partitionExpr := strings .TrimSpace (partitionByMatches [1 ])
142+
143+ version , err := ch .GetVersion (ctx )
144+ if err != nil {
145+ log .Warn ().Msgf ("can't get ClickHouse version, falling back to temporary table: %v" , err )
146+ }
147+
148+ if version >= 21008000 {
149+ partitionId , partitionName , err := getPartitionIdWithFunction (ctx , ch , createQuery , partitionExpr , partition )
150+ if err != nil {
151+ log .Warn ().Msgf ("partitionId function failed, using temporary table: %v" , err )
152+ return getPartitionIdWithTempTable (ctx , ch , database , table , createQuery , partition , partitionByMatches )
153+ }
154+ return partitionId , partitionName , nil
155+ }
156+
157+ return getPartitionIdWithTempTable (ctx , ch , database , table , createQuery , partition , partitionByMatches )
158+ }
159+
160+ func getPartitionIdWithFunction (ctx context.Context , ch * clickhouse.ClickHouse , createQuery , partitionExpr , partition string ) (string , string , error ) {
161+ partitionValues := splitAndParsePartition (partition )
162+ partitionComponents := extractPartitionComponents (partitionExpr )
163+
164+ if len (partitionValues ) != len (partitionComponents ) {
165+ return "" , "" , fmt .Errorf ("partition values count (%d) doesn't match components count (%d)" , len (partitionValues ), len (partitionComponents ))
166+ }
167+
168+ directId , directName , directErr := tryDirectPartitionId (ctx , ch , partitionValues )
169+ evaluatedId , evaluatedName , evaluatedErr := tryEvaluatedPartitionId (ctx , ch , createQuery , partitionExpr , partitionComponents , partitionValues )
170+
171+ if evaluatedErr == nil {
172+ return evaluatedId , evaluatedName , nil
173+ }
174+
175+ if directErr == nil {
176+ return directId , directName , nil
177+ }
178+
179+ return "" , "" , fmt .Errorf ("both approaches failed: direct=%v, evaluated=%v" , directErr , evaluatedErr )
180+ }
181+
182+ func tryDirectPartitionId (ctx context.Context , ch * clickhouse.ClickHouse , partitionValues []interface {}) (string , string , error ) {
183+ placeholders := make ([]string , len (partitionValues ))
184+ var args []interface {}
185+
186+ for i := range partitionValues {
187+ placeholders [i ] = "?"
188+ args = append (args , partitionValues [i ])
189+ }
190+
191+ var partitionNameExpr string
192+ if len (partitionValues ) == 1 {
193+ partitionNameExpr = "toString(?)"
194+ args = append (args , partitionValues [0 ])
195+ } else {
196+ partitionNameExpr = fmt .Sprintf ("toString((%s))" , strings .Join (placeholders , ", " ))
197+ args = append (args , partitionValues ... )
198+ }
199+
200+ sql := fmt .Sprintf ("SELECT partitionId(%s) AS partition_id, %s AS partition_name" ,
201+ strings .Join (placeholders , ", " ), partitionNameExpr )
202+
203+ var result []struct {
204+ PartitionId string `ch:"partition_id"`
205+ PartitionName string `ch:"partition_name"`
206+ }
207+
208+ if err := ch .SelectContext (ctx , & result , sql , args ... ); err != nil {
209+ return "" , "" , err
210+ }
211+
212+ if len (result ) != 1 {
213+ return "" , "" , fmt .Errorf ("unexpected result count: %d" , len (result ))
214+ }
215+
216+ return result [0 ].PartitionId , result [0 ].PartitionName , nil
217+ }
218+
219+ func tryEvaluatedPartitionId (ctx context.Context , ch * clickhouse.ClickHouse , createQuery , partitionExpr string , partitionComponents []string , partitionValues []interface {}) (string , string , error ) {
220+ columns := extractPartitionByFieldNames (partitionExpr )
221+ if len (columns ) == 0 {
222+ return "" , "" , fmt .Errorf ("can't extract column names from partition expression: %s" , partitionExpr )
223+ }
224+
225+ if len (partitionValues ) != len (columns ) {
226+ return "" , "" , fmt .Errorf ("partition values count (%d) doesn't match columns count (%d)" , len (partitionValues ), len (columns ))
227+ }
228+
229+ var args []interface {}
230+ selectItems := make ([]string , len (columns ))
231+ for i , col := range columns {
232+ columnType := inferColumnType (createQuery , col .Name , partitionValues [i ])
233+ selectItems [i ] = fmt .Sprintf ("CAST(? AS %s) AS %s" , columnType , col .Name )
234+ args = append (args , partitionValues [i ])
235+ }
236+
237+ partitionIdArgs := partitionExpr
238+ if len (partitionComponents ) > 0 {
239+ partitionIdArgs = strings .Join (partitionComponents , ", " )
240+ }
241+
242+ sql := fmt .Sprintf ("SELECT partitionId(%s) AS partition_id, toString(%s) AS partition_name FROM (SELECT %s)" ,
243+ partitionIdArgs , partitionExpr , strings .Join (selectItems , ", " ))
244+
245+ var result []struct {
246+ PartitionId string `ch:"partition_id"`
247+ PartitionName string `ch:"partition_name"`
248+ }
249+
250+ if err := ch .SelectContext (ctx , & result , sql , args ... ); err != nil {
251+ return "" , "" , errors .Wrapf (err , "can't execute partitionId query: sql=%s, args=%#v" , sql , args )
252+ }
253+
254+ if len (result ) != 1 {
255+ return "" , "" , fmt .Errorf ("unexpected result: %#v" , result )
256+ }
257+
258+ return result [0 ].PartitionId , result [0 ].PartitionName , nil
259+ }
260+
261+ func inferColumnType (createQuery , columnName string , value interface {}) string {
262+ re := regexp .MustCompile (fmt .Sprintf (`(?i)[\s,\(]%s\s+([a-zA-Z0-9\(\)]+)` , regexp .QuoteMeta (columnName )))
263+ if matches := re .FindStringSubmatch (createQuery ); len (matches ) >= 2 {
264+ return matches [1 ]
265+ }
266+
267+ switch value .(type ) {
268+ case int64 :
269+ return "Int64"
270+ case float64 :
271+ return "Float64"
272+ default :
273+ return "String"
274+ }
275+ }
276+
277+ func getPartitionIdWithTempTable (ctx context.Context , ch * clickhouse.ClickHouse , database , table , createQuery , partition string , partitionByMatches []string ) (string , string , error ) {
80278 createQuery = replicatedMergeTreeRE .ReplaceAllString (createQuery , "$1($4)$5" )
81279 if len (uuidRE .FindAllString (createQuery , - 1 )) > 0 {
82280 newUUID , _ := uuid .NewUUID ()
@@ -99,7 +297,6 @@ func GetPartitionIdAndName(ctx context.Context, ch *clickhouse.ClickHouse, datab
99297 }, 0 )
100298 sql := "SELECT name FROM system.columns WHERE database=? AND table=? AND is_in_partition_key"
101299 oldVersion := false
102- partitionByMatches := PartitionByRE .FindStringSubmatch (createQuery )
103300 if err := ch .SelectContext (ctx , & columns , sql , database , partitionIdTable ); err != nil {
104301 if len (partitionByMatches ) == 0 {
105302 if dropErr := dropPartitionIdTable (ch , database , partitionIdTable ); dropErr != nil {
0 commit comments