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
107 changes: 107 additions & 0 deletions contrib/drivers/sqlitecgo/sqlitecgo_do_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package sqlitecgo

import (
"context"
"strings"

"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/text/gstr"
Expand All @@ -25,5 +26,111 @@ func (d *Driver) DoFilter(
case gstr.HasPrefix(sql, gdb.InsertOperationReplace):
sql = "INSERT OR REPLACE" + sql[len(gdb.InsertOperationReplace):]
}
// The core parenthesizes compound and EXISTS sub-queries in ways SQLite rejects.
sql = unwrapDoubledExistsParentheses(sql)
sql = wrapUnionOperandsAsSubQueries(sql)
return d.Core.DoFilter(ctx, link, sql, args)
}

// wrapUnionOperandsAsSubQueries rewrites the compound query the core builds as
// `(SELECT ...) UNION (SELECT ...)` into `SELECT * FROM (SELECT ...) UNION SELECT * FROM (SELECT ...)`.
// SQLite forbids parentheses around the operands of a compound SELECT, and also forbids
// ORDER BY and LIMIT on any operand but the last; wrapping each operand as a sub-query in
// FROM keeps both legal and preserves the meaning.
func wrapUnionOperandsAsSubQueries(sql string) string {
if !strings.HasPrefix(sql, "(") {
return sql
}
var (
b strings.Builder
operands = 0
)
b.Grow(len(sql) + 64)
for i := 0; i < len(sql); {
if sql[i] != '(' {
b.WriteByte(sql[i])
i++
continue
}
if !precededByUnionOrStart(sql, i) {
b.WriteByte(sql[i])
i++
continue
}
end := matchingParenthesis(sql, i)
if end < 0 {
return sql
}
operands++
b.WriteString("SELECT * FROM ")
b.WriteString(sql[i : end+1])
i = end + 1
}
if operands < 2 {
return sql
}
return b.String()
}

// precededByUnionOrStart reports whether the parenthesis at `pos` opens a compound
// operand: it is the first character, or follows `UNION` / `UNION ALL`.
func precededByUnionOrStart(sql string, pos int) bool {
if pos == 0 {
return true
}
head := strings.ToUpper(strings.TrimRight(sql[:pos], " "))
return strings.HasSuffix(head, " UNION") || strings.HasSuffix(head, " UNION ALL")
}

// unwrapDoubledExistsParentheses rewrites `EXISTS ((SELECT ...))`, which the core builds
// by parenthesizing an already parenthesized sub-query, into `EXISTS (SELECT ...)`.
// SQLite rejects the doubled parentheses as a syntax error.
func unwrapDoubledExistsParentheses(sql string) string {
const marker = "EXISTS (("
for {
pos := gstr.PosI(sql, marker)
if pos < 0 {
return sql
}
outer := pos + len(marker) - 2
end := matchingParenthesis(sql, outer)
if end < 0 {
return sql
}
sql = sql[:outer] + sql[outer+1:end] + sql[end+1:]
}
}

// matchingParenthesis returns the index of the parenthesis closing the one at `open`,
// ignoring parentheses inside quoted strings and identifiers, or -1 if unbalanced.
func matchingParenthesis(sql string, open int) int {
var (
depth = 0
quote byte
)
for i := open; i < len(sql); i++ {
c := sql[i]
if quote != 0 {
if c == quote {
if quote == '\'' && i+1 < len(sql) && sql[i+1] == '\'' {
i++
continue
}
quote = 0
}
continue
}
switch c {
case '\'', '"', '`':
quote = c
case '(':
depth++
case ')':
depth--
if depth == 0 {
return i
}
}
}
return -1
}
4 changes: 2 additions & 2 deletions contrib/drivers/sqlitecgo/sqlitecgo_open.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) {
}

// Multiple PRAGMAs can be specified, e.g.:
// path/to/some.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)
// path/to/some.db?_busy_timeout=5000&_journal_mode=WAL
if config.Extra != "" {
var (
options string
Expand All @@ -54,7 +54,7 @@ func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) {
if options != "" {
options += "&"
}
options += fmt.Sprintf(`_pragma=%s(%s)`, k, gurl.Encode(gconv.String(v)))
options += fmt.Sprintf(`_%s=%s`, k, gurl.Encode(gconv.String(v)))
}
if len(options) > 1 {
source += "?" + options
Expand Down
12 changes: 12 additions & 0 deletions contrib/drivers/sqlitecgo/sqlitecgo_order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

package sqlitecgo

// OrderRandomFunction returns the SQL function for random ordering.
func (d *Driver) OrderRandomFunction() string {
return "RANDOM()"
}
83 changes: 83 additions & 0 deletions contrib/drivers/sqlitecgo/sqlitecgo_z_unit_basic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

package sqlitecgo_test

import (
"context"
"testing"

"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/test/gtest"
)

func Test_Instance(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
_, err := gdb.Instance("none")
t.AssertNE(err, nil)

db, err := gdb.Instance()
t.AssertNil(err)

err1 := db.PingMaster()
err2 := db.PingSlave()
t.Assert(err1, nil)
t.Assert(err2, nil)
})
}

