Skip to content

Commit 16f5219

Browse files
dialect/sql: added support for some yql select clauses
1 parent 8b07d4a commit 16f5219

File tree

2 files changed

+285
-35
lines changed

2 files changed

+285
-35
lines changed

dialect/sql/builder.go

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,15 +1702,15 @@ func (s *SelectTable) As(alias string) *SelectTable {
17021702
}
17031703

17041704
// View sets the secondary index name for the VIEW clause (YDB-specific).
1705-
// This allows explicit use of secondary indexes in JOIN operations.
1705+
// This allows explicit use of secondary indexes in SELECT and JOIN operations.
17061706
//
17071707
// t := Table("users").View("idx_email").As("u")
17081708
// Select().From(Table("orders")).Join(t).On(...)
17091709
func (s *SelectTable) View(index string) *SelectTable {
17101710
if s.ydb() {
17111711
s.index = index
17121712
} else {
1713-
s.AddError(fmt.Errorf("JOIN VIEW: unsupported dialect: %q", s.dialect))
1713+
s.AddError(fmt.Errorf("VIEW: unsupported dialect: %q", s.dialect))
17141714
}
17151715
return s
17161716
}
@@ -1792,24 +1792,25 @@ type Selector struct {
17921792
Builder
17931793
// ctx stores contextual data typically from
17941794
// generated code such as alternate table schemas.
1795-
ctx context.Context
1796-
as string
1797-
selection []selection
1798-
from []TableView
1799-
joins []join
1800-
collected [][]*Predicate
1801-
where *Predicate
1802-
or bool
1803-
not bool
1804-
order []any
1805-
group []string
1806-
having *Predicate
1807-
limit *int
1808-
offset *int
1809-
distinct bool
1810-
setOps []setOp
1811-
prefix Queries
1812-
lock *LockOptions
1795+
ctx context.Context
1796+
as string
1797+
selection []selection
1798+
from []TableView
1799+
joins []join
1800+
collected [][]*Predicate
1801+
where *Predicate
1802+
or bool
1803+
not bool
1804+
order []any
1805+
assumeOrder []string // YDB-specific: ASSUME ORDER BY columns
1806+
group []string
1807+
having *Predicate
1808+
limit *int
1809+
offset *int
1810+
distinct bool
1811+
setOps []setOp
1812+
prefix Queries
1813+
lock *LockOptions
18131814
}
18141815

18151816
// New returns a new Selector with the same dialect and context.
@@ -2543,21 +2544,22 @@ func (s *Selector) Clone() *Selector {
25432544
joins[i] = s.joins[i].clone()
25442545
}
25452546
return &Selector{
2546-
Builder: s.Builder.clone(),
2547-
ctx: s.ctx,
2548-
as: s.as,
2549-
or: s.or,
2550-
not: s.not,
2551-
from: s.from,
2552-
limit: s.limit,
2553-
offset: s.offset,
2554-
distinct: s.distinct,
2555-
where: s.where.clone(),
2556-
having: s.having.clone(),
2557-
joins: append([]join{}, joins...),
2558-
group: append([]string{}, s.group...),
2559-
order: append([]any{}, s.order...),
2560-
selection: append([]selection{}, s.selection...),
2547+
Builder: s.Builder.clone(),
2548+
ctx: s.ctx,
2549+
as: s.as,
2550+
or: s.or,
2551+
not: s.not,
2552+
from: s.from,
2553+
limit: s.limit,
2554+
offset: s.offset,
2555+
distinct: s.distinct,
2556+
where: s.where.clone(),
2557+
having: s.having.clone(),
2558+
joins: append([]join{}, joins...),
2559+
group: append([]string{}, s.group...),
2560+
order: append([]any{}, s.order...),
2561+
assumeOrder: append([]string{}, s.assumeOrder...),
2562+
selection: append([]selection{}, s.selection...),
25612563
}
25622564
}
25632565

