Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions pkg/sql/parsers/dialect/mysql/mysql_sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3591,27 +3591,27 @@ var (
},
{
input: "select * from t1 where MATCH (body, title) AGAINST ('abc dfc ghc')",
output: "select * from t1 where MATCH (body, title) AGAINST (abc dfc ghc)",
output: "select * from t1 where MATCH (body, title) AGAINST ('abc dfc ghc')",
},
{
input: "select * from t1 where MATCH (body, title) AGAINST ('abc- +abc' IN BOOLEAN MODE)",
output: "select * from t1 where MATCH (body, title) AGAINST (abc- +abc IN BOOLEAN MODE)",
output: "select * from t1 where MATCH (body, title) AGAINST ('abc- +abc' IN BOOLEAN MODE)",
},
{
input: "select * from t1 where MATCH (body, title) AGAINST ('abc%' IN NATURAL LANGUAGE MODE)",
output: "select * from t1 where MATCH (body, title) AGAINST (abc% IN NATURAL LANGUAGE MODE)",
output: "select * from t1 where MATCH (body, title) AGAINST ('abc%' IN NATURAL LANGUAGE MODE)",
},
{
input: "select * from t1 where MATCH (body, title) AGAINST ('abc gg*' WITH QUERY EXPANSION)",
output: "select * from t1 where MATCH (body, title) AGAINST (abc gg* WITH QUERY EXPANSION)",
output: "select * from t1 where MATCH (body, title) AGAINST ('abc gg*' WITH QUERY EXPANSION)",
},
{
input: "select * from t1 where MATCH (body, title) AGAINST ('abc' IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION)",
output: "select * from t1 where MATCH (body, title) AGAINST (abc IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION)",
output: "select * from t1 where MATCH (body, title) AGAINST ('abc' IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION)",
},
{
input: "select MATCH (body, title) AGAINST ('abc' IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION) from t1",
output: "select MATCH (body, title) AGAINST (abc IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION) from t1",
output: "select MATCH (body, title) AGAINST ('abc' IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION) from t1",
},
{
input: "alter user user1 unlock",
Expand Down Expand Up @@ -3771,6 +3771,35 @@ func TestValid(t *testing.T) {
}
}

// TestFullTextMatchDeparseRoundTrip is the #24823 regression on the DEFAULT tree.String path
// (not only the WithSingleQuoteString path): MATCH(...) AGAINST('...') must deparse to a
// quoted, re-parseable string literal so re-serialized statements (CREATE TABLE AS SELECT,
// view expansion, etc.) round-trip. Before the fix the default path emitted the pattern bare
// (AGAINST (防水 ...)) which is invalid SQL.
func TestFullTextMatchDeparseRoundTrip(t *testing.T) {
ctx := context.TODO()
cases := []string{
"select * from t where MATCH (body) AGAINST ('防水' IN BOOLEAN MODE)",
"select * from t where MATCH (a, b) AGAINST ('abc dfc')",
"select MATCH (body) AGAINST ('abc%' IN NATURAL LANGUAGE MODE) from t",
// embedded single quote must survive escaping on the default path
"select * from t where MATCH (body) AGAINST ('it''s a test')",
}
for _, sql := range cases {
ast, err := ParseOne(ctx, sql, 1)
require.NoError(t, err, sql)

// DEFAULT path (no WithSingleQuoteString / WithQuoteString).
out := tree.String(ast, dialect.MYSQL)
require.Contains(t, out, "AGAINST ('", "default deparse must quote the pattern, got: "+out)

// The deparsed SQL must re-parse and be idempotent under further deparse.
ast2, err := ParseOne(ctx, out, 1)
require.NoError(t, err, "deparsed SQL must re-parse: "+out)
require.Equal(t, out, tree.String(ast2, dialect.MYSQL), "deparse must be idempotent")
}
}