func Test_Func_FormatSqlWithArgs(t *testing.T) {
// mysql
gtest.C(t, func(t *gtest.T) {
var s string
s = gdb.FormatSqlWithArgs("select * from table where id>=? and sex=?", []any{100, 1})
t.Assert(s, "select * from table where id>=100 and sex=1")
})
// mssql
gtest.C(t, func(t *gtest.T) {
var s string
s = gdb.FormatSqlWithArgs("select * from table where id>=@p1 and sex=@p2", []any{100, 1})
t.Assert(s, "select * from table where id>=100 and sex=1")
})
// pgsql
gtest.C(t, func(t *gtest.T) {
var s string
s = gdb.FormatSqlWithArgs("select * from table where id>=$1 and sex=$2", []any{100, 1})
t.Assert(s, "select * from table where id>=100 and sex=1")
})
// oracle
gtest.C(t, func(t *gtest.T) {
var s string
s = gdb.FormatSqlWithArgs("select * from table where id>=:v1 and sex=:v2", []any{100, 1})
t.Assert(s, "select * from table where id>=100 and sex=1")
})
}

func Test_Func_ToSQL(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
sql, err := gdb.ToSQL(ctx, func(ctx context.Context) error {
value, err := db.Ctx(ctx).Model(TableName).Fields("nickname").Where("id", 1).Value()
t.Assert(value, nil)
return err
})
t.AssertNil(err)
t.Assert(sql, "SELECT `nickname` FROM `user` WHERE `id`=1 LIMIT 1")
})
}

func Test_Func_CatchSQL(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
array, err := gdb.CatchSQL(ctx, func(ctx context.Context) error {
value, err := db.Ctx(ctx).Model(table).Fields("nickname").Where("id", 1).Value()
t.Assert(value, "name_1")
return err
})
t.AssertNil(err)
t.AssertGE(len(array), 1)
})
}
148 changes: 148 additions & 0 deletions contrib/drivers/sqlitecgo/sqlitecgo_z_unit_core_extra_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

package sqlitecgo_test

import (
"context"
"fmt"
"testing"
"time"

"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/text/gstr"
)

func Test_DB_Insert_NilGjson(t *testing.T) {
var tableName = "nil" + gtime.TimestampNanoStr()
_, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
id INTEGER PRIMARY KEY AUTOINCREMENT,
json_empty_string TEXT DEFAULT NULL,
json_nil TEXT DEFAULT NULL,
json_null TEXT DEFAULT NULL
);
`, tableName))
if err != nil {
gtest.Fatal(err)
}
defer dropTable(tableName)

gtest.C(t, func(t *gtest.T) {
type Json struct {
Id int
JsonEmptyString *gjson.Json
JsonNil *gjson.Json
JsonNull *gjson.Json
}

data := Json{
Id: 1,
JsonEmptyString: gjson.New(""),
JsonNil: gjson.New(nil),
JsonNull: gjson.New(struct{}{}),
}

_, err = db.Insert(ctx, tableName, data)
t.AssertNil(err)

one, err := db.GetOne(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", tableName), 1)
t.AssertNil(err)

t.AssertEQ(len(one), 4)

t.Assert(one["json_empty_string"], nil)
t.Assert(one["json_nil"], nil)
t.Assert(one["json_null"], "null")
})
}

func Test_Model_RightJoin(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
table1 := createInitTable("user1")
table2 := createInitTable("user2")

defer dropTable(table1)
defer dropTable(table2)

res, err := db.Model(table1).Where("id > ?", 3).Delete()
if err != nil {
t.Fatal(err)
}

n, err := res.RowsAffected()
if err != nil {
t.Fatal(err)
}

t.Assert(n, 7)

result, err := db.Model(table1+" u1").RightJoin(table2+" u2", "u1.id = u2.id").All()
if err != nil {
t.Fatal(err)
}
t.Assert(len(result), 10)

result, err = db.Model(table1+" u1").RightJoin(table2+" u2", "u1.id = u2.id").Where("u1.id > 2").All()
if err != nil {
t.Fatal(err)
}
t.Assert(len(result), 1)
})
}

func Test_DB_Ctx(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
defer cancel()
_, err := db.Query(ctx, `
WITH RECURSIVE counter(i) AS (
SELECT 1 UNION ALL SELECT i+1 FROM counter WHERE i < 1000000
)
SELECT COUNT(*) FROM counter
`)
t.AssertNE(err, nil)
t.Assert(gstr.Contains(err.Error(), "deadline"), true)
})
}

func Test_Core_ClearTableFields(t *testing.T) {
table := createTable()
defer dropTable(table)

gtest.C(t, func(t *gtest.T) {
fields, err := db.TableFields(ctx, table)
t.AssertNil(err)
t.Assert(len(fields), 5)
})
gtest.C(t, func(t *gtest.T) {
err := db.GetCore().ClearTableFields(ctx, table)
t.AssertNil(err)
})
}

func Test_Core_ClearTableFieldsAll(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
err := db.GetCore().ClearTableFieldsAll(ctx)
t.AssertNil(err)
})
}

func Test_Core_ClearCache(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
err := db.GetCore().ClearCache(ctx, "")
t.AssertNil(err)
})
}

func Test_Core_ClearCacheAll(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
err := db.GetCore().ClearCacheAll(ctx)
t.AssertNil(err)
})
}
Loading
Loading