From c9561ecc377d324ec26eeb4b521e93e23abe9e46 Mon Sep 17 00:00:00 2001 From: zer0sub Date: Tue, 7 May 2019 13:44:30 +0200 Subject: [PATCH] Add LOCK table support. --- sqlbuilder/insert_statement.go | 5 -- sqlbuilder/literal_expression.go | 2 - sqlbuilder/lock_statement.go | 95 +++++++++++++++++++++++++++ sqlbuilder/lock_statement_test.go | 24 +++++++ sqlbuilder/set_statement_test.go | 6 +- sqlbuilder/statement.go | 105 +----------------------------- sqlbuilder/table.go | 6 ++ tests/select_test.go | 15 +++++ 8 files changed, 142 insertions(+), 116 deletions(-) create mode 100644 sqlbuilder/lock_statement.go create mode 100644 sqlbuilder/lock_statement_test.go diff --git a/sqlbuilder/insert_statement.go b/sqlbuilder/insert_statement.go index a138359..6f71cac 100644 --- a/sqlbuilder/insert_statement.go +++ b/sqlbuilder/insert_statement.go @@ -29,11 +29,6 @@ func newInsertStatement(t WritableTable, columns ...Column) InsertStatement { } } -type columnAssignment struct { - col Column - expr Expression -} - type insertStatementImpl struct { table WritableTable columns []Column diff --git a/sqlbuilder/literal_expression.go b/sqlbuilder/literal_expression.go index 1790036..d56d6fe 100644 --- a/sqlbuilder/literal_expression.go +++ b/sqlbuilder/literal_expression.go @@ -14,8 +14,6 @@ func Literal(value interface{}) *literalExpression { } func (l literalExpression) Serialize(out *queryData, options ...serializeOption) error { - //sqltypes.Value(c.value).EncodeSql(out) - out.InsertArgument(l.value) return nil diff --git a/sqlbuilder/lock_statement.go b/sqlbuilder/lock_statement.go new file mode 100644 index 0000000..e2fe998 --- /dev/null +++ b/sqlbuilder/lock_statement.go @@ -0,0 +1,95 @@ +package sqlbuilder + +import ( + "database/sql" + "github.com/pkg/errors" + "github.com/sub0zero/go-sqlbuilder/types" +) + +type lockMode string + +const ( + LOCK_ACCESS_SHARE = "ACCESS SHARE" + LOCK_ROW_SHARE = "ROW SHARE" + LOCK_ROW_EXCLUSIVE = "ROW EXCLUSIVE" + LOCK_SHARE_UPDATE_EXCLUSIVE = "SHARE UPDATE EXCLUSIVE" + LOCK_SHARE = "SHARE" + LOCK_SHARE_ROW_EXCLUSIVE = "SHARE ROW EXCLUSIVE" + LOCK_EXCLUSIVE = "EXCLUSIVE" + LOCK_ACCESS_EXCLUSIVE = "ACCESS EXCLUSIVE" +) + +type lockStatement interface { + Statement + + IN(lockMode lockMode) lockStatement + NOWAIT() lockStatement +} + +type lockStatementImpl struct { + tables []tableInterface + lockMode lockMode + nowait bool +} + +func LOCK(tables ...tableInterface) lockStatement { + return &lockStatementImpl{ + tables: tables, + } +} + +func (l *lockStatementImpl) IN(lockMode lockMode) lockStatement { + l.lockMode = lockMode + return l +} + +func (l *lockStatementImpl) NOWAIT() lockStatement { + l.nowait = true + return l +} + +func (l *lockStatementImpl) Sql() (query string, args []interface{}, err error) { + if l == nil { + return "", nil, errors.New("nil statement.") + } + + if len(l.tables) == 0 { + return "", nil, errors.New("There is no table selected to be locked. ") + } + + out := &queryData{} + + out.WriteString("LOCK TABLE ") + + for i, table := range l.tables { + if i > 0 { + out.WriteString(", ") + } + + err := table.SerializeSql(out) + + if err != nil { + return "", nil, err + } + } + + if l.lockMode != "" { + out.WriteString(" IN ") + out.WriteString(string(l.lockMode)) + out.WriteString(" MODE") + } + + if l.nowait { + out.WriteString(" NOWAIT") + } + + return out.buff.String(), out.args, nil +} + +func (l *lockStatementImpl) Query(db types.Db, destination interface{}) error { + return Query(l, db, destination) +} + +func (l *lockStatementImpl) Execute(db types.Db) (sql.Result, error) { + return Execute(l, db) +} diff --git a/sqlbuilder/lock_statement_test.go b/sqlbuilder/lock_statement_test.go new file mode 100644 index 0000000..cd625d9 --- /dev/null +++ b/sqlbuilder/lock_statement_test.go @@ -0,0 +1,24 @@ +package sqlbuilder + +import ( + "gotest.tools/assert" + "testing" +) + +func TestLockSingleTable(t *testing.T) { + lock := table1.LOCK().IN(LOCK_ROW_SHARE) + + queryStr, _, err := lock.Sql() + + assert.NilError(t, err) + assert.Equal(t, queryStr, `LOCK TABLE db.table1 IN ROW SHARE MODE`) +} + +func TestLockMultipleTable(t *testing.T) { + lock := LOCK(table2, table1).IN(LOCK_ACCESS_EXCLUSIVE).NOWAIT() + + queryStr, _, err := lock.Sql() + + assert.NilError(t, err) + assert.Equal(t, queryStr, `LOCK TABLE db.table2, db.table1 IN ACCESS EXCLUSIVE MODE NOWAIT`) +} diff --git a/sqlbuilder/set_statement_test.go b/sqlbuilder/set_statement_test.go index a5c6c04..91c038d 100644 --- a/sqlbuilder/set_statement_test.go +++ b/sqlbuilder/set_statement_test.go @@ -1,7 +1,6 @@ package sqlbuilder import ( - "fmt" "gotest.tools/assert" "testing" ) @@ -15,14 +14,11 @@ func TestUnionNoSelect(t *testing.T) { } func TestUnionOneSelect(t *testing.T) { - query, args, err := UNION( + _, _, err := UNION( table1.SELECT(table1Col1), ).Sql() assert.Assert(t, err != nil) - fmt.Println(err.Error()) - fmt.Println(query) - fmt.Println(args) } func TestUnionTwoSelect(t *testing.T) { diff --git a/sqlbuilder/statement.go b/sqlbuilder/statement.go index 2fa4095..2a29c92 100644 --- a/sqlbuilder/statement.go +++ b/sqlbuilder/statement.go @@ -13,115 +13,12 @@ type Statement interface { Execute(db types.Db) (sql.Result, error) } -// LockStatement is used to take Read/Write lock on tables. -// See http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html -//type LockStatement interface { -// Statement -// -// AddReadLock(table *Table) LockStatement -// AddWriteLock(table *Table) LockStatement -//} - -//// UnlockStatement can be used to release tableName locks taken using LockStatement. -//// NOTE: You can not selectively release a lock and continue to hold lock on -//// another tableName. UnlockStatement releases all the lock held in the current -//// session. -//type UnlockStatement interface { -// Statement -//} -// //// SetGtidNextStatement returns a SQL statement that can be used to explicitly set the next GTID. //type GtidNextStatement interface { // Statement //} // -//// -//// UNION SELECT Statement ====================================================== -//// -//// -//// LOCK statement =========================================================== -//// -// -//// NewLockStatement returns a SQL representing empty set of locks. You need to use -//// AddReadLock/AddWriteLock to add tables that need to be locked. -//// NOTE: You need at least one lock in the set for it to be a valid statement. -//func NewLockStatement() LockStatement { -// return &lockStatementImpl{} -//} -// -//type lockStatementImpl struct { -// locks []tableLock -//} -// -//type tableLock struct { -// t *Table -// w bool -//} -// -//func (l *lockStatementImpl) Execute(db *sql.DB, data interface{}) error { -// return nil -//} -// -//// AddReadLock takes read lock on the tableName. -//func (s *lockStatementImpl) AddReadLock(t *Table) LockStatement { -// s.locks = append(s.locks, tableLock{t: t, w: false}) -// return s -//} -// -//// AddWriteLock takes write lock on the tableName. -//func (s *lockStatementImpl) AddWriteLock(t *Table) LockStatement { -// s.locks = append(s.locks, tableLock{t: t, w: true}) -// return s -//} -// -//func (s *lockStatementImpl) String() (sql string, err error) { -// if len(s.locks) == 0 { -// return "", errors.New("No locks added") -// } -// -// buf := new(bytes.Buffer) -// _, _ = buf.WriteString("LOCK TABLES ") -// -// for idx, lock := range s.locks { -// if lock.t == nil { -// return "", errors.Newf("nil tableName.", buf.String()) -// } -// -// if err = lock.t.Serialize(buf); err != nil { -// return -// } -// -// if lock.w { -// _, _ = buf.WriteString(" WRITE") -// } else { -// _, _ = buf.WriteString(" READ") -// } -// -// if idx != len(s.locks)-1 { -// _, _ = buf.WriteString(", ") -// } -// } -// -// return buf.String(), nil -//} -// -//// NewUnlockStatement returns SQL statement that can be used to release tableName locks -//// grabbed by the current session. -//func NewUnlockStatement() UnlockStatement { -// return &unlockStatementImpl{} -//} -// -//type unlockStatementImpl struct { -//} -// -//func (u *unlockStatementImpl) Execute(db *sql.DB, data interface{}) error { -// return nil -//} -// -//func (s *unlockStatementImpl) String() (sql string, err error) { -// return "UNLOCK TABLES", nil -//} -// + //// SET GTID_NEXT statement returns a SQL statement that can be used to explicitly set the next GTID. //func NewGtidNextStatement(sid []byte, gno uint64) GtidNextStatement { // return >idNextStatementImpl{ diff --git a/sqlbuilder/table.go b/sqlbuilder/table.go index f6eefea..2f11abc 100644 --- a/sqlbuilder/table.go +++ b/sqlbuilder/table.go @@ -44,6 +44,8 @@ type WritableTable interface { INSERT(columns ...Column) InsertStatement UPDATE(columns ...Column) UpdateStatement DELETE() DeleteStatement + + LOCK() lockStatement } // Defines a physical tableName in the database that is both readable and writable. @@ -172,6 +174,10 @@ func (t *Table) DELETE() DeleteStatement { return newDeleteStatement(t) } +func (t *Table) LOCK() lockStatement { + return LOCK(t) +} + type joinType int const ( diff --git a/tests/select_test.go b/tests/select_test.go index dc88625..5dc6b70 100644 --- a/tests/select_test.go +++ b/tests/select_test.go @@ -694,6 +694,21 @@ func TestSelectWithCase(t *testing.T) { assert.Equal(t, dest[1].StaffIdNum, "ONE") } +func TestLockTable(t *testing.T) { + query := Address.LOCK().IN(sqlbuilder.LOCK_EXCLUSIVE).NOWAIT() + + queryStr, _, err := query.Sql() + + assert.NilError(t, err) + assert.Equal(t, queryStr, `LOCK TABLE dvds.address IN EXCLUSIVE MODE NOWAIT`) + + tx, _ := db.Begin() + + _, err = query.Execute(tx) + + assert.NilError(t, err) +} + func int16Ptr(i int16) *int16 { return &i }