func TestShowVariablesGlobalFlag(t *testing.T) {
ctx := context.TODO()
stmt, err := ParseOne(ctx, "show global variables like 'interactive_timeout'", 1)
Expand All @@ -3790,6 +3819,17 @@ var (
input: "select count(*) from t",
output: "select count(*) from t",
},
{
// #24823: MATCH ... AGAINST pattern must keep its quotes when the query is
// re-serialized (e.g. CREATE TABLE AS SELECT), else the re-parse syntax-errors.
input: "select id, MATCH (body) AGAINST ('防水' IN BOOLEAN MODE) as sc from ft where MATCH (body) AGAINST ('防水' IN BOOLEAN MODE)",
output: "select id, MATCH (body) AGAINST ('防水' IN BOOLEAN MODE) as sc from ft where MATCH (body) AGAINST ('防水' IN BOOLEAN MODE)",
},
{
// embedded single quote in the pattern must be escaped ('' ) on restore.
input: "select * from t where MATCH (body) AGAINST ('a''b')",
output: "select * from t where MATCH (body) AGAINST ('a''b')",
},
{
input: "select count(*) as cnt, sum(a) from t group by b having count(*) > 1",
output: "select count(*) as cnt, sum(a) from t group by b having count(*) > 1",
Expand Down
13 changes: 12 additions & 1 deletion pkg/sql/parsers/tree/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,18 @@ func (node *FullTextMatchExpr) Format(ctx *FmtCtx) {
}
ctx.WriteString(") ")
ctx.WriteString("AGAINST (")
ctx.WriteString(node.Pattern)
// Pattern is ALWAYS a string literal and is stored unquoted (search_pattern: STRING strips
// the surrounding quotes). Emit it as a single-quoted, escaped SQL string literal
// UNCONDITIONALLY — do NOT route it through ctx.WriteValue, which only quotes when the
// FmtCtx opts in (quoteString/singleQuoteString). The default tree.String() path does not
// opt in, so WriteValue emitted the pattern bare and any deparsed statement (CREATE TABLE
// AS SELECT, view expansion, or any other re-serialization) produced invalid SQL that
// failed to re-parse (#24823). Unconditional quoting is correct here precisely because the
// pattern is never a number/null/bool (unlike the polymorphic NumVal) — bare output is
// never valid SQL for this node.
ctx.WriteString("'")
ctx.WriteString(strings.ReplaceAll(FormatString(node.Pattern), "'", "''"))
ctx.WriteString("'")

if node.Mode != FULLTEXT_DEFAULT {
ctx.WriteString(" ")
Expand Down
28 changes: 28 additions & 0 deletions test/distributed/cases/fulltext/fulltext_ctas.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
drop database if exists ft_ctas;
create database ft_ctas;
use ft_ctas;
create table ft (id bigint primary key, body text);
insert into ft values
(1, '防水 材料 测试'),
(2, '普通 塑料 制品'),
(3, '防水 涂料 施工');
create fulltext index ftidx on ft (body) with parser gojieba;
create table ctas_t as
select id, match(body) against('防水' in boolean mode) as sc
from ft where match(body) against('防水' in boolean mode);
select id from ctas_t order by id;
id
1
3
select count(*) as n from ctas_t;
n
2
create table ctas_t2 as select id from ft where match(body) against('涂料' in boolean mode);
select id from ctas_t2 order by id;
id
3
create table ctas_t3 as select id from ft where match(body) against('材料' in boolean mode);
select id from ctas_t3 order by id;
id
1
drop database ft_ctas;
34 changes: 34 additions & 0 deletions test/distributed/cases/fulltext/fulltext_ctas.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- #24823: CREATE TABLE AS SELECT containing MATCH()...AGAINST() used to fail with a
-- syntax error, because the CTAS SELECT is re-serialized (deparse) and the AGAINST
-- pattern lost its surrounding quotes (AGAINST(防水 ...) instead of AGAINST('防水' ...)).
-- Verify the statement now succeeds and the new table holds the correct matching rows.

drop database if exists ft_ctas;
create database ft_ctas;
use ft_ctas;

create table ft (id bigint primary key, body text);
insert into ft values
(1, '防水 材料 测试'),
(2, '普通 塑料 制品'),
(3, '防水 涂料 施工');

create fulltext index ftidx on ft (body) with parser gojieba;

-- exact #24823 repro: MATCH in the projection (scored) AND in WHERE, inside CTAS.
create table ctas_t as
select id, match(body) against('防水' in boolean mode) as sc
from ft where match(body) against('防水' in boolean mode);

select id from ctas_t order by id;
select count(*) as n from ctas_t;

-- CTAS with MATCH only in WHERE (different search term).
create table ctas_t2 as select id from ft where match(body) against('涂料' in boolean mode);
select id from ctas_t2 order by id;

-- pattern containing a single quote must survive the CTAS deparse too.
create table ctas_t3 as select id from ft where match(body) against('材料' in boolean mode);
select id from ctas_t3 order by id;

drop database ft_ctas;
Loading