Skip to content

Commit b75cce0

Browse files
Ajit Pratap Singhclaude
authored andcommitted
feat: add MATERIALIZED CTE support and improve metrics
Features: - Add MATERIALIZED/NOT MATERIALIZED parsing for CTEs (SQL standard) - Add comprehensive parser and AST metrics tracking - Add pool metrics for AST, statement, and expression objects Fixes: - Fix tokenizer negative column numbers in toSQLPosition - Correct column calculation to be consistently 1-based Metrics additions: - RecordParse() for parser operation tracking - RecordASTPoolGet/Put() for AST pool metrics - RecordStatementPoolGet/Put() for statement pool metrics - RecordExpressionPoolGet/Put() for expression pool metrics - Extended Stats struct with parser and pool metrics Tests: - Add TestParser_MaterializedCTE test cases - Update tokenizer error location test for correct column value - Add column validation to fuzz tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8d4b2f7 commit b75cce0

8 files changed

Lines changed: 353 additions & 45 deletions

File tree

pkg/metrics/metrics.go

Lines changed: 224 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,22 @@ type Metrics struct {
1515
tokenizeDuration int64 // Total tokenization time in nanoseconds
1616
lastTokenizeTime int64 // Last tokenization timestamp
1717

18-
// Memory metrics
18+
// Parser metrics
19+
parseOperations int64 // Total parse operations
20+
parseErrors int64 // Total parse errors
21+
parseDuration int64 // Total parse time in nanoseconds
22+
lastParseTime int64 // Last parse timestamp
23+
statementsCreated int64 // Total statements parsed
24+
25+
// AST pool metrics
26+
astPoolGets int64 // AST pool retrievals
27+
astPoolPuts int64 // AST pool returns
28+
stmtPoolGets int64 // Statement pool retrievals
29+
stmtPoolPuts int64 // Statement pool returns
30+
exprPoolGets int64 // Expression pool retrievals
31+
exprPoolPuts int64 // Expression pool returns
32+
33+
// Memory metrics (tokenizer pool)
1934
poolGets int64 // Total pool retrievals
2035
poolPuts int64 // Total pool returns
2136
poolMisses int64 // Pool misses (had to create new)
@@ -119,23 +134,120 @@ func RecordPoolPut() {
119134
atomic.AddInt64(&globalMetrics.poolPuts, 1)
120135
}
121136

137+
// RecordParse records a parse operation
138+
func RecordParse(duration time.Duration, statementCount int, err error) {
139+
if atomic.LoadInt32(&globalMetrics.enabled) == 0 {
140+
return
141+
}
142+
143+
// Record operation
144+
atomic.AddInt64(&globalMetrics.parseOperations, 1)
145+
atomic.AddInt64(&globalMetrics.parseDuration, int64(duration))
146+
atomic.StoreInt64(&globalMetrics.lastParseTime, time.Now().UnixNano())
147+
atomic.AddInt64(&globalMetrics.statementsCreated, int64(statementCount))
148+
149+
// Record errors
150+
if err != nil {
151+
atomic.AddInt64(&globalMetrics.parseErrors, 1)
152+
153+
// Record error by type
154+
errorType := "parse:" + err.Error()
155+
globalMetrics.errorsMutex.Lock()
156+
globalMetrics.errorsByType[errorType]++
157+
globalMetrics.errorsMutex.Unlock()
158+
}
159+
}
160+
161+
// RecordASTPoolGet records an AST pool retrieval
162+
func RecordASTPoolGet() {
163+
if atomic.LoadInt32(&globalMetrics.enabled) == 0 {
164+
return
165+
}
166+
atomic.AddInt64(&globalMetrics.astPoolGets, 1)
167+
}
168+
169+
// RecordASTPoolPut records an AST pool return
170+
func RecordASTPoolPut() {
171+
if atomic.LoadInt32(&globalMetrics.enabled) == 0 {
172+
return
173+
}
174+
atomic.AddInt64(&globalMetrics.astPoolPuts, 1)
175+
}
176+
177+
// RecordStatementPoolGet records a statement pool retrieval
178+
func RecordStatementPoolGet() {
179+
if atomic.LoadInt32(&globalMetrics.enabled) == 0 {
180+
return
181+
}
182+
atomic.AddInt64(&globalMetrics.stmtPoolGets, 1)
183+
}
184+
185+
// RecordStatementPoolPut records a statement pool return
186+
func RecordStatementPoolPut() {
187+
if atomic.LoadInt32(&globalMetrics.enabled) == 0 {
188+
return
189+
}
190+
atomic.AddInt64(&globalMetrics.stmtPoolPuts, 1)
191+
}
192+
193+
// RecordExpressionPoolGet records an expression pool retrieval
194+
func RecordExpressionPoolGet() {
195+
if atomic.LoadInt32(&globalMetrics.enabled) == 0 {
196+
return
197+
}
198+
atomic.AddInt64(&globalMetrics.exprPoolGets, 1)
199+
}
200+
201+
// RecordExpressionPoolPut records an expression pool return
202+
func RecordExpressionPoolPut() {
203+
if atomic.LoadInt32(&globalMetrics.enabled) == 0 {
204+
return
205+
}
206+
atomic.AddInt64(&globalMetrics.exprPoolPuts, 1)
207+
}
208+
122209
// Stats represents current performance statistics
123210
type Stats struct {
124-
// Basic counts
211+
// Tokenization counts
125212
TokenizeOperations int64 `json:"tokenize_operations"`
126213
TokenizeErrors int64 `json:"tokenize_errors"`
127-
ErrorRate float64 `json:"error_rate"`
214+
TokenizeErrorRate float64 `json:"tokenize_error_rate"`
128215

129-
// Performance metrics
130-
AverageDuration time.Duration `json:"average_duration"`
131-
OperationsPerSecond float64 `json:"operations_per_second"`
216+
// Parser counts
217+
ParseOperations int64 `json:"parse_operations"`
218+
ParseErrors int64 `json:"parse_errors"`
219+
ParseErrorRate float64 `json:"parse_error_rate"`
220+
StatementsCreated int64 `json:"statements_created"`
132221

133-
// Memory/Pool metrics
222+
// Tokenization performance metrics
223+
AverageTokenizeDuration time.Duration `json:"average_tokenize_duration"`
224+
TokenizeOperationsPerSecond float64 `json:"tokenize_operations_per_second"`
225+
226+
// Parser performance metrics
227+
AverageParseDuration time.Duration `json:"average_parse_duration"`
228+
ParseOperationsPerSecond float64 `json:"parse_operations_per_second"`
229+
230+
// Tokenizer pool metrics
134231
PoolGets int64 `json:"pool_gets"`
135232
PoolPuts int64 `json:"pool_puts"`
136233
PoolBalance int64 `json:"pool_balance"`
137234
PoolMissRate float64 `json:"pool_miss_rate"`
138235

236+
// AST pool metrics
237+
ASTPoolGets int64 `json:"ast_pool_gets"`
238+
ASTPoolPuts int64 `json:"ast_pool_puts"`
239+
ASTPoolBalance int64 `json:"ast_pool_balance"`
240+
241+
// Statement pool metrics
242+
StmtPoolGets int64 `json:"stmt_pool_gets"`
243+
StmtPoolPuts int64 `json:"stmt_pool_puts"`
244+
StmtPoolBalance int64 `json:"stmt_pool_balance"`
245+
246+
// Expression pool metrics
247+
ExprPoolGets int64 `json:"expr_pool_gets"`
248+
ExprPoolPuts int64 `json:"expr_pool_puts"`
249+
ExprPoolBalance int64 `json:"expr_pool_balance"`
250+
139251
// Query size metrics
140252
MinQuerySize int64 `json:"min_query_size"`
141253
MaxQuerySize int64 `json:"max_query_size"`
@@ -148,6 +260,9 @@ type Stats struct {
148260

149261
// Error breakdown
150262
ErrorsByType map[string]int64 `json:"errors_by_type"`
263+
264+
// Legacy field for backwards compatibility
265+
ErrorRate float64 `json:"error_rate"`
151266
}
152267

153268
// GetStats returns current performance statistics
@@ -156,49 +271,115 @@ func GetStats() Stats {
156271
return Stats{}
157272
}
158273

159-
operations := atomic.LoadInt64(&globalMetrics.tokenizeOperations)
160-
errors := atomic.LoadInt64(&globalMetrics.tokenizeErrors)
161-
duration := atomic.LoadInt64(&globalMetrics.tokenizeDuration)
274+
// Tokenization metrics
275+
tokenizeOps := atomic.LoadInt64(&globalMetrics.tokenizeOperations)
276+
tokenizeErrs := atomic.LoadInt64(&globalMetrics.tokenizeErrors)
277+
tokenizeDur := atomic.LoadInt64(&globalMetrics.tokenizeDuration)
278+
lastTokenizeTime := atomic.LoadInt64(&globalMetrics.lastTokenizeTime)
279+
280+
// Parser metrics
281+
parseOps := atomic.LoadInt64(&globalMetrics.parseOperations)
282+
parseErrs := atomic.LoadInt64(&globalMetrics.parseErrors)
283+
parseDur := atomic.LoadInt64(&globalMetrics.parseDuration)
284+
lastParseTime := atomic.LoadInt64(&globalMetrics.lastParseTime)
285+
stmtsCreated := atomic.LoadInt64(&globalMetrics.statementsCreated)
286+
287+
// Pool metrics
162288
poolGets := atomic.LoadInt64(&globalMetrics.poolGets)
163289
poolPuts := atomic.LoadInt64(&globalMetrics.poolPuts)
164290
poolMisses := atomic.LoadInt64(&globalMetrics.poolMisses)
291+
292+
// AST pool metrics
293+
astPoolGets := atomic.LoadInt64(&globalMetrics.astPoolGets)
294+
astPoolPuts := atomic.LoadInt64(&globalMetrics.astPoolPuts)
295+
stmtPoolGets := atomic.LoadInt64(&globalMetrics.stmtPoolGets)
296+
stmtPoolPuts := atomic.LoadInt64(&globalMetrics.stmtPoolPuts)
297+
exprPoolGets := atomic.LoadInt64(&globalMetrics.exprPoolGets)
298+
exprPoolPuts := atomic.LoadInt64(&globalMetrics.exprPoolPuts)
299+
300+
// Query size metrics
165301
minSize := atomic.LoadInt64(&globalMetrics.minQuerySize)
166302
maxSize := atomic.LoadInt64(&globalMetrics.maxQuerySize)
167303
totalBytes := atomic.LoadInt64(&globalMetrics.totalQueryBytes)
168-
lastOpTime := atomic.LoadInt64(&globalMetrics.lastTokenizeTime)
169304

170305
// Load start time atomically
171306
startTime := globalMetrics.startTime.Load().(time.Time)
172307

173308
stats := Stats{
174-
TokenizeOperations: operations,
175-
TokenizeErrors: errors,
176-
PoolGets: poolGets,
177-
PoolPuts: poolPuts,
178-
PoolBalance: poolGets - poolPuts,
309+
// Tokenization
310+
TokenizeOperations: tokenizeOps,
311+
TokenizeErrors: tokenizeErrs,
312+
313+
// Parser
314+
ParseOperations: parseOps,
315+
ParseErrors: parseErrs,
316+
StatementsCreated: stmtsCreated,
317+
318+
// Tokenizer pool
319+
PoolGets: poolGets,
320+
PoolPuts: poolPuts,
321+
PoolBalance: poolGets - poolPuts,
322+
323+
// AST pools
324+
ASTPoolGets: astPoolGets,
325+
ASTPoolPuts: astPoolPuts,
326+
ASTPoolBalance: astPoolGets - astPoolPuts,
327+
StmtPoolGets: stmtPoolGets,
328+
StmtPoolPuts: stmtPoolPuts,
329+
StmtPoolBalance: stmtPoolGets - stmtPoolPuts,
330+
ExprPoolGets: exprPoolGets,
331+
ExprPoolPuts: exprPoolPuts,
332+
ExprPoolBalance: exprPoolGets - exprPoolPuts,
333+
334+
// Query size
179335
MinQuerySize: minSize,
180336
MaxQuerySize: maxSize,
181337
TotalBytesProcessed: totalBytes,
182338
Uptime: time.Since(startTime),
183339
}
184340

185-
// Calculate rates and averages
186-
if operations > 0 {
187-
stats.ErrorRate = float64(errors) / float64(operations)
188-
stats.AverageDuration = time.Duration(duration / operations)
189-
stats.AverageQuerySize = float64(totalBytes) / float64(operations)
341+
// Calculate tokenization rates and averages
342+
if tokenizeOps > 0 {
343+
stats.TokenizeErrorRate = float64(tokenizeErrs) / float64(tokenizeOps)
344+
stats.AverageTokenizeDuration = time.Duration(tokenizeDur / tokenizeOps)
345+
stats.AverageQuerySize = float64(totalBytes) / float64(tokenizeOps)
190346

191347
// Operations per second
192348
uptime := time.Since(startTime).Seconds()
193349
if uptime > 0 {
194-
stats.OperationsPerSecond = float64(operations) / uptime
350+
stats.TokenizeOperationsPerSecond = float64(tokenizeOps) / uptime
195351
}
196352
}
197353

354+
// Calculate parse rates and averages
355+
if parseOps > 0 {
356+
stats.ParseErrorRate = float64(parseErrs) / float64(parseOps)
357+
stats.AverageParseDuration = time.Duration(parseDur / parseOps)
358+
359+
// Operations per second
360+
uptime := time.Since(startTime).Seconds()
361+
if uptime > 0 {
362+
stats.ParseOperationsPerSecond = float64(parseOps) / uptime
363+
}
364+
}
365+
366+
// Calculate pool miss rate
198367
if poolGets > 0 {
199368
stats.PoolMissRate = float64(poolMisses) / float64(poolGets)
200369
}
201370

371+
// Legacy error rate (combined)
372+
totalOps := tokenizeOps + parseOps
373+
totalErrs := tokenizeErrs + parseErrs
374+
if totalOps > 0 {
375+
stats.ErrorRate = float64(totalErrs) / float64(totalOps)
376+
}
377+
378+
// Determine last operation time (most recent of tokenize or parse)
379+
lastOpTime := lastTokenizeTime
380+
if lastParseTime > lastOpTime {
381+
lastOpTime = lastParseTime
382+
}
202383
if lastOpTime > 0 {
203384
stats.LastOperationTime = time.Unix(0, lastOpTime)
204385
}
@@ -216,17 +397,38 @@ func GetStats() Stats {
216397

217398
// Reset clears all metrics (useful for testing)
218399
func Reset() {
400+
// Tokenization metrics
219401
atomic.StoreInt64(&globalMetrics.tokenizeOperations, 0)
220402
atomic.StoreInt64(&globalMetrics.tokenizeErrors, 0)
221403
atomic.StoreInt64(&globalMetrics.tokenizeDuration, 0)
222404
atomic.StoreInt64(&globalMetrics.lastTokenizeTime, 0)
405+
406+
// Parser metrics
407+
atomic.StoreInt64(&globalMetrics.parseOperations, 0)
408+
atomic.StoreInt64(&globalMetrics.parseErrors, 0)
409+
atomic.StoreInt64(&globalMetrics.parseDuration, 0)
410+
atomic.StoreInt64(&globalMetrics.lastParseTime, 0)
411+
atomic.StoreInt64(&globalMetrics.statementsCreated, 0)
412+
413+
// Tokenizer pool metrics
223414
atomic.StoreInt64(&globalMetrics.poolGets, 0)
224415
atomic.StoreInt64(&globalMetrics.poolPuts, 0)
225416
atomic.StoreInt64(&globalMetrics.poolMisses, 0)
417+
418+
// AST pool metrics
419+
atomic.StoreInt64(&globalMetrics.astPoolGets, 0)
420+
atomic.StoreInt64(&globalMetrics.astPoolPuts, 0)
421+
atomic.StoreInt64(&globalMetrics.stmtPoolGets, 0)
422+
atomic.StoreInt64(&globalMetrics.stmtPoolPuts, 0)
423+
atomic.StoreInt64(&globalMetrics.exprPoolGets, 0)
424+
atomic.StoreInt64(&globalMetrics.exprPoolPuts, 0)
425+
426+
// Query size metrics
226427
atomic.StoreInt64(&globalMetrics.minQuerySize, -1)
227428
atomic.StoreInt64(&globalMetrics.maxQuerySize, 0)
228429
atomic.StoreInt64(&globalMetrics.totalQueryBytes, 0)
229430

431+
// Error tracking
230432
globalMetrics.errorsMutex.Lock()
231433
globalMetrics.errorsByType = make(map[string]int64)
232434
globalMetrics.errorsMutex.Unlock()

pkg/metrics/metrics_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ func TestMetricsBasicFunctionality(t *testing.T) {
9595
}
9696

9797
// Verify timing
98-
if stats.AverageDuration <= 0 {
99-
t.Error("Average duration should be positive")
98+
if stats.AverageTokenizeDuration <= 0 {
99+
t.Error("Average tokenize duration should be positive")
100100
}
101101

102-
if stats.OperationsPerSecond <= 0 {
103-
t.Error("Operations per second should be positive")
102+
if stats.TokenizeOperationsPerSecond <= 0 {
103+
t.Error("Tokenize operations per second should be positive")
104104
}
105105

106106
if stats.Uptime <= 0 {

pkg/sql/ast/ast.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@ func (w WithClause) Children() []Node {
5656
// CommonTableExpr represents a single Common Table Expression in a WITH clause.
5757
// It supports optional column specifications and any statement type as the CTE query.
5858
// Phase 2 Complete: Full parser support with column specifications.
59+
// Phase 2.6: Added MATERIALIZED/NOT MATERIALIZED support for query optimization hints.
5960
type CommonTableExpr struct {
6061
Name string
6162
Columns []string
6263
Statement Statement
63-
Materialized *bool // TODO: Add MATERIALIZED/NOT MATERIALIZED parsing support
64+
Materialized *bool // nil = default, true = MATERIALIZED, false = NOT MATERIALIZED
6465
}
6566

6667
func (c *CommonTableExpr) statementNode() {}

pkg/sql/parser/cte.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,23 @@ func (p *Parser) parseCommonTableExpr() (*ast.CommonTableExpr, error) {
129129
}
130130
p.advance()
131131

132+
// Parse optional MATERIALIZED / NOT MATERIALIZED
133+
// Syntax: AS [NOT] MATERIALIZED (query)
134+
var materialized *bool
135+
if p.isType(models.TokenTypeNot) {
136+
p.advance() // Consume NOT
137+
if !p.isType(models.TokenTypeMaterialized) {
138+
return nil, p.expectedError("MATERIALIZED after NOT")
139+
}
140+
p.advance() // Consume MATERIALIZED
141+
notMaterialized := false
142+
materialized = &notMaterialized
143+
} else if p.isType(models.TokenTypeMaterialized) {
144+
p.advance() // Consume MATERIALIZED
145+
isMaterialized := true
146+
materialized = &isMaterialized
147+
}
148+
132149
// Parse the CTE query (must be in parentheses)
133150
if !p.isType(models.TokenTypeLParen) {
134151
return nil, p.expectedError("( before CTE query")
@@ -147,9 +164,10 @@ func (p *Parser) parseCommonTableExpr() (*ast.CommonTableExpr, error) {
147164
p.advance() // Consume )
148165

149166
return &ast.CommonTableExpr{
150-
Name: name,
151-
Columns: columns,
152-
Statement: stmt,
167+
Name: name,
168+
Columns: columns,
169+
Statement: stmt,
170+
Materialized: materialized,
153171
}, nil
154172
}
155173

0 commit comments

Comments
 (0)