Skip to content

Commit 18f2f76

Browse files
authored
build: upgrade DoltgreSQL to v1.2.0
Review-only migration candidate. Fixes the Python autocommit transaction lifetime regression found in task #55. Depends on task #54-approved PR #481, which remains unmerged; do not merge or publish independently.
1 parent 749b616 commit 18f2f76

90 files changed

Lines changed: 5007 additions & 675 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/engine.go

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
// Copyright 2024-2025 ApeCloud, Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package backend
15+
16+
import (
17+
"context"
18+
"strings"
19+
"unicode"
20+
21+
"github.com/apecloud/myduckserver/catalog"
22+
sqle "github.com/dolthub/go-mysql-server"
23+
"github.com/dolthub/go-mysql-server/sql"
24+
"github.com/dolthub/go-mysql-server/sql/analyzer"
25+
"github.com/dolthub/go-mysql-server/sql/types"
26+
"github.com/dolthub/vitess/go/vt/sqlparser"
27+
)
28+
29+
// NewEngine constructs the MySQL engine with MyDuck's parser and executor
30+
// compatibility boundaries installed consistently.
31+
func NewEngine(provider *catalog.DatabaseProvider) (*sqle.Engine, *DuckBuilder) {
32+
registerMySQLCompatibilitySystemVariables()
33+
parser := &mysqlParser{Parser: sql.NewMysqlParser()}
34+
overrides := sql.EngineOverrides{
35+
Builder: sql.BuilderOverrides{Parser: parser},
36+
}
37+
engine := sqle.New(analyzer.NewBuilder(provider).AddOverrides(overrides).Build(), nil)
38+
builder := NewDuckBuilder(engine.Analyzer.ExecBuilder, provider)
39+
engine.Analyzer.ExecBuilder.PriorityBuilder = builder
40+
return engine, builder
41+
}
42+
43+
// registerMySQLCompatibilitySystemVariables keeps MyDuck's advertised SQL
44+
// compatibility level stable across GMS upgrades. Clients such as MySQL Shell
45+
// branch on @@version and otherwise probe newer variables MyDuck does not
46+
// implement.
47+
func registerMySQLCompatibilitySystemVariables() {
48+
const compatibilityVersion = "8.0.23"
49+
sql.SystemVariables.AddSystemVariables([]sql.SystemVariable{
50+
&sql.MysqlSystemVariable{
51+
Name: "version",
52+
Scope: sql.GetMysqlScope(sql.SystemVariableScope_Global),
53+
Dynamic: false,
54+
SetVarHintApplies: false,
55+
Type: types.NewSystemStringType("version"),
56+
Default: compatibilityVersion,
57+
},
58+
})
59+
}
60+
61+
// mysqlParser restores MyDuck syntax and AST contracts that are missing from
62+
// the selected Vitess parser.
63+
type mysqlParser struct {
64+
sql.Parser
65+
}
66+
67+
func (p *mysqlParser) ParseSimple(query string) (sqlparser.Statement, error) {
68+
compat := rewriteMySQLCompatibility(query)
69+
stmt, err := p.Parser.ParseSimple(compat.query)
70+
return normalizeMySQLStatement(stmt, compat.replacements), err
71+
}
72+
73+
func (p *mysqlParser) Parse(ctx *sql.Context, query string, multi bool) (sqlparser.Statement, string, string, error) {
74+
compat := rewriteMySQLCompatibility(query)
75+
stmt, parsed, remainder, err := p.Parser.Parse(ctx, compat.query, multi)
76+
return normalizeMySQLStatement(stmt, compat.replacements), compat.restoreParsedQuery(parsed), remainder, err
77+
}
78+
79+
func (p *mysqlParser) ParseWithOptions(
80+
ctx context.Context,
81+
query string,
82+
delimiter rune,
83+
multi bool,
84+
options sqlparser.ParserOptions,
85+
) (sqlparser.Statement, string, string, error) {
86+
compat := rewriteMySQLCompatibility(query)
87+
stmt, parsed, remainder, err := p.Parser.ParseWithOptions(ctx, compat.query, delimiter, multi, options)
88+
return normalizeMySQLStatement(stmt, compat.replacements), compat.restoreParsedQuery(parsed), remainder, err
89+
}
90+
91+
func (p *mysqlParser) ParseOneWithOptions(
92+
ctx context.Context,
93+
query string,
94+
options sqlparser.ParserOptions,
95+
) (sqlparser.Statement, int, error) {
96+
compat := rewriteMySQLCompatibility(query)
97+
stmt, index, err := p.Parser.ParseOneWithOptions(ctx, compat.query, options)
98+
return normalizeMySQLStatement(stmt, compat.replacements), compat.originalOffset(index), err
99+
}
100+
101+
type mysqlOptionReplacement struct {
102+
optionIndex int
103+
start int
104+
end int
105+
name string
106+
alias string
107+
}
108+
109+
type mysqlParserCompat struct {
110+
original string
111+
query string
112+
replacements []mysqlOptionReplacement
113+
}
114+
115+
func (c mysqlParserCompat) restoreParsedQuery(parsed string) string {
116+
if len(c.replacements) == 0 || parsed == "" {
117+
return parsed
118+
}
119+
120+
trimmed := strings.TrimLeftFunc(c.query, unicode.IsSpace)
121+
offset := len(c.query) - len(trimmed)
122+
if !strings.HasPrefix(trimmed, parsed) {
123+
return parsed
124+
}
125+
126+
end := c.originalOffset(offset + len(parsed))
127+
if end < offset || end > len(c.original) {
128+
return parsed
129+
}
130+
return c.original[offset:end]
131+
}
132+
133+
func (c mysqlParserCompat) originalOffset(rewrittenOffset int) int {
134+
if len(c.replacements) == 0 || rewrittenOffset <= 0 {
135+
return rewrittenOffset
136+
}
137+
138+
originalPos := 0
139+
rewrittenPos := 0
140+
for _, replacement := range c.replacements {
141+
unchanged := replacement.start - originalPos
142+
if rewrittenOffset <= rewrittenPos+unchanged {
143+
return originalPos + rewrittenOffset - rewrittenPos
144+
}
145+
rewrittenPos += unchanged
146+
originalPos = replacement.start
147+
148+
aliasEnd := rewrittenPos + len(replacement.alias)
149+
if rewrittenOffset <= aliasEnd {
150+
inside := rewrittenOffset - rewrittenPos
151+
if originalLength := replacement.end - replacement.start; inside > originalLength {
152+
inside = originalLength
153+
}
154+
return replacement.start + inside
155+
}
156+
rewrittenPos = aliasEnd
157+
originalPos = replacement.end
158+
}
159+
return originalPos + rewrittenOffset - rewrittenPos
160+
}
161+
162+
func (c *mysqlParserCompat) applyReplacements() {
163+
if len(c.replacements) == 0 {
164+
return
165+
}
166+
167+
var rewritten strings.Builder
168+
last := 0
169+
for _, replacement := range c.replacements {
170+
rewritten.WriteString(c.original[last:replacement.start])
171+
rewritten.WriteString(replacement.alias)
172+
last = replacement.end
173+
}
174+
rewritten.WriteString(c.original[last:])
175+
c.query = rewritten.String()
176+
}
177+
178+
func rewriteMySQLCompatibility(query string) mysqlParserCompat {
179+
compat := rewriteReplicationSourceOptions(query)
180+
if len(compat.replacements) > 0 {
181+
return compat
182+
}
183+
return rewriteReplicationFilterOptions(query)
184+
}
185+
186+
// rewriteReplicationSourceOptions maps the two file-position options removed
187+
// from the selected Vitess grammar onto same-type options it still accepts.
188+
// The aliases have the same byte width as the original tokens so parser error
189+
// positions and multi-statement offsets remain stable. The AST names are
190+
// restored by normalizeMySQLStatement.
191+
func rewriteReplicationSourceOptions(query string) mysqlParserCompat {
192+
compat := mysqlParserCompat{original: query, query: query}
193+
tokenizer := sqlparser.NewStringTokenizer(query)
194+
prefix := []int{sqlparser.CHANGE, sqlparser.REPLICATION, sqlparser.SOURCE, sqlparser.TO}
195+
prefixIndex := 0
196+
197+
for prefixIndex < len(prefix) {
198+
token, _ := tokenizer.Scan()
199+
if token == sqlparser.COMMENT {
200+
continue
201+
}
202+
if token != prefix[prefixIndex] {
203+
return compat
204+
}
205+
prefixIndex++
206+
}
207+
208+
type parseState uint8
209+
const (
210+
expectOption parseState = iota
211+
expectEquals
212+
expectValue
213+
expectSeparator
214+
)
215+
216+
state := expectOption
217+
optionIndex := 0
218+
var pending *mysqlOptionReplacement
219+
for {
220+
token, value := tokenizer.Scan()
221+
if token == sqlparser.COMMENT {
222+
continue
223+
}
224+
225+
switch state {
226+
case expectOption:
227+
if token == 0 || token == ';' {
228+
return mysqlParserCompat{original: query, query: query}
229+
}
230+
name := string(value)
231+
var alias string
232+
switch {
233+
case strings.EqualFold(name, "SOURCE_LOG_FILE"):
234+
alias = "SOURCE_PASSWORD"
235+
case strings.EqualFold(name, "SOURCE_LOG_POS"):
236+
alias = "SOURCE_PORT"
237+
}
238+
if alias != "" {
239+
end := tokenizer.Position - 1
240+
alias += strings.Repeat(" ", len(value)-len(alias))
241+
pending = &mysqlOptionReplacement{
242+
optionIndex: optionIndex,
243+
start: end - len(value),
244+
end: end,
245+
name: name,
246+
alias: alias,
247+
}
248+
}
249+
state = expectEquals
250+
251+
case expectEquals:
252+
if token != '=' {
253+
return mysqlParserCompat{original: query, query: query}
254+
}
255+
state = expectValue
256+
257+
case expectValue:
258+
if pending != nil {
259+
if strings.EqualFold(pending.name, "SOURCE_LOG_FILE") && token != sqlparser.STRING {
260+
return mysqlParserCompat{original: query, query: query}
261+
}
262+
if strings.EqualFold(pending.name, "SOURCE_LOG_POS") && token != sqlparser.INTEGRAL {
263+
return mysqlParserCompat{original: query, query: query}
264+
}
265+
compat.replacements = append(compat.replacements, *pending)
266+
pending = nil
267+
}
268+
state = expectSeparator
269+
270+
case expectSeparator:
271+
switch token {
272+
case ',':
273+
optionIndex++
274+
state = expectOption
275+
case 0, ';':
276+
compat.applyReplacements()
277+
return compat
278+
default:
279+
return mysqlParserCompat{original: query, query: query}
280+
}
281+
}
282+
}
283+
}
284+
285+
// rewriteReplicationFilterOptions maps the DB-level filters removed from the
286+
// selected Vitess grammar onto its table-filter productions. Both productions
287+
// parse the same TableNames value; normalizeMySQLStatement restores the option
288+
// name before GMS applies the filter.
289+
func rewriteReplicationFilterOptions(query string) mysqlParserCompat {
290+
compat := mysqlParserCompat{original: query, query: query}
291+
tokenizer := sqlparser.NewStringTokenizer(query)
292+
prefix := []int{sqlparser.CHANGE, sqlparser.REPLICATION, sqlparser.FILTER}
293+
for _, expected := range prefix {
294+
token, _ := tokenizer.Scan()
295+
for token == sqlparser.COMMENT {
296+
token, _ = tokenizer.Scan()
297+
}
298+
if token != expected {
299+
return compat
300+
}
301+
}
302+
303+
optionIndex := 0
304+
expectOption := true
305+
depth := 0
306+
for {
307+
token, value := tokenizer.Scan()
308+
if token == sqlparser.COMMENT {
309+
continue
310+
}
311+
312+
if expectOption {
313+
if token == 0 || token == ';' {
314+
return mysqlParserCompat{original: query, query: query}
315+
}
316+
name := string(value)
317+
var alias string
318+
switch {
319+
case strings.EqualFold(name, "REPLICATE_DO_DB"):
320+
alias = "REPLICATE_DO_TABLE"
321+
case strings.EqualFold(name, "REPLICATE_IGNORE_DB"):
322+
alias = "REPLICATE_IGNORE_TABLE"
323+
}
324+
if alias != "" {
325+
end := tokenizer.Position - 1
326+
compat.replacements = append(compat.replacements, mysqlOptionReplacement{
327+
optionIndex: optionIndex,
328+
start: end - len(value),
329+
end: end,
330+
name: name,
331+
alias: alias,
332+
})
333+
}
334+
expectOption = false
335+
continue
336+
}
337+
338+
switch token {
339+
case '(':
340+
depth++
341+
case ')':
342+
if depth > 0 {
343+
depth--
344+
}
345+
case ',':
346+
if depth == 0 {
347+
optionIndex++
348+
expectOption = true
349+
}
350+
case 0, ';':
351+
compat.applyReplacements()
352+
return compat
353+
}
354+
}
355+
}
356+
357+
func normalizeMySQLStatement(
358+
stmt sqlparser.Statement,
359+
replacements []mysqlOptionReplacement,
360+
) sqlparser.Statement {
361+
ddl, ok := stmt.(*sqlparser.DDL)
362+
if ok && ddl.ViewSpec != nil && ddl.Table.IsEmpty() {
363+
ddl.Table = ddl.ViewSpec.ViewName
364+
}
365+
366+
changeSource, ok := stmt.(*sqlparser.ChangeReplicationSource)
367+
if ok {
368+
for _, replacement := range replacements {
369+
if replacement.optionIndex < len(changeSource.Options) {
370+
changeSource.Options[replacement.optionIndex].Name = replacement.name
371+
}
372+
}
373+
}
374+
375+
changeFilter, ok := stmt.(*sqlparser.ChangeReplicationFilter)
376+
if ok {
377+
for _, replacement := range replacements {
378+
if replacement.optionIndex < len(changeFilter.Options) {
379+
changeFilter.Options[replacement.optionIndex].Name = replacement.name
380+
}
381+
}
382+
}
383+
return stmt
384+
}

0 commit comments

Comments
 (0)