@@ -2585,6 +2587,11 @@ func DescExpr(x Querier) Querier {
25852587

25862588
// OrderBy appends the `ORDER BY` clause to the `SELECT` statement.
25872589
func (s *Selector) OrderBy(columns ...string) *Selector {
2590+
if s.ydb() && len(s.assumeOrder) != 0 {
2591+
s.AddError(fmt.Errorf("ORDER BY: can't be used with ASSUME ORDER BY simultaneously"))
2592+
return s
2593+
}
2594+
25882595
for i := range columns {
25892596
s.order = append(s.order, columns[i])
25902597
}
@@ -2626,6 +2633,27 @@ func (s *Selector) ClearOrder() *Selector {
26262633
return s
26272634
}
26282635

2636+
// AssumeOrderBy appends the `ASSUME ORDER BY` clause to the `SELECT` statement (YDB-specific).
2637+
// This tells YDB to assume the data is already sorted without actually sorting it.
2638+
// This is an optimization hint and only works with column names (not expressions).
2639+
//
2640+
// Select("*").
2641+
// From(Table("users")).
2642+
// AssumeOrderBy("first-key", Desc("second-key"))
2643+
func (s *Selector) AssumeOrderBy(columns ...string) *Selector {
2644+
if s.ydb() {
2645+
if len(s.order) != 0 {
2646+
s.AddError(fmt.Errorf("ASSUME ORDER BY: can't be used with ORDER BY simultaneously"))
2647+
return s
2648+
}
2649+
2650+
s.assumeOrder = append(s.assumeOrder, columns...)
2651+
} else {
2652+
s.AddError(fmt.Errorf("ASSUME ORDER BY: unsupported dialect: %q", s.dialect))
2653+
}
2654+
return s
2655+
}
2656+
26292657
// GroupBy appends the `GROUP BY` clause to the `SELECT` statement.
26302658
func (s *Selector) GroupBy(columns ...string) *Selector {
26312659
s.group = append(s.group, columns...)
@@ -2716,6 +2744,7 @@ func (s *Selector) Query() (string, []any) {
27162744
s.joinSetOps(&b)
27172745
}
27182746
joinOrder(s.order, &b)
2747+
s.joinAssumeOrder(&b)
27192748
if s.limit != nil {
27202749
b.WriteString(" LIMIT ")
27212750
b.WriteString(strconv.Itoa(*s.limit))
@@ -2794,6 +2823,19 @@ func joinOrder(order []any, b *Builder) {
27942823
}
27952824
}
27962825

2826+
func (s *Selector) joinAssumeOrder(b *Builder) {
2827+
if !b.ydb() || len(s.assumeOrder) == 0 {
2828+
return
2829+
}
2830+
b.WriteString(" ASSUME ORDER BY ")
2831+
for i := range s.assumeOrder {
2832+
if i > 0 {
2833+
b.Comma()
2834+
}
2835+
b.Ident(s.assumeOrder[i])
2836+
}
2837+
}
2838+
27972839
func joinReturning(columns []string, b *Builder) {
27982840
supportedByDialect := b.postgres() || b.sqlite() || b.ydb()
27992841
if len(columns) == 0 || !supportedByDialect {

dialect/sql/builder_test.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,53 @@ AND "users"."id1" < "users"."id2") AND "users"."id1" <= "users"."id2"`, "\n", ""
18461846
}(),
18471847
wantQuery: "SELECT `a`.`value`, `b`.`value`, `c`.`column2` FROM `a_table` AS `a` CROSS JOIN `b_table` AS `b` LEFT JOIN `c_table` AS `c` ON `c`.`ref` = `a`.`key` AND `c`.`column1` = `b`.`value`",
18481848
},
1849+
{
1850+
input: Dialect(dialect.YDB).
1851+
Select("value").
1852+
From(Table("my_table")).
1853+
Distinct(),
1854+
wantQuery: "SELECT DISTINCT `value` FROM `my_table`",
1855+
},
1856+
{
1857+
input: Dialect(dialect.YDB).
1858+
Select("*").
1859+
From(Table("users")).
1860+
Distinct(),
1861+
wantQuery: "SELECT DISTINCT * FROM `users`",
1862+
},
1863+
{
1864+
input: Dialect(dialect.YDB).
1865+
Select("status", "type").
1866+
From(Table("orders")).
1867+
Distinct().
1868+
Where(GT("created_at", "2024-01-01")),
1869+
wantQuery: "SELECT DISTINCT `status`, `type` FROM `orders` WHERE `created_at` > $p0",
1870+
wantArgs: []any{driver.NamedValue{Name: "p0", Value: "2024-01-01"}},
1871+
},
1872+
{
1873+
input: Dialect(dialect.YDB).
1874+
Select("category").
1875+
From(Table("products")).
1876+
Distinct().
1877+
OrderBy("category").
1878+
Limit(10),
1879+
wantQuery: "SELECT DISTINCT `category` FROM `products` ORDER BY `category` LIMIT 10",
1880+
},
1881+
{
1882+
input: Dialect(dialect.YDB).
1883+
Select("name", "age").
1884+
From(Table("users")).
1885+
Distinct(),
1886+
wantQuery: "SELECT DISTINCT `name`, `age` FROM `users`",
1887+
},
1888+
{
1889+
input: Dialect(dialect.YDB).
1890+
Select("email").
1891+
From(Table("subscribers")).
1892+
Where(EQ("active", true)).
1893+
Distinct(),
1894+
wantQuery: "SELECT DISTINCT `email` FROM `subscribers` WHERE `active`",
1895+
},
18491896
}
18501897
for i, tt := range tests {
18511898
t.Run(strconv.Itoa(i), func(t *testing.T) {
@@ -1969,6 +2016,62 @@ func TestSelector_Union(t *testing.T) {
19692016
Query()
19702017
require.Equal(t, `SELECT * FROM "users" WHERE "active" UNION SELECT * FROM "old_users1" WHERE "is_active" AND "age" > $1 UNION ALL SELECT * FROM "old_users2" WHERE "is_active" = $2 AND "age" < $3`, query)
19712018
require.Equal(t, []any{20, "true", 18}, args)
2019+
2020+
query, args = Dialect(dialect.YDB).
2021+
Select("key").
2022+
From(Table("T1")).
2023+
Union(
2024+
Dialect(dialect.YDB).
2025+
Select("key").
2026+
From(Table("T2")),
2027+
).
2028+
Query()
2029+
require.Equal(t, "SELECT `key` FROM `T1` UNION SELECT `key` FROM `T2`", query)
2030+
require.Empty(t, args)
2031+
2032+
query, args = Dialect(dialect.YDB).
2033+
Select("id", "name").
2034+
From(Table("users")).
2035+
Where(EQ("status", "active")).
2036+
Union(
2037+
Dialect(dialect.YDB).
2038+
Select("id", "name").
2039+
From(Table("users")).
2040+
Where(EQ("status", "inactive")),
2041+
).
2042+
Query()
2043+
require.Equal(t, "SELECT `id`, `name` FROM `users` WHERE `status` = $p0 UNION SELECT `id`, `name` FROM `users` WHERE `status` = $p1", query)
2044+
require.Equal(t, []any{
2045+
driver.NamedValue{Name: "p0", Value: "active"},
2046+
driver.NamedValue{Name: "p1", Value: "inactive"},
2047+
}, args)
2048+
2049+
query, args = Dialect(dialect.YDB).
2050+
Select("x").
2051+
UnionAll(Dialect(dialect.YDB).Select("y")).
2052+
UnionAll(Dialect(dialect.YDB).Select("z")).
2053+
Query()
2054+
require.Equal(t, "SELECT `x` UNION ALL SELECT `y` UNION ALL SELECT `z`", query)
2055+
require.Empty(t, args)
2056+
2057+
query, args = Dialect(dialect.YDB).
2058+
Select("id").
2059+
From(Table("orders")).
2060+
Where(EQ("status", "pending")).
2061+
Union(
2062+
Dialect(dialect.YDB).
2063+
Select("id").
2064+
From(Table("orders")).
2065+
Where(EQ("status", "processing")),
2066+
).
2067+
OrderBy("id").
2068+
Limit(100).
2069+
Query()
2070+
require.Equal(t, "SELECT `id` FROM `orders` WHERE `status` = $p0 UNION SELECT `id` FROM `orders` WHERE `status` = $p1 ORDER BY `id` LIMIT 100", query)
2071+
require.Equal(t, []any{
2072+
driver.NamedValue{Name: "p0", Value: "pending"},
2073+
driver.NamedValue{Name: "p1", Value: "processing"},
2074+
}, args)
19722075
}
19732076

19742077
func TestSelector_Except(t *testing.T) {
@@ -2031,6 +2134,111 @@ func TestSelector_Intersect(t *testing.T) {
20312134
require.Equal(t, []any{20, "true", 18}, args)
20322135
}
20332136

2137+
func TestSelector_AssumeOrderBy_YDB(t *testing.T) {
2138+
query, args := Dialect(dialect.YDB).
2139+
Select("*").
2140+
From(Table("my_table")).
2141+
AssumeOrderBy("key").
2142+
Query()
2143+
require.Equal(t, "SELECT * FROM `my_table` ASSUME ORDER BY `key`", query)
2144+
require.Empty(t, args)
2145+
2146+
query, args = Dialect(dialect.YDB).
2147+
Select("key", "subkey").
2148+
From(Table("my_table")).
2149+
AssumeOrderBy("key", Desc("subkey")).
2150+
Query()
2151+
require.Equal(t, "SELECT `key`, `subkey` FROM `my_table` ASSUME ORDER BY `key`, `subkey` DESC", query)
2152+
require.Empty(t, args)
2153+
2154+
query, args = Dialect(dialect.YDB).
2155+
Select("*").
2156+
From(Table("orders")).
2157+
Where(EQ("status", "active")).
2158+
AssumeOrderBy("created_at").
2159+
Query()
2160+
require.Equal(t, "SELECT * FROM `orders` WHERE `status` = $p0 ASSUME ORDER BY `created_at`", query)
2161+
require.Equal(t, []any{driver.NamedValue{Name: "p0", Value: "active"}}, args)
2162+
2163+
query, args = Dialect(dialect.YDB).
2164+
Select("id", "name").
2165+
From(Table("users")).
2166+
AssumeOrderBy("id").
2167+
Limit(10).
2168+
Query()
2169+
require.Equal(t, "SELECT `id`, `name` FROM `users` ASSUME ORDER BY `id` LIMIT 10", query)
2170+
require.Empty(t, args)
2171+
2172+
t.Run("OrderBy then AssumeOrderBy should error", func(t *testing.T) {
2173+
selector := Dialect(dialect.YDB).
2174+
Select("*").
2175+
From(Table("users")).
2176+
OrderBy("id").
2177+
AssumeOrderBy("id")
2178+
2179+
err := selector.Err()
2180+
require.Error(t, err)
2181+
})
2182+
2183+
t.Run("AssumeOrderBy then OrderBy should error", func(t *testing.T) {
2184+
selector := Dialect(dialect.YDB).
2185+
Select("*").
2186+
From(Table("users")).
2187+
AssumeOrderBy("id").
2188+
OrderBy("id")
2189+
2190+
err := selector.Err()
2191+
require.Error(t, err)
2192+
})
2193+
2194+
t.Run("Non-YDB dialect with AssumeOrderBy should error", func(t *testing.T) {
2195+
selector := Dialect(dialect.Postgres).
2196+
Select("*").
2197+
From(Table("users")).
2198+
AssumeOrderBy("name")
2199+
2200+
err := selector.Err()
2201+
require.Error(t, err)
2202+
})
2203+
}
2204+
2205+
func TestSelector_VIEW_SecondaryIndex_YDB(t *testing.T) {
2206+
t.Run("Simple SELECT with VIEW in FROM clause", func(t *testing.T) {
2207+
d := Dialect(dialect.YDB)
2208+
query, args := d.Select("series_id", "title", "info", "release_date", "views", "uploaded_user_id").
2209+
From(d.Table("series").View("views_index")).
2210+
Where(GTE("views", 1000)).
2211+
Query()
2212+
2213+
require.Equal(t, "SELECT `series_id`, `title`, `info`, `release_date`, `views`, `uploaded_user_id` FROM `series` VIEW `views_index` WHERE `views` >= $p0", query)
2214+
require.Equal(t, []any{driver.NamedValue{Name: "p0", Value: 1000}}, args)
2215+
})
2216+
2217+
t.Run("JOIN with VIEW on both tables", func(t *testing.T) {
2218+
d := Dialect(dialect.YDB)
2219+
series := d.Table("series").View("users_index").As("t1")
2220+
users := d.Table("users").View("name_index").As("t2")
2221+
2222+
query, args := d.Select(series.C("series_id"), series.C("title")).
2223+
From(series).
2224+
Join(users).
2225+
On(series.C("uploaded_user_id"), users.C("user_id")).
2226+
Where(EQ(users.C("name"), "John Doe")).
2227+
Query()
2228+
2229+
require.Equal(t, "SELECT `t1`.`series_id`, `t1`.`title` FROM `series` VIEW `users_index` AS `t1` JOIN `users` VIEW `name_index` AS `t2` ON `t1`.`uploaded_user_id` = `t2`.`user_id` WHERE `t2`.`name` = $p0", query)
2230+
require.Equal(t, []any{driver.NamedValue{Name: "p0", Value: "John Doe"}}, args)
2231+
})
2232+
2233+
t.Run("VIEW on non-YDB dialect should error", func(t *testing.T) {
2234+
d := Dialect(dialect.Postgres)
2235+
table := d.Table("users").View("idx_name")
2236+
2237+
err := table.Err()
2238+
require.Error(t, err)
2239+
})
2240+
}
2241+
20342242
func TestSelector_SetOperatorWithRecursive(t *testing.T) {
20352243
t1, t2, t3 := Table("files"), Table("files"), Table("path")
20362244
n := Queries{

0 commit comments

Comments
 (0)