diff --git a/dialect/duckdb/duckdb.go b/dialect/duckdb/duckdb.go index 817fdeb..fa7d7bf 100644 --- a/dialect/duckdb/duckdb.go +++ b/dialect/duckdb/duckdb.go @@ -4,6 +4,7 @@ import ( _ "github.com/marcboeker/go-duckdb" "git.fsdpf.net/go/db" + "git.fsdpf.net/go/db/exp" ) func DialectOptions() *db.SQLDialectOptions { @@ -30,6 +31,12 @@ func DialectOptions() *db.SQLDialectOptions { // Use double quotes for identifiers (PostgreSQL-style) do.QuoteRune = '"' + // regexp_matches(col, pattern) 做部分匹配,等价于 MySQL REGEXP + do.BooleanFunctionLookup = map[exp.BooleanOperation][]byte{ + exp.RegexpLikeOp: []byte("regexp_matches"), + exp.RegexpILikeOp: []byte("regexp_matches"), + } + return do } diff --git a/dialect/duckdb/duckdb_test.go b/dialect/duckdb/duckdb_test.go index 41c4c74..56e35c1 100644 --- a/dialect/duckdb/duckdb_test.go +++ b/dialect/duckdb/duckdb_test.go @@ -107,6 +107,32 @@ func TestJoinSQL(t *testing.T) { assert.Contains(t, sql, `ON ("users"."id" = "orders"."user_id")`) } +func TestRegexpSQL(t *testing.T) { + db := dbv2.Dialect("duckdb") + + // RegexpLike → regexp_matches(col, pattern) + ds := db.From("docs").Where(dbv2.C("content").RegexpLike("简介")) + sql, _, err := ds.ToSQL() + assert.NoError(t, err) + assert.Equal(t, `SELECT * FROM "docs" WHERE regexp_matches("content", '简介')`, sql) + + // RegexpILike → 同样生成 regexp_matches + ds = db.From("docs").Where(dbv2.C("content").RegexpILike("简介")) + sql, _, err = ds.ToSQL() + assert.NoError(t, err) + assert.Equal(t, `SELECT * FROM "docs" WHERE regexp_matches("content", '简介')`, sql) +} + +func TestRegexpPreparedSQL(t *testing.T) { + db := dbv2.Dialect("duckdb") + + ds := db.From("docs").Prepared(true).Where(dbv2.C("content").RegexpLike("简介")) + sql, args, err := ds.ToSQL() + assert.NoError(t, err) + assert.Equal(t, `SELECT * FROM "docs" WHERE regexp_matches("content", $1)`, sql) + assert.Equal(t, []interface{}{"简介"}, args) +} + func TestWindowFunction(t *testing.T) { db := dbv2.Dialect("duckdb") diff --git a/sqlgen/expression_sql_generator.go b/sqlgen/expression_sql_generator.go index d47cc48..1c1e2b2 100644 --- a/sqlgen/expression_sql_generator.go +++ b/sqlgen/expression_sql_generator.go @@ -386,10 +386,22 @@ func (esg *expressionSQLGenerator) aliasedExpressionSQL(b sb.SQLBuilder, aliased // Generates SQL for a BooleanExpresion (e.g. I("a").Eq(2) -> "a" = 2) func (esg *expressionSQLGenerator) booleanExpressionSQL(b sb.SQLBuilder, operator exp.BooleanExpression) { + operatorOp := operator.Op() + + // 函数式操作符:生成 FUNC(LHS, RHS) + if fn, ok := esg.dialectOptions.BooleanFunctionLookup[operatorOp]; ok { + b.Write(fn) + b.WriteRunes(esg.dialectOptions.LeftParenRune) + esg.Generate(b, operator.LHS()) + b.WriteRunes(esg.dialectOptions.CommaRune, esg.dialectOptions.SpaceRune) + esg.Generate(b, operator.RHS()) + b.WriteRunes(esg.dialectOptions.RightParenRune) + return + } + b.WriteRunes(esg.dialectOptions.LeftParenRune) esg.Generate(b, operator.LHS()) b.WriteRunes(esg.dialectOptions.SpaceRune) - operatorOp := operator.Op() if val, ok := esg.dialectOptions.BooleanOperatorLookup[operatorOp]; ok { b.Write(val) } else { diff --git a/sqlgen/expression_sql_generator_test.go b/sqlgen/expression_sql_generator_test.go index 70866c3..73790e9 100644 --- a/sqlgen/expression_sql_generator_test.go +++ b/sqlgen/expression_sql_generator_test.go @@ -127,8 +127,8 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_UnsupportedType() { type strct struct{} esgs.assertCases( sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), - expressionTestCase{val: strct{}, err: "dbv2_encode_error: Unable to encode value {}"}, - expressionTestCase{val: strct{}, err: "dbv2_encode_error: Unable to encode value {}", isPrepared: true}, + expressionTestCase{val: strct{}, err: "db_encode_error: Unable to encode value {}"}, + expressionTestCase{val: strct{}, err: "db_encode_error: Unable to encode value {}", isPrepared: true}, ) } @@ -624,6 +624,24 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_BooleanExpression() { expressionTestCase{val: ident.NotILike("a%"), err: "db: boolean operator 'notilike' not supported"}, expressionTestCase{val: ident.NotILike(re), err: "db: boolean operator 'regexpnotilike' not supported"}, ) + + // BooleanFunctionLookup 生成 FUNC(LHS, RHS) 格式,优先级高于 BooleanOperatorLookup + opts2 := sqlgen.DefaultDialectOptions() + opts2.BooleanFunctionLookup = map[exp.BooleanOperation][]byte{ + exp.RegexpLikeOp: []byte("regexp_matches"), + exp.RegexpILikeOp: []byte("regexp_matches"), + } + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts2), + // 函数调用格式 + expressionTestCase{val: ident.Like(re), sql: `regexp_matches("a", '[ab]')`}, + expressionTestCase{val: ident.Like(re), sql: `regexp_matches("a", ?)`, isPrepared: true, args: []interface{}{"[ab]"}}, + expressionTestCase{val: ident.ILike(re), sql: `regexp_matches("a", '[ab]')`}, + expressionTestCase{val: ident.ILike(re), sql: `regexp_matches("a", ?)`, isPrepared: true, args: []interface{}{"[ab]"}}, + // 未在 BooleanFunctionLookup 中的操作符仍走中缀格式 + expressionTestCase{val: ident.Eq(1), sql: `("a" = 1)`}, + expressionTestCase{val: ident.NotLike(re), sql: `("a" !~ '[ab]')`}, + ) } func (esgs *expressionSQLGeneratorSuite) TestGenerate_BitwiseExpression() { diff --git a/sqlgen/sql_dialect_options.go b/sqlgen/sql_dialect_options.go index ed8d4a8..480c54d 100644 --- a/sqlgen/sql_dialect_options.go +++ b/sqlgen/sql_dialect_options.go @@ -219,6 +219,8 @@ type ( // exp.RegexpNotILikeOp: []byte("!~*"), // }) BooleanOperatorLookup map[exp.BooleanOperation][]byte + // 函数式布尔操作符,生成 FUNC(LHS, RHS) 格式,优先级高于 BooleanOperatorLookup + BooleanFunctionLookup map[exp.BooleanOperation][]byte // A map used to look up BitwiseOperations and their SQL equivalents // (Default=map[exp.BitwiseOperation][]byte{ // exp.BitwiseInversionOp: []byte("~"), diff --git a/sqlgen/sql_dialect_options_test.go b/sqlgen/sql_dialect_options_test.go index 35f1b40..48b2b1f 100644 --- a/sqlgen/sql_dialect_options_test.go +++ b/sqlgen/sql_dialect_options_test.go @@ -44,6 +44,12 @@ func (sfts *sqlFragmentTypeSuite) TestOptions_SQLFragmentType() { } } +func (sfts *sqlFragmentTypeSuite) TestDefaultDialectOptions_BooleanFunctionLookup() { + opts := sqlgen.DefaultDialectOptions() + // 默认不设置函数式操作符,各方言按需覆盖 + sfts.Nil(opts.BooleanFunctionLookup) +} + func TestSQLFragmentType(t *testing.T) { suite.Run(t, new(sqlFragmentTypeSuite)) }