Skip to content

Commit 97de8c5

Browse files
authored
Merge pull request #3 from VictoriaMetrics/subqueries
Implement subqueries support
2 parents 533c041 + 0749bb5 commit 97de8c5

File tree

5 files changed

+180
-21
lines changed

5 files changed

+180
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Supported highlights:
112112
- and date helpers (`CURRENT_DATE`, `CURRENT_TIMESTAMP`).
113113
- `WHERE` with comparison operators, `BETWEEN`, `IN`, `LIKE`, `IS (NOT) NULL`
114114
- `ORDER BY`, `LIMIT`, `OFFSET`, `DISTINCT`
115-
- Common Table Expressions (CTE) using `WITH` keyword
115+
- Common Table Expressions (CTE) using `WITH` keyword and subqueries
116116
- `GROUP BY`, `HAVING`, `COUNT/SUM/AVG/MIN/MAX`
117117
- Window functions (`OVER (PARTITION BY ... ORDER BY ...)`) for `SUM` and `COUNT`
118118
- `JOIN` (inner/left) on equality predicates, including subqueries.

cmd/sql-to-logsql/web/ui/src/components/docs/Docs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function Docs() {
5454
<p>
5555
<ul className={"list-disc pl-4 pt-2"}>
5656
<li><code>SELECT, DISTINCT, AS, OVER, PARTITION BY</code></li>
57-
<li><code>FROM, WITH</code></li>
57+
<li><code>FROM, WITH, subqueries</code></li>
5858
<li><code>WHERE, AND, OR</code></li>
5959
<li><code>LEFT JOIN / JOIN / INNER JOIN</code></li>
6060
<li><code>LIKE, NOT LIKE, BETWEEN, IN, NOT IN, IS NULL, IS NOT NULL</code></li>

cmd/sql-to-logsql/web/ui/src/components/sql-editor/examples.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ ORDER BY messages_count DESC`
8686
SELECT UPPER(container), total
8787
FROM container_stats
8888
WHERE container IS NOT NULL
89+
ORDER BY total DESC`,
90+
},
91+
{
92+
id: "subqueries",
93+
title: "Subqueries",
94+
sql: `SELECT UPPER(container), total
95+
FROM (
96+
SELECT kubernetes.container_name AS container, COUNT(*) AS total
97+
FROM logs
98+
GROUP BY kubernetes.container_name
99+
LIMIT 20
100+
) container_stats
101+
WHERE container IS NOT NULL
89102
ORDER BY total DESC`,
90103
},
91104
{

lib/logsql/select.go

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type selectTranslatorVisitor struct {
5151
sp *store.Provider
5252

5353
bindings map[string]*tableBinding
54+
autoAliasCounter int
5455
baseAlias string
5556
pendingLeftFilter []ast.Expr
5657
aggResults map[string]string
@@ -172,6 +173,7 @@ func (v *selectTranslatorVisitor) translateSimpleSelect(stmt *ast.SelectStatemen
172173
}
173174

174175
v.bindings = make(map[string]*tableBinding)
176+
v.autoAliasCounter = 0
175177
v.pendingLeftFilter = nil
176178
v.aggResults = nil
177179
v.baseAlias = ""
@@ -399,6 +401,11 @@ func (v *selectTranslatorVisitor) processFrom(from ast.TableExpr) ([]string, err
399401
return nil, err
400402
}
401403
return nil, nil
404+
case *ast.SubqueryTable:
405+
if err := v.registerBaseSubquery(t); err != nil {
406+
return nil, err
407+
}
408+
return nil, nil
402409
case *ast.JoinExpr:
403410
return v.processJoin(t)
404411
default:
@@ -487,6 +494,43 @@ func (v *selectTranslatorVisitor) registerBaseTable(table *ast.TableName) error
487494
return nil
488495
}
489496

497+
func (v *selectTranslatorVisitor) registerBaseSubquery(table *ast.SubqueryTable) error {
498+
if table == nil || table.Select == nil {
499+
return &TranslationError{
500+
Code: http.StatusBadRequest,
501+
Message: "translator: invalid subquery reference",
502+
}
503+
}
504+
alias := strings.TrimSpace(table.Alias)
505+
if alias == "" {
506+
alias = v.generateSubqueryAlias("base")
507+
}
508+
aliasLower := strings.ToLower(alias)
509+
if v.baseAlias != "" && v.baseAlias != aliasLower {
510+
return &TranslationError{
511+
Code: http.StatusBadRequest,
512+
Message: "translator: multiple base tables are not supported",
513+
}
514+
}
515+
subQuery, err := translateSelectStatementToLogsQLWithContext(table.Select, translationContext{
516+
sp: v.sp,
517+
ctes: v.availableCTEs,
518+
})
519+
if err != nil {
520+
return &TranslationError{
521+
Code: http.StatusBadRequest,
522+
Message: fmt.Sprintf("translator: failed to translate subquery: %s", err),
523+
Err: err,
524+
}
525+
}
526+
v.baseAlias = aliasLower
527+
v.baseUsesPipeline = true
528+
v.basePipeline = subQuery
529+
v.baseFilter = ""
530+
v.registerBinding(aliasLower, true)
531+
return nil
532+
}
533+
490534
func (v *selectTranslatorVisitor) registerBinding(alias string, isBase bool) {
491535
key := strings.ToLower(alias)
492536
if key == "" {
@@ -495,6 +539,21 @@ func (v *selectTranslatorVisitor) registerBinding(alias string, isBase bool) {
495539
v.bindings[key] = &tableBinding{alias: key, isBase: isBase}
496540
}
497541

542+
func (v *selectTranslatorVisitor) generateSubqueryAlias(prefix string) string {
543+
base := strings.TrimSpace(prefix)
544+
if base == "" {
545+
base = "subquery"
546+
}
547+
base = strings.ToLower(base)
548+
for {
549+
v.autoAliasCounter++
550+
candidate := fmt.Sprintf("__%s_%d", base, v.autoAliasCounter)
551+
if _, exists := v.bindings[candidate]; !exists {
552+
return candidate
553+
}
554+
}
555+
}
556+
498557
func (v *selectTranslatorVisitor) processJoin(join *ast.JoinExpr) ([]string, error) {
499558
if join == nil {
500559
return nil, &TranslationError{
@@ -509,16 +568,21 @@ func (v *selectTranslatorVisitor) processJoin(join *ast.JoinExpr) ([]string, err
509568
}
510569
}
511570

512-
leftTable, ok := join.Left.(*ast.TableName)
513-
if !ok {
571+
switch left := join.Left.(type) {
572+
case *ast.TableName:
573+
if err := v.registerBaseTable(left); err != nil {
574+
return nil, err
575+
}
576+
case *ast.SubqueryTable:
577+
if err := v.registerBaseSubquery(left); err != nil {
578+
return nil, err
579+
}
580+
default:
514581
return nil, &TranslationError{
515582
Code: http.StatusBadRequest,
516583
Message: "translator: JOIN left side must be table reference",
517584
}
518585
}
519-
if err := v.registerBaseTable(leftTable); err != nil {
520-
return nil, err
521-
}
522586

523587
var rightAlias string
524588
var rightQuery string
@@ -606,10 +670,7 @@ func (v *selectTranslatorVisitor) processJoin(join *ast.JoinExpr) ([]string, err
606670
case *ast.SubqueryTable:
607671
alias := strings.TrimSpace(rt.Alias)
608672
if alias == "" {
609-
return nil, &TranslationError{
610-
Code: http.StatusBadRequest,
611-
Message: "translator: JOIN subquery requires alias",
612-
}
673+
alias = v.generateSubqueryAlias("join")
613674
}
614675
rightAlias = strings.ToLower(alias)
615676
if _, exists := v.bindings[rightAlias]; exists {
@@ -716,8 +777,8 @@ func (v *selectTranslatorVisitor) extractJoinSpec(cond ast.JoinCondition, rightA
716777

717778
switch {
718779
case leftIsIdent && rightIsIdent:
719-
leftQual := v.qualifierForIdentifier(leftIdent)
720-
rightQual := v.qualifierForIdentifier(rightIdent)
780+
leftQual := v.qualifierForIdentifierWithDefault(leftIdent, v.baseAlias)
781+
rightQual := v.qualifierForIdentifierWithDefault(rightIdent, rightAlias)
721782
if leftQual == v.baseAlias && rightQual == rightAlias {
722783
leftField, err := v.normalizeIdentifier(leftIdent)
723784
if err != nil {
@@ -757,8 +818,8 @@ func (v *selectTranslatorVisitor) extractJoinSpec(cond ast.JoinCondition, rightA
757818
}
758819
}
759820

760-
leftAliases := v.aliasesForExpr(bin.Left)
761-
rightAliases := v.aliasesForExpr(bin.Right)
821+
leftAliases := v.aliasesForExprWithDefault(bin.Left, v.baseAlias)
822+
rightAliases := v.aliasesForExprWithDefault(bin.Right, rightAlias)
762823

763824
if v.isAliasOnly(leftAliases, v.baseAlias) && len(rightAliases) == 0 {
764825
leftFilters = append(leftFilters, expr)
@@ -806,22 +867,23 @@ func flattenAnd(expr ast.Expr) []ast.Expr {
806867
return []ast.Expr{expr}
807868
}
808869

809-
func (v *selectTranslatorVisitor) qualifierForIdentifier(ident *ast.Identifier) string {
870+
func (v *selectTranslatorVisitor) qualifierForIdentifierWithDefault(ident *ast.Identifier, fallback string) string {
810871
if ident == nil || len(ident.Parts) == 0 {
811-
return v.baseAlias
872+
return fallback
812873
}
813874
first := strings.ToLower(ident.Parts[0])
814875
if _, ok := v.bindings[first]; ok {
815876
return first
816877
}
817-
return v.baseAlias
878+
return fallback
818879
}
819880

820-
func (v *selectTranslatorVisitor) aliasesForExpr(expr ast.Expr) map[string]struct{} {
881+
func (v *selectTranslatorVisitor) aliasesForExprWithDefault(expr ast.Expr, fallback string) map[string]struct{} {
821882
aliases := make(map[string]struct{})
822883
walkExpr(expr, func(e ast.Expr) {
823884
if id, ok := e.(*ast.Identifier); ok {
824-
aliases[v.qualifierForIdentifier(id)] = struct{}{}
885+
alias := v.qualifierForIdentifierWithDefault(id, fallback)
886+
aliases[alias] = struct{}{}
825887
}
826888
})
827889
delete(aliases, "")
@@ -883,7 +945,13 @@ func (v *selectTranslatorVisitor) ensureBaseAliasesOnly(expr ast.Expr) error {
883945
}
884946

885947
func (v *selectTranslatorVisitor) ensureAliases(expr ast.Expr, allowed map[string]struct{}) error {
886-
aliases := v.aliasesForExpr(expr)
948+
fallback := v.baseAlias
949+
if len(allowed) == 1 {
950+
for alias := range allowed {
951+
fallback = alias
952+
}
953+
}
954+
aliases := v.aliasesForExprWithDefault(expr, fallback)
887955
for alias := range aliases {
888956
if alias == "" {
889957
continue

lib/logsql/select_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,54 @@ SELECT user FROM recent_errors WHERE service = 'api'`,
324324
sql: "SELECT LOWER(user) AS user_lower, COUNT(*) AS total FROM logs GROUP BY user_lower",
325325
expected: "* | format \"<lc:user>\" as group_1 | stats by (group_1) count() total | rename group_1 as user_lower",
326326
},
327+
{
328+
name: "subquery as base table",
329+
sql: `SELECT *
330+
FROM (
331+
SELECT *
332+
FROM logs
333+
WHERE level = 'error'
334+
) AS recent_errors`,
335+
expected: "level:error",
336+
},
337+
{
338+
name: "subquery as base table without alias",
339+
sql: `SELECT *
340+
FROM (
341+
SELECT *
342+
FROM logs
343+
WHERE level = 'error'
344+
)`,
345+
expected: "level:error",
346+
},
347+
{
348+
name: "subquery as base with filter",
349+
sql: `SELECT recent.user, recent.fail_count
350+
FROM (
351+
SELECT user, COUNT(*) AS fail_count
352+
FROM logs
353+
WHERE level = 'error'
354+
GROUP BY user
355+
) AS recent
356+
WHERE recent.fail_count > 10
357+
ORDER BY recent.fail_count DESC
358+
LIMIT 5`,
359+
expected: "level:error | stats by (user) count() fail_count | filter fail_count:>10 | fields user, fail_count | sort by (fail_count desc) | limit 5",
360+
},
361+
{
362+
name: "subquery as base with filter without alias",
363+
sql: `SELECT user, fail_count
364+
FROM (
365+
SELECT user, COUNT(*) AS fail_count
366+
FROM logs
367+
WHERE level = 'error'
368+
GROUP BY user
369+
)
370+
WHERE fail_count > 10
371+
ORDER BY fail_count DESC
372+
LIMIT 5`,
373+
expected: "level:error | stats by (user) count() fail_count | filter fail_count:>10 | fields user, fail_count | sort by (fail_count desc) | limit 5",
374+
},
327375
{
328376
name: "join with subquery",
329377
sql: `SELECT l.user, m.fail_count
@@ -336,6 +384,21 @@ INNER JOIN (
336384
) AS m ON l.user = m.user
337385
WHERE l.level = 'error'
338386
ORDER BY m.fail_count DESC
387+
LIMIT 5`,
388+
expected: "level:error | join by (user) (level:error | stats by (user) count() fail_count) inner | fields user, fail_count | sort by (fail_count desc) | limit 5",
389+
},
390+
{
391+
name: "join with subquery without alias",
392+
sql: `SELECT l.user, fail_count
393+
FROM logs AS l
394+
INNER JOIN (
395+
SELECT user, COUNT(*) AS fail_count
396+
FROM logs
397+
WHERE level = 'error'
398+
GROUP BY user
399+
) ON l.user = user
400+
WHERE l.level = 'error'
401+
ORDER BY fail_count DESC
339402
LIMIT 5`,
340403
expected: "level:error | join by (user) (level:error | stats by (user) count() fail_count) inner | fields user, fail_count | sort by (fail_count desc) | limit 5",
341404
},
@@ -379,6 +442,21 @@ func TestToLogsQLWithConfig(t *testing.T) {
379442
}
380443
})
381444

445+
t.Run("join with subquery base", func(t *testing.T) {
446+
sql := `SELECT recent.user, a.level
447+
FROM (
448+
SELECT user
449+
FROM logs
450+
WHERE level = 'error'
451+
) AS recent
452+
INNER JOIN api AS a ON recent.user = a.user`
453+
got := mustTranslateWithTables(t, sql, tables)
454+
expected := "level:error | fields user | join by (user) (service:api) inner | fields user, level"
455+
if got != expected {
456+
t.Fatalf("unexpected query:\nexpected: %s\n got: %s", expected, got)
457+
}
458+
})
459+
382460
t.Run("unknown table", func(t *testing.T) {
383461
_, err := translateWithTables(t, "SELECT * FROM missing", tables)
384462
if err == nil {

0 commit comments

Comments
 (0)