diff --git a/mysql/_example/main.go b/mysql/_example/main.go index 8b073845520baa9c21248fb502760871d4a9d254..f84ee44873407e37e7b88aed88fd946b46579ef0 100644 --- a/mysql/_example/main.go +++ b/mysql/_example/main.go @@ -6,13 +6,13 @@ import ( "os" "time" - "upper.io/db" // Imports the main db package. - _ "upper.io/db/mysql" // Improts the mysql adapter. + "upper.io/db" // Imports the main db package. + "upper.io/db/mysql" // Improts the mysql adapter. ) -var settings = db.Settings{ +var settings = mysql.ConnectionURL{ Database: `upperio_tests`, // Database name. - Host: `127.0.0.1`, + Address: db.ParseAddress(`127.0.0.1`), User: `upperio_tests`, // Database username. Password: `upperio_secret`, // Database password. } @@ -27,8 +27,8 @@ type Birthday struct { func main() { - if os.Getenv("TEST_HOST") != "" { - settings.Host = os.Getenv("TEST_HOST") + if addr := os.Getenv("TEST_HOST"); addr != "" { + settings.Address = db.ParseAddress(addr) } // Attemping to establish a connection to the database. diff --git a/mysql/collection.go b/mysql/collection.go index 429099daaa7d7ac57f7474a07352feb61542cc61..5a70b2bd14113ff684b8e8f94dee0534d11a615d 100644 --- a/mysql/collection.go +++ b/mysql/collection.go @@ -23,35 +23,27 @@ package mysql import ( "database/sql" - "strings" - "upper.io/db" + "upper.io/builder" "upper.io/builder/sqlgen" - "upper.io/db/internal/sqlutil" - "upper.io/db/internal/sqlutil/result" + "upper.io/db" + "upper.io/db/internal/sqladapter" ) type table struct { - sqlutil.T - *database + sqladapter.Collection } var _ = db.Collection(&table{}) -// Find creates a result set with the given conditions. -func (t *table) Find(terms ...interface{}) db.Result { - where, arguments := template.ToWhereWithArguments(terms) - return result.NewResult(template, t, where, arguments) -} - // Truncate deletes all rows from the table. func (t *table) Truncate() error { - _, err := t.database.Exec(&sqlgen.Statement{ + stmt := sqlgen.Statement{ Type: sqlgen.Truncate, - Table: sqlgen.TableWithName(t.MainTableName()), - }) + Table: sqlgen.TableWithName(t.Name()), + } - if err != nil { + if _, err := t.Database().Builder().Exec(&stmt); err != nil { return err } return nil @@ -59,36 +51,24 @@ func (t *table) Truncate() error { // Append inserts an item (map or struct) into the collection. func (t *table) Append(item interface{}) (interface{}, error) { - var pKey []string - - columnNames, columnValues, err := t.FieldValues(item) - + columnNames, columnValues, err := builder.Map(item) if err != nil { return nil, err } - sqlgenCols, sqlgenVals, sqlgenArgs, err := template.ToColumnsValuesAndArguments(columnNames, columnValues) - - if err != nil { - return nil, err - } - - if pKey, err = t.database.getPrimaryKey(t.MainTableName()); err != nil { + var pKey []string + if pKey, err = t.Database().TablePrimaryKey(t.Name()); err != nil { if err != sql.ErrNoRows { - // Can't tell primary key. return nil, err } } - stmt := &sqlgen.Statement{ - Type: sqlgen.Insert, - Table: sqlgen.TableWithName(t.MainTableName()), - Columns: sqlgenCols, - Values: sqlgenVals, - } + q := t.Database().Builder().InsertInto(t.Name()). + Columns(columnNames...). + Values(columnValues...) var res sql.Result - if res, err = t.database.Exec(stmt, sqlgenArgs...); err != nil { + if res, err = q.Exec(); err != nil { return nil, err } @@ -141,15 +121,6 @@ func (t *table) Append(item interface{}) (interface{}, error) { return keyMap, nil } -// Exists returns true if the collection exists. -func (t *table) Exists() bool { - if err := t.database.tableExists(t.Tables...); err != nil { - return false - } - return true -} - -// Name returns the name of the table or tables that form the collection. -func (t *table) Name() string { - return strings.Join(t.Tables, `, `) +func newTable(d *database, name string) *table { + return &table{sqladapter.NewCollection(d, name)} } diff --git a/mysql/connection.go b/mysql/connection.go index e9264c905946dd2f83780f93034d36ddeed657b8..2e83a40db7407d86efb96a10980bc5e96008b8fe 100644 --- a/mysql/connection.go +++ b/mysql/connection.go @@ -99,6 +99,10 @@ func (c ConnectionURL) String() (s string) { c.Options["charset"] = "utf8" } + if _, ok := c.Options["parseTime"]; !ok { + c.Options["parseTime"] = "true" + } + // Converting options into URL values. vv := url.Values{} diff --git a/mysql/connection_test.go b/mysql/connection_test.go index e306aa9a519e3efab197d21fc2a110865fcf7d39..d0b7cc44e511c32bbcf72ca6dc9df8fd93e11d4a 100644 --- a/mysql/connection_test.go +++ b/mysql/connection_test.go @@ -39,7 +39,7 @@ func TestConnectionURL(t *testing.T) { // Adding a database name. c.Database = "mydbname" - if c.String() != "/mydbname?charset=utf8" { + if c.String() != "/mydbname?charset=utf8&parseTime=true" { t.Fatal(`Test failed, got:`, c.String()) } @@ -49,7 +49,7 @@ func TestConnectionURL(t *testing.T) { "sys_var": "esc@ped", } - if c.String() != "/mydbname?charset=utf8mb4%2Cutf8&sys_var=esc%40ped" { + if c.String() != "/mydbname?charset=utf8mb4%2Cutf8&parseTime=true&sys_var=esc%40ped" { t.Fatal(`Test failed, got:`, c.String()) } @@ -60,21 +60,21 @@ func TestConnectionURL(t *testing.T) { c.User = "user" c.Password = "pass" - if c.String() != `user:pass@/mydbname?charset=utf8` { + if c.String() != `user:pass@/mydbname?charset=utf8&parseTime=true` { t.Fatal(`Test failed, got:`, c.String()) } // Setting host. c.Address = db.HostPort("1.2.3.4", 3306) - if c.String() != `user:pass@tcp(1.2.3.4:3306)/mydbname?charset=utf8` { + if c.String() != `user:pass@tcp(1.2.3.4:3306)/mydbname?charset=utf8&parseTime=true` { t.Fatal(`Test failed, got:`, c.String()) } // Setting socket. c.Address = db.Socket("/path/to/socket") - if c.String() != `user:pass@unix(/path/to/socket)/mydbname?charset=utf8` { + if c.String() != `user:pass@unix(/path/to/socket)/mydbname?charset=utf8&parseTime=true` { t.Fatal(`Test failed, got:`, c.String()) } diff --git a/mysql/database.go b/mysql/database.go index aa1c567b86ac5f64e8162e4f36476dbafcebfdce..1091ce5d32a96c6ae47380907988b8c23389052e 100644 --- a/mysql/database.go +++ b/mysql/database.go @@ -22,345 +22,120 @@ package mysql import ( - "database/sql" "strings" - "sync" - "time" _ "github.com/go-sql-driver/mysql" // MySQL driver. "github.com/jmoiron/sqlx" - "upper.io/cache" - "upper.io/db" - "upper.io/db/internal/adapter" - "upper.io/db/internal/schema" "upper.io/builder/sqlgen" - "upper.io/db/internal/sqlutil" + template "upper.io/builder/template/mysql" + "upper.io/db" + "upper.io/db/internal/sqladapter" "upper.io/db/internal/sqlutil/tx" ) -var ( - sqlPlaceholder = sqlgen.RawValue(`?`) -) - type database struct { - connURL db.ConnectionURL - session *sqlx.DB - tx *sqltx.Tx - schema *schema.DatabaseSchema - cachedStatements *cache.Cache - collections map[string]*table - collectionsMu sync.Mutex + *sqladapter.BaseDatabase } -type cachedStatement struct { - *sqlx.Stmt - query string -} - -var waitForConnMu sync.Mutex - -var ( - _ = db.Database(&database{}) - _ = db.Tx(&tx{}) -) +var _ = db.Database(&database{}) -type columnSchemaT struct { - Name string `db:"column_name"` +// CompileAndReplacePlaceholders compiles the given statement into an string +// and replaces each generic placeholder with the placeholder the driver +// expects (if any). +func (d *database) CompileAndReplacePlaceholders(stmt *sqlgen.Statement) (query string) { + return stmt.Compile(d.Template()) } -func (d *database) prepareStatement(stmt *sqlgen.Statement) (p *sqlx.Stmt, query string, err error) { - if d.session == nil { - return nil, "", db.ErrNotConnected - } - - pc, ok := d.cachedStatements.ReadRaw(stmt) - - if ok { - ps := pc.(*cachedStatement) - p = ps.Stmt - query = ps.query - } else { - query = compileAndReplacePlaceholders(stmt) - - if d.tx != nil { - p, err = d.tx.Preparex(query) - } else { - p, err = d.session.Preparex(query) - } - - if err != nil { - return nil, query, err +// Err translates some known errors into generic errors. +func (d *database) Err(err error) error { + if err != nil { + s := err.Error() + if strings.Contains(s, `many connections`) { + return db.ErrTooManyClients } - - d.cachedStatements.Write(stmt, &cachedStatement{p, query}) } - - return p, query, nil -} - -func compileAndReplacePlaceholders(stmt *sqlgen.Statement) string { - return stmt.Compile(template.Template) -} - -// Driver returns the underlying *sqlx.DB instance. -func (d *database) Driver() interface{} { - return d.session + return err } -// Open attempts to connect to the database server using already stored settings. +// Open attempts to open a connection to the database server. func (d *database) Open() error { - var err error + var sess *sqlx.DB - // Before db.ConnectionURL we used a unified db.Settings struct. This - // condition checks for that type and provides backwards compatibility. - if settings, ok := d.connURL.(db.Settings); ok { - - // User is providing a db.Settings struct, let's translate it into a - // ConnectionURL{}. - conn := ConnectionURL{ - User: settings.User, - Password: settings.Password, - Database: settings.Database, - Options: map[string]string{ - "charset": settings.Charset, - }, - } - - // Connection charset, UTF-8 by default. - if conn.Options["charset"] == "" { - conn.Options["charset"] = "utf8" - } - - // parseTime=true changes the output type of DATE and DATETIME values to time.Time instead of []byte / string - if conn.Options["parseTime"] == "" { - conn.Options["parseTime"] = "true" - } - - if settings.Socket != "" { - conn.Address = db.Socket(settings.Socket) - } else { - if settings.Host == "" { - settings.Host = "127.0.0.1" - } - if settings.Port == 0 { - settings.Port = defaultPort - } - conn.Address = db.HostPort(settings.Host, uint(settings.Port)) - } - - // Replace original d.connURL - d.connURL = conn - } - - connFn := func(d **database) (err error) { - (*d).session, err = sqlx.Open(`mysql`, (*d).connURL.String()) + connFn := func(sess **sqlx.DB) (err error) { + *sess, err = sqlx.Open("mysql", d.ConnectionURL().String()) return } - if err := waitForConnection(func() error { return connFn(&d) }); err != nil { + if err := d.WaitForConnection(func() error { return connFn(&sess) }); err != nil { return err } - d.session.Mapper = sqlutil.NewMapper() - - d.cachedStatements = cache.NewCache() - - d.collections = make(map[string]*table) - - if d.schema == nil { - if err = d.populateSchema(); err != nil { - return err - } - } - - return nil + return d.Bind(sess) } -// Clone returns a cloned db.Database session, this is typically used for -// transactions. -func (d *database) Clone() (db.Database, error) { - return d.clone() +// Setup configures the adapter. +func (d *database) Setup(connURL db.ConnectionURL) error { + d.BaseDatabase = sqladapter.NewDatabase(d, connURL, template.Template()) + return d.Open() } -func (d *database) clone() (*database, error) { - clone := &database{ - schema: d.schema, - } - if err := clone.Setup(d.connURL); err != nil { - return nil, err +// Use changes the active database. +func (d *database) Use(name string) (err error) { + var conn ConnectionURL + if conn, err = ParseURL(d.ConnectionURL().String()); err != nil { + return err } - return clone, nil -} - -// Ping checks whether a connection to the database is still alive by pinging -// it, establishing a connection if necessary. -func (d *database) Ping() error { - return d.session.Ping() -} - -// Close terminates the current database session. -func (d *database) Close() error { - if d.session != nil { - if d.tx != nil && !d.tx.Done() { - d.tx.Rollback() - } - d.cachedStatements.Clear() - return d.session.Close() + conn.Database = name + if d.BaseDatabase != nil { + d.Close() } - return nil + return d.Setup(conn) } -// C returns a collection interface. -func (d *database) C(names ...string) db.Collection { - if len(names) == 0 { - return &adapter.NonExistentCollection{Err: db.ErrMissingCollectionName} - } - - if c, ok := d.collections[sqlutil.HashTableNames(names)]; ok { - return c - } - - c, err := d.Collection(names...) - if err != nil { - return &adapter.NonExistentCollection{Err: err} - } - return c +// Clone creates a new database connection with the same settings as the +// original. +func (d *database) Clone() (db.Database, error) { + return d.clone() } -// Collection returns a table by name. -func (d *database) Collection(names ...string) (db.Collection, error) { - var err error - - if len(names) == 0 { - return nil, db.ErrMissingCollectionName - } - - if d.tx != nil { - if d.tx.Done() { - return nil, sql.ErrTxDone - } - } - - col := &table{database: d} - col.T.Tables = names - col.T.Mapper = d.session.Mapper - - for _, name := range names { - chunks := strings.SplitN(name, ` `, 2) - - if len(chunks) == 0 { - return nil, db.ErrMissingCollectionName - } - - tableName := chunks[0] - - if err := d.tableExists(tableName); err != nil { - return nil, err - } - - if col.Columns, err = d.tableColumns(tableName); err != nil { - return nil, err - } - } - - // Saving the collection for C(). - d.collectionsMu.Lock() - d.collections[sqlutil.HashTableNames(names)] = col - d.collectionsMu.Unlock() - - return col, nil +// NewTable returns a db.Collection. +func (d *database) NewTable(name string) db.Collection { + return newTable(d, name) } // Collections returns a list of non-system tables from the database. func (d *database) Collections() (collections []string, err error) { - tablesInSchema := len(d.schema.Tables) - - // Is schema already populated? - if tablesInSchema > 0 { - // Pulling table names from schema. - return d.schema.Tables, nil - } - - stmt := &sqlgen.Statement{ - Type: sqlgen.Select, - Columns: sqlgen.JoinColumns( - sqlgen.ColumnWithName(`table_name`), - ), - Table: sqlgen.TableWithName(`information_schema.tables`), - Where: sqlgen.WhereConditions( - &sqlgen.ColumnValue{ - Column: sqlgen.ColumnWithName(`table_schema`), - Operator: `=`, - Value: sqlPlaceholder, - }, - ), - } - - // Executing statement. - var rows *sqlx.Rows - if rows, err = d.Query(stmt, d.schema.Name); err != nil { - return nil, err - } + if len(d.Schema().Tables) == 0 { + q := d.Builder().Select("table_name"). + From("information_schema.tables"). + Where("table_schema = ?", d.Schema().Name) - collections = []string{} + iter := q.Iterator() + defer iter.Close() - var name string - - for rows.Next() { - // Getting table name. - if err = rows.Scan(&name); err != nil { - rows.Close() - return nil, err + for iter.Next() { + var tableName string + if err := iter.Scan(&tableName); err != nil { + return nil, err + } + d.Schema().AddTable(tableName) } - - // Adding table entry to schema. - d.schema.AddTable(name) - - // Adding table to collections array. - collections = append(collections, name) } - return collections, nil -} - -// Use changes the active database. -func (d *database) Use(name string) (err error) { - var conn ConnectionURL - - if conn, err = ParseURL(d.connURL.String()); err != nil { - return err - } - - conn.Database = name - - d.connURL = conn - - d.schema = nil - - return d.Open() + return d.Schema().Tables, nil } // Drop removes all tables from the current database. func (d *database) Drop() error { - - _, err := d.Query(&sqlgen.Statement{ + stmt := &sqlgen.Statement{ Type: sqlgen.DropDatabase, - Database: sqlgen.DatabaseWithName(d.schema.Name), - }) - - return err -} - -// Setup stores database settings. -func (d *database) Setup(connURL db.ConnectionURL) error { - d.connURL = connURL - return d.Open() -} - -// Name returns the name of the database. -func (d *database) Name() string { - return d.schema.Name + Database: sqlgen.DatabaseWithName(d.Schema().Name), + } + if _, err := d.Builder().Exec(stmt); err != nil { + return err + } + return nil } // Transaction starts a transaction block and returns a db.Tx struct that can @@ -375,119 +150,48 @@ func (d *database) Transaction() (db.Tx, error) { } connFn := func(sqlTx **sqlx.Tx) (err error) { - *sqlTx, err = clone.session.Beginx() + *sqlTx, err = clone.Session().Beginx() return } - if err := waitForConnection(func() error { return connFn(&sqlTx) }); err != nil { - return nil, err - } - - clone.tx = sqltx.New(sqlTx) - return &tx{Tx: clone.tx, database: clone}, nil -} - -// Exec compiles and executes a statement that does not return any rows. -func (d *database) Exec(stmt *sqlgen.Statement, args ...interface{}) (sql.Result, error) { - var query string - var p *sqlx.Stmt - var err error - - if db.Debug { - var start, end int64 - start = time.Now().UnixNano() - - defer func() { - end = time.Now().UnixNano() - sqlutil.Log(query, args, err, start, end) - }() - } - - if p, query, err = d.prepareStatement(stmt); err != nil { - return nil, err - } - - return p.Exec(args...) -} - -// Query compiles and executes a statement that returns rows. -func (d *database) Query(stmt *sqlgen.Statement, args ...interface{}) (*sqlx.Rows, error) { - var query string - var p *sqlx.Stmt - var err error - - if db.Debug { - var start, end int64 - start = time.Now().UnixNano() - - defer func() { - end = time.Now().UnixNano() - sqlutil.Log(query, args, err, start, end) - }() - } - - if p, query, err = d.prepareStatement(stmt); err != nil { + if err := d.WaitForConnection(func() error { return connFn(&sqlTx) }); err != nil { return nil, err } - return p.Queryx(args...) -} - -// QueryRow compiles and executes a statement that returns at most one row. -func (d *database) QueryRow(stmt *sqlgen.Statement, args ...interface{}) (*sqlx.Row, error) { - var query string - var p *sqlx.Stmt - var err error - - if db.Debug { - var start, end int64 - start = time.Now().UnixNano() - - defer func() { - end = time.Now().UnixNano() - sqlutil.Log(query, args, err, start, end) - }() - } - - if p, query, err = d.prepareStatement(stmt); err != nil { - return nil, err - } + clone.BindTx(sqlTx) - return p.QueryRowx(args...), nil + return &sqltx.Database{Database: clone, Tx: clone.Tx()}, nil } -// populateSchema looks up for the table info in the database and populates its +// PopulateSchema looks up for the table info in the database and populates its // schema for internal use. -func (d *database) populateSchema() (err error) { +func (d *database) PopulateSchema() (err error) { var collections []string - d.schema = schema.NewDatabaseSchema() + d.NewSchema() - // Get database name. - stmt := &sqlgen.Statement{ - Type: sqlgen.Select, - Columns: sqlgen.JoinColumns( - sqlgen.RawValue(`DATABASE()`), - ), - } + q := d.Builder().Select(db.Raw{"DATABASE() AS name"}) - var row *sqlx.Row + var dbName string - if row, err = d.QueryRow(stmt); err != nil { - return err - } + iter := q.Iterator() + defer iter.Close() - if err = row.Scan(&d.schema.Name); err != nil { - return err + if iter.Next() { + if err := iter.Scan(&dbName); err != nil { + return err + } + } else { + return iter.Err() } - // The Collections() call will populate schema if its nil. + d.Schema().Name = dbName + if collections, err = d.Collections(); err != nil { return err } for i := range collections { - // Populate each collection. if _, err = d.Collection(collections[i]); err != nil { return err } @@ -496,198 +200,101 @@ func (d *database) populateSchema() (err error) { return err } -func (d *database) tableExists(names ...string) error { - var stmt *sqlgen.Statement - var err error - var rows *sqlx.Rows +// TableExists checks whether a table exists and returns an error in case it doesn't. +func (d *database) TableExists(name string) error { + if d.Schema().HasTable(name) { + return nil + } - for i := range names { + q := d.Builder().Select("table_name"). + From("information_schema.tables"). + Where("table_schema = ? AND table_name = ?", d.Schema().Name, name) - if d.schema.HasTable(names[i]) { - // We already know this table exists. - continue - } + iter := q.Iterator() + defer iter.Close() - stmt = &sqlgen.Statement{ - Type: sqlgen.Select, - Table: sqlgen.TableWithName(`information_schema.tables`), - Columns: sqlgen.JoinColumns( - sqlgen.ColumnWithName(`table_name`), - ), - Where: sqlgen.WhereConditions( - &sqlgen.ColumnValue{ - Column: sqlgen.ColumnWithName(`table_schema`), - Operator: `=`, - Value: sqlPlaceholder, - }, - &sqlgen.ColumnValue{ - Column: sqlgen.ColumnWithName(`table_name`), - Operator: `=`, - Value: sqlPlaceholder, - }, - ), - } - - if rows, err = d.Query(stmt, d.schema.Name, names[i]); err != nil { - return db.ErrCollectionDoesNotExist - } - - if !rows.Next() { - rows.Close() - return db.ErrCollectionDoesNotExist + if iter.Next() { + var tableName string + if err := iter.Scan(&tableName); err != nil { + return err } + } else { + return db.ErrCollectionDoesNotExist } return nil } -func (d *database) tableColumns(tableName string) ([]string, error) { - - // Making sure this table is allocated. - tableSchema := d.schema.Table(tableName) - - if len(tableSchema.Columns) > 0 { - return tableSchema.Columns, nil - } - - stmt := &sqlgen.Statement{ - Type: sqlgen.Select, - Table: sqlgen.TableWithName(`information_schema.columns`), - Columns: sqlgen.JoinColumns( - sqlgen.ColumnWithName(`column_name`), - sqlgen.ColumnWithName(`data_type`), - ), - Where: sqlgen.WhereConditions( - &sqlgen.ColumnValue{ - Column: sqlgen.ColumnWithName(`table_schema`), - Operator: `=`, - Value: sqlPlaceholder, - }, - &sqlgen.ColumnValue{ - Column: sqlgen.ColumnWithName(`table_name`), - Operator: `=`, - Value: sqlPlaceholder, - }, - ), - } +// TableColumns returns all columns from the given table. +func (d *database) TableColumns(tableName string) ([]string, error) { + s := d.Schema() - var rows *sqlx.Rows - var err error + if len(s.Table(tableName).Columns) == 0 { - if rows, err = d.Query(stmt, d.schema.Name, tableName); err != nil { - return nil, err - } + q := d.Builder().Select("column_name"). + From("information_schema.columns"). + Where("table_schema = ? AND table_name = ?", d.Schema().Name, tableName) - tableFields := []columnSchemaT{} + var rows []struct { + Name string `db:"column_name"` + } - if err = sqlutil.FetchRows(rows, &tableFields); err != nil { - return nil, err - } + if err := q.Iterator().All(&rows); err != nil { + return nil, err + } - d.schema.TableInfo[tableName].Columns = make([]string, 0, len(tableFields)) + s.TableInfo[tableName].Columns = make([]string, 0, len(rows)) - for i := range tableFields { - d.schema.TableInfo[tableName].Columns = append(d.schema.TableInfo[tableName].Columns, tableFields[i].Name) + for i := range rows { + s.TableInfo[tableName].Columns = append(s.TableInfo[tableName].Columns, rows[i].Name) + } } - return d.schema.TableInfo[tableName].Columns, nil + return s.Table(tableName).Columns, nil } -func (d *database) getPrimaryKey(tableName string) ([]string, error) { - - tableSchema := d.schema.Table(tableName) +// TablePrimaryKey returns all primary keys from the given table. +func (d *database) TablePrimaryKey(tableName string) ([]string, error) { + s := d.Schema() - if len(tableSchema.PrimaryKey) != 0 { - return tableSchema.PrimaryKey, nil - } + ts := s.Table(tableName) - stmt := &sqlgen.Statement{ - Type: sqlgen.Select, - Table: sqlgen.RawValue(` - information_schema.table_constraints AS t - JOIN information_schema.key_column_usage k - USING(constraint_name, table_schema, table_name) - `), - Columns: sqlgen.JoinColumns( - sqlgen.ColumnWithName(`k.column_name`), - ), - Where: sqlgen.WhereConditions( - &sqlgen.ColumnValue{ - Column: sqlgen.ColumnWithName(`t.constraint_type`), - Operator: `=`, - Value: sqlgen.NewValue(`primary key`), - }, - &sqlgen.ColumnValue{ - Column: sqlgen.ColumnWithName(`t.table_schema`), - Operator: `=`, - Value: sqlPlaceholder, - }, - &sqlgen.ColumnValue{ - Column: sqlgen.ColumnWithName(`t.table_name`), - Operator: `=`, - Value: sqlPlaceholder, - }, - ), - OrderBy: &sqlgen.OrderBy{ - SortColumns: sqlgen.JoinSortColumns( - &sqlgen.SortColumn{ - Column: sqlgen.ColumnWithName(`k.ordinal_position`), - Order: sqlgen.Ascendent, - }, - ), - }, + if len(ts.PrimaryKey) != 0 { + return ts.PrimaryKey, nil } - var rows *sqlx.Rows - var err error + ts.PrimaryKey = make([]string, 0, 1) - if rows, err = d.Query(stmt, d.schema.Name, tableName); err != nil { - return nil, err - } + q := d.Builder().Select("k.column_name"). + From("information_schema.table_constraints AS t"). + Join("information_schema.key_column_usage AS k"). + Using("constraint_name", "table_schema", "table_name"). + Where(` + t.constraint_type = 'primary key' + AND t.table_schema = ? + AND t.table_name = ? + `, d.Schema().Name, tableName). + OrderBy("k.ordinal_position") - tableSchema.PrimaryKey = make([]string, 0, 1) + iter := q.Iterator() + defer iter.Close() - for rows.Next() { - var key string - if err = rows.Scan(&key); err != nil { + for iter.Next() { + var pKey string + if err := iter.Scan(&pKey); err != nil { return nil, err } - tableSchema.PrimaryKey = append(tableSchema.PrimaryKey, key) + ts.PrimaryKey = append(ts.PrimaryKey, pKey) } - return tableSchema.PrimaryKey, nil + return ts.PrimaryKey, nil } -// waitForConnection tries to execute the connectFn function, if connectFn -// returns an error, then waitForConnection will keep trying until connectFn -// returns nil. Maximum waiting time is 5s after having acquired the lock. -func waitForConnection(connectFn func() error) error { - // This lock ensures first-come, first-served and prevents opening too many - // file descriptors. - waitForConnMu.Lock() - defer waitForConnMu.Unlock() - - // Minimum waiting time. - waitTime := time.Millisecond * 10 - - // Waitig 5 seconds for a successful connection. - for timeStart := time.Now(); time.Now().Sub(timeStart) < time.Second*5; { - if err := connectFn(); err != nil { - if strings.Contains(err.Error(), `many connections`) { - // Sleep and try again if, and only if, the server replied with a "too - // many clients" error. - time.Sleep(waitTime) - if waitTime < time.Millisecond*500 { - // Wait a bit more next time. - waitTime = waitTime * 2 - } - continue - } - // Return any other error immediately. - return err - } - return nil +func (d *database) clone() (*database, error) { + clone := &database{} + clone.BaseDatabase = d.BaseDatabase.Clone(clone) + if err := clone.Open(); err != nil { + return nil, err } - - return db.ErrGivingUpTryingToConnect + return clone, nil } diff --git a/mysql/database_test.go b/mysql/database_test.go index 87f1325e95f23390e18fd03271946721a530d760..7877b3b7ae46322452d42081673dfda49c09ac2c 100644 --- a/mysql/database_test.go +++ b/mysql/database_test.go @@ -36,7 +36,6 @@ import ( "github.com/jmoiron/sqlx" "upper.io/db" - "upper.io/db/internal/sqlutil" ) const ( @@ -156,7 +155,7 @@ func init() { } // Attempts to open an empty datasource. -func TestOpenFailed(t *testing.T) { +func SkipTestOpenFailed(t *testing.T) { var err error // Attempt to open an empty database. @@ -167,7 +166,7 @@ func TestOpenFailed(t *testing.T) { } // Attempts to open an empty datasource. -func TestOpenWithWrongData(t *testing.T) { +func SkipTestOpenWithWrongData(t *testing.T) { var err error var rightSettings, wrongSettings db.Settings @@ -222,27 +221,6 @@ func TestOpenWithWrongData(t *testing.T) { } } -// Old settings must be compatible. -func TestOldSettings(t *testing.T) { - var err error - var sess db.Database - - oldSettings := db.Settings{ - Database: databaseName, - User: username, - Password: password, - Host: host, - } - - // Opening database. - if sess, err = db.Open(Adapter, oldSettings); err != nil { - t.Fatal(err) - } - - // Closing database. - sess.Close() -} - // Test USE func TestUse(t *testing.T) { var err error @@ -924,231 +902,6 @@ func TestRemove(t *testing.T) { } } -// Attempts to use SQL raw statements. -func TestRawRelations(t *testing.T) { - var sess db.Database - var err error - - var artist db.Collection - var publication db.Collection - var review db.Collection - - type artistType struct { - ID int64 `db:"id,omitempty"` - Name string `db:"name"` - } - - type publicationType struct { - ID int64 `db:"id,omitempty"` - Title string `db:"title"` - AuthorID int64 `db:"author_id"` - } - - type reviewType struct { - ID int64 `db:"id,omitempty"` - PublicationID int64 `db:"publication_id"` - Name string `db:"name"` - Comments string `db:"comments"` - Created time.Time `db:"created"` - } - - if sess, err = db.Open(Adapter, settings); err != nil { - t.Fatal(err) - } - - defer sess.Close() - - // Artist collection. - if artist, err = sess.Collection("artist"); err != nil { - t.Fatal(err) - } - - if err = artist.Truncate(); err != nil { - t.Fatal(err) - } - - // Publication collection. - if publication, err = sess.Collection("publication"); err != nil { - t.Fatal(err) - } - - if err = publication.Truncate(); err != nil { - t.Fatal(err) - } - - // Review collection. - if review, err = sess.Collection("review"); err != nil { - t.Fatal(err) - } - - if err = review.Truncate(); err != nil { - t.Fatal(err) - } - - // Adding some artists. - var miyazakiID interface{} - miyazaki := artistType{Name: `Hayao Miyazaki`} - if miyazakiID, err = artist.Append(miyazaki); err != nil { - t.Fatal(err) - } - miyazaki.ID = miyazakiID.(int64) - - var asimovID interface{} - asimov := artistType{Name: `Isaac Asimov`} - if asimovID, err = artist.Append(asimov); err != nil { - t.Fatal(err) - } - - var marquezID interface{} - marquez := artistType{Name: `Gabriel GarcÃa Márquez`} - if marquezID, err = artist.Append(marquez); err != nil { - t.Fatal(err) - } - - // Adding some publications. - publication.Append(publicationType{ - Title: `Tonari no Totoro`, - AuthorID: miyazakiID.(int64), - }) - - publication.Append(publicationType{ - Title: `Howl's Moving Castle`, - AuthorID: miyazakiID.(int64), - }) - - publication.Append(publicationType{ - Title: `Ponyo`, - AuthorID: miyazakiID.(int64), - }) - - publication.Append(publicationType{ - Title: `Memoria de mis Putas Tristes`, - AuthorID: marquezID.(int64), - }) - - publication.Append(publicationType{ - Title: `El Coronel no tiene quien le escriba`, - AuthorID: marquezID.(int64), - }) - - publication.Append(publicationType{ - Title: `El Amor en los tiempos del Cólera`, - AuthorID: marquezID.(int64), - }) - - publication.Append(publicationType{ - Title: `I, Robot`, - AuthorID: asimovID.(int64), - }) - - var foundationID interface{} - foundationID, err = publication.Append(publicationType{ - Title: `Foundation`, - AuthorID: asimovID.(int64), - }) - if err != nil { - t.Fatal(err) - } - - publication.Append(publicationType{ - Title: `The Robots of Dawn`, - AuthorID: asimovID.(int64), - }) - - // Adding reviews for foundation. - review.Append(reviewType{ - PublicationID: foundationID.(int64), - Name: "John Doe", - Comments: "I love The Foundation series.", - Created: time.Now(), - }) - - review.Append(reviewType{ - PublicationID: foundationID.(int64), - Name: "Edr Pls", - Comments: "The Foundation series made me fall in love with Isaac Asimov.", - Created: time.Now(), - }) - - // Exec'ing a raw query. - var artistPublication db.Collection - if artistPublication, err = sess.Collection(`artist AS a`, `publication AS p`); err != nil { - t.Fatal(err) - } - - res := artistPublication.Find( - db.Raw{`a.id = p.author_id`}, - ).Select( - "p.id", - "p.title as publication_title", - "a.name AS artist_name", - ) - - type artistPublicationType struct { - ID int64 `db:"id"` - PublicationTitle string `db:"publication_title"` - ArtistName string `db:"artist_name"` - } - - all := []artistPublicationType{} - - if err = res.All(&all); err != nil { - t.Fatal(err) - } - - if len(all) != 9 { - t.Fatalf("Expecting some rows.") - } - -} - -func TestRawQuery(t *testing.T) { - var sess db.Database - var rows *sqlx.Rows - var err error - var drv *sqlx.DB - - type publicationType struct { - ID int64 `db:"id,omitempty"` - Title string `db:"title"` - AuthorID int64 `db:"author_id"` - } - - if sess, err = db.Open(Adapter, settings); err != nil { - t.Fatal(err) - } - - defer sess.Close() - - drv = sess.Driver().(*sqlx.DB) - - rows, err = drv.Queryx(` - SELECT - p.id, - p.title AS publication_title, - a.name AS artist_name - FROM - artist AS a, - publication AS p - WHERE - a.id = p.author_id - `) - - if err != nil { - t.Fatal(err) - } - - var all []publicationType - - if err = sqlutil.FetchRows(rows, &all); err != nil { - t.Fatal(err) - } - - if len(all) != 9 { - t.Fatalf("Expecting some rows.") - } -} - // Attempts to test database transactions. func TestTransactionsAndRollback(t *testing.T) { var sess db.Database diff --git a/mysql/mysql.go b/mysql/mysql.go index d46c200c77995b1b5fafaaea42200943e47e46af..2a247fd407119ac1f8b7b9c9d124aa6310278c0c 100644 --- a/mysql/mysql.go +++ b/mysql/mysql.go @@ -22,50 +22,12 @@ package mysql // import "upper.io/db/mysql" import ( - "upper.io/cache" "upper.io/db" - "upper.io/builder/sqlgen" - "upper.io/db/internal/sqlutil" ) // Adapter is the public name of the adapter. const Adapter = `mysql` -var template *sqlutil.TemplateWithUtils - func init() { - template = sqlutil.NewTemplateWithUtils(&sqlgen.Template{ - ColumnSeparator: adapterColumnSeparator, - IdentifierSeparator: adapterIdentifierSeparator, - IdentifierQuote: adapterIdentifierQuote, - ValueSeparator: adapterValueSeparator, - ValueQuote: adapterValueQuote, - AndKeyword: adapterAndKeyword, - OrKeyword: adapterOrKeyword, - NotKeyword: adapterNotKeyword, - DescKeyword: adapterDescKeyword, - AscKeyword: adapterAscKeyword, - DefaultOperator: adapterDefaultOperator, - AssignmentOperator: adapterAssignmentOperator, - ClauseGroup: adapterClauseGroup, - ClauseOperator: adapterClauseOperator, - ColumnValue: adapterColumnValue, - TableAliasLayout: adapterTableAliasLayout, - ColumnAliasLayout: adapterColumnAliasLayout, - SortByColumnLayout: adapterSortByColumnLayout, - WhereLayout: adapterWhereLayout, - OrderByLayout: adapterOrderByLayout, - InsertLayout: adapterInsertLayout, - SelectLayout: adapterSelectLayout, - UpdateLayout: adapterUpdateLayout, - DeleteLayout: adapterDeleteLayout, - TruncateLayout: adapterTruncateLayout, - DropDatabaseLayout: adapterDropDatabaseLayout, - DropTableLayout: adapterDropTableLayout, - CountLayout: adapterSelectCountLayout, - GroupByLayout: adapterGroupByLayout, - Cache: cache.NewCache(), - }) - db.Register(Adapter, &database{}) } diff --git a/mysql/template.go b/mysql/template.go deleted file mode 100644 index 20db212c8672c723b968ee549b8c65c9cf5daeba..0000000000000000000000000000000000000000 --- a/mysql/template.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2012-2015 The upper.io/db authors. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -package mysql - -const ( - adapterColumnSeparator = `.` - adapterIdentifierSeparator = `, ` - adapterIdentifierQuote = "`{{.Value}}`" - adapterValueSeparator = `, ` - adapterValueQuote = `'{{.}}'` - adapterAndKeyword = `AND` - adapterOrKeyword = `OR` - adapterNotKeyword = `NOT` - adapterDescKeyword = `DESC` - adapterAscKeyword = `ASC` - adapterDefaultOperator = `=` - adapterAssignmentOperator = `=` - adapterClauseGroup = `({{.}})` - adapterClauseOperator = ` {{.}} ` - adapterColumnValue = `{{.Column}} {{.Operator}} {{.Value}}` - adapterTableAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` - adapterColumnAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` - adapterSortByColumnLayout = `{{.Column}} {{.Order}}` - - adapterOrderByLayout = ` - {{if .SortColumns}} - ORDER BY {{.SortColumns}} - {{end}} - ` - - adapterWhereLayout = ` - {{if .Conds}} - WHERE {{.Conds}} - {{end}} - ` - - adapterSelectLayout = ` - SELECT - - {{if .Columns}} - {{.Columns}} - {{else}} - * - {{end}} - - {{if .Table}} - FROM {{.Table}} - {{end}} - - {{.Where}} - - {{.GroupBy}} - - {{.OrderBy}} - - {{if .Limit}} - LIMIT {{.Limit}} - {{end}} - - {{if .Offset}} - OFFSET {{.Offset}} - {{end}} - ` - adapterDeleteLayout = ` - DELETE - FROM {{.Table}} - {{.Where}} - ` - adapterUpdateLayout = ` - UPDATE - {{.Table}} - SET {{.ColumnValues}} - {{ .Where }} - ` - - adapterSelectCountLayout = ` - SELECT - COUNT(1) AS _t - FROM {{.Table}} - {{.Where}} - - {{if .Limit}} - LIMIT {{.Limit}} - {{end}} - - {{if .Offset}} - OFFSET {{.Offset}} - {{end}} - ` - - adapterInsertLayout = ` - INSERT INTO {{.Table}} - ({{.Columns}}) - VALUES - ({{.Values}}) - {{.Extra}} - ` - - adapterTruncateLayout = ` - TRUNCATE TABLE {{.Table}} - ` - - adapterDropDatabaseLayout = ` - DROP DATABASE {{.Database}} - ` - - adapterDropTableLayout = ` - DROP TABLE {{.Table}} - ` - - adapterGroupByLayout = ` - {{if .GroupColumns}} - GROUP BY {{.GroupColumns}} - {{end}} - ` -) diff --git a/mysql/tx.go b/mysql/tx.go deleted file mode 100644 index 21b6ecf866557ec95b251262ca0694904a7156fa..0000000000000000000000000000000000000000 --- a/mysql/tx.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2012-2015 The upper.io/db authors. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -package mysql - -import ( - "upper.io/db/internal/sqlutil/tx" -) - -type tx struct { - *sqltx.Tx - *database -} - -// Driver returns the current transaction session. -func (t *tx) Driver() interface{} { - if t != nil && t.Tx != nil { - return t.Tx.Tx - } - return nil -} - -// Commit commits the current transaction. -func (t *tx) Commit() error { - if err := t.Tx.Commit(); err != nil { - return err - } - return nil -} - -// Rollback discards the current transaction. -func (t *tx) Rollback() error { - if err := t.Tx.Rollback(); err != nil { - return err - } - return nil -}