From ad0d1583c554aeee3ee3f454474e17c39f36ab49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= <jose.carlos@menteslibres.net> Date: Thu, 28 May 2015 06:53:23 -0500 Subject: [PATCH] Adapting sqlite adapter to newer sqlgen --- sqlite/collection.go | 245 ++--------- sqlite/database.go | 646 ++++++++++++++---------------- sqlite/database_test.go | 10 +- sqlite/result.go | 308 -------------- sqlite/sqlite.go | 69 ++++ sqlite/{layout.go => template.go} | 64 ++- sqlite/tx.go | 44 -- 7 files changed, 445 insertions(+), 941 deletions(-) delete mode 100644 sqlite/result.go create mode 100644 sqlite/sqlite.go rename sqlite/{layout.go => template.go} (66%) delete mode 100644 sqlite/tx.go diff --git a/sqlite/collection.go b/sqlite/collection.go index c65e64d4..199a56af 100644 --- a/sqlite/collection.go +++ b/sqlite/collection.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam +// Copyright (c) 2012-2015 José Carlos Nieto, https://menteslibres.net/xiam // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the @@ -22,8 +22,6 @@ package sqlite import ( - "fmt" - "reflect" "strings" "database/sql" @@ -31,228 +29,52 @@ import ( "upper.io/db" "upper.io/db/util/sqlgen" "upper.io/db/util/sqlutil" + "upper.io/db/util/sqlutil/result" ) -const defaultOperator = `=` - type table struct { sqlutil.T - source *source - names []string -} - -func whereValues(term interface{}) (where sqlgen.Where, args []interface{}) { - - args = []interface{}{} - - switch t := term.(type) { - case []interface{}: - l := len(t) - where = make(sqlgen.Where, 0, l) - for _, cond := range t { - w, v := whereValues(cond) - args = append(args, v...) - where = append(where, w...) - } - case db.And: - and := make(sqlgen.And, 0, len(t)) - for _, cond := range t { - k, v := whereValues(cond) - args = append(args, v...) - and = append(and, k...) - } - where = append(where, and) - case db.Or: - or := make(sqlgen.Or, 0, len(t)) - for _, cond := range t { - k, v := whereValues(cond) - args = append(args, v...) - or = append(or, k...) - } - where = append(where, or) - case db.Raw: - if s, ok := t.Value.(string); ok == true { - where = append(where, sqlgen.Raw{s}) - } - case db.Cond: - k, v := conditionValues(t) - args = append(args, v...) - for _, kk := range k { - where = append(where, kk) - } - case db.Constrainer: - k, v := conditionValues(t.Constraint()) - args = append(args, v...) - for _, kk := range k { - where = append(where, kk) - } - default: - panic(fmt.Sprintf(db.ErrUnknownConditionType.Error(), reflect.TypeOf(t))) - } - - return where, args -} - -func interfaceArgs(value interface{}) (args []interface{}) { - if value == nil { - return nil - } - - v := reflect.ValueOf(value) - - switch v.Type().Kind() { - case reflect.Slice: - var i, total int - - total = v.Len() - if total > 0 { - args = make([]interface{}, total) - - for i = 0; i < total; i++ { - args[i] = v.Index(i).Interface() - } - - return args - } - return nil - default: - args = []interface{}{value} - } - - return args -} - -func conditionValues(cond db.Cond) (columnValues sqlgen.ColumnValues, args []interface{}) { - - args = []interface{}{} - - for column, value := range cond { - var columnValue sqlgen.ColumnValue - - // Guessing operator from input, or using a default one. - column := strings.TrimSpace(column) - chunks := strings.SplitN(column, ` `, 2) - - columnValue.Column = sqlgen.Column{chunks[0]} - - if len(chunks) > 1 { - columnValue.Operator = chunks[1] - } else { - columnValue.Operator = defaultOperator - } - - switch value := value.(type) { - case db.Func: - // Catches functions. - v := interfaceArgs(value.Args) - columnValue.Operator = value.Name - - if v == nil { - // A function with no arguments. - columnValue.Value = sqlgen.Value{sqlgen.Raw{`()`}} - } else { - // A function with one or more arguments. - columnValue.Value = sqlgen.Value{sqlgen.Raw{fmt.Sprintf(`(?%s)`, strings.Repeat(`, ?`, len(v)-1))}} - } - - args = append(args, v...) - default: - // Catches everything else. - v := interfaceArgs(value) - l := len(v) - if v == nil || l == 0 { - // Nil value given. - columnValue.Value = sqlgen.Value{sqlgen.Raw{`NULL`}} - } else { - if l > 1 { - // Array value given. - columnValue.Value = sqlgen.Value{sqlgen.Raw{fmt.Sprintf(`(?%s)`, strings.Repeat(`, ?`, len(v)-1))}} - } else { - // Single value given. - columnValue.Value = sqlPlaceholder - } - args = append(args, v...) - } - } - - columnValues = append(columnValues, columnValue) - } - - return columnValues, args + *database } -func (c *table) Find(terms ...interface{}) db.Result { - where, arguments := whereValues(terms) - - result := &result{ - table: c, - where: where, - arguments: arguments, - } +var _ = db.Collection(&table{}) - return result -} - -func (c *table) tableN(i int) string { - if len(c.names) > i { - chunks := strings.SplitN(c.names[i], " ", 2) - if len(chunks) > 0 { - return chunks[0] - } - } - return "" +// Find creates a result set with the given conditions. +func (t *table) Find(terms ...interface{}) db.Result { + where, arguments := sqlutil.ToWhereWithArguments(terms) + return result.NewResult(t, where, arguments) } -// Deletes all the rows within the collection. -func (c *table) Truncate() error { - - _, err := c.source.doExec(sqlgen.Statement{ - Type: sqlgen.SqlTruncate, - Table: sqlgen.Table{c.tableN(0)}, +// Truncate deletes all rows from the table. +func (t *table) Truncate() error { + _, err := t.database.Exec(sqlgen.Statement{ + Type: sqlgen.Truncate, + Table: sqlgen.TableWithName(t.MainTableName()), }) if err != nil { return err } - return nil } -// Appends an item (map or struct) into the collection. -func (c *table) Append(item interface{}) (interface{}, error) { - +// Append inserts an item (map or struct) into the collection. +func (t *table) Append(item interface{}) (interface{}, error) { var pKey []string - var columns sqlgen.Columns - var values sqlgen.Values - var arguments []interface{} - cols, vals, err := c.FieldValues(item) + columnNames, columnValues, err := t.FieldValues(item) - // Error ocurred, stop appending. if err != nil { return nil, err } - columns = make(sqlgen.Columns, 0, len(cols)) - for i := range cols { - columns = append(columns, sqlgen.Column{cols[i]}) - } + sqlgenCols, sqlgenVals, sqlgenArgs, err := sqlutil.ToColumnsValuesAndArguments(columnNames, columnValues) - arguments = make([]interface{}, 0, len(vals)) - values = make(sqlgen.Values, 0, len(vals)) - for i := range vals { - switch v := vals[i].(type) { - case sqlgen.Value: - // Adding value. - values = append(values, v) - default: - // Adding both value and placeholder. - values = append(values, sqlPlaceholder) - arguments = append(arguments, v) - } + if err != nil { + return nil, err } - if pKey, err = c.source.getPrimaryKey(c.tableN(0)); err != nil { + if pKey, err = t.database.getPrimaryKey(t.MainTableName()); err != nil { if err != sql.ErrNoRows { // Can't tell primary key. return nil, err @@ -260,14 +82,14 @@ func (c *table) Append(item interface{}) (interface{}, error) { } stmt := sqlgen.Statement{ - Type: sqlgen.SqlInsert, - Table: sqlgen.Table{c.tableN(0)}, - Columns: columns, - Values: values, + Type: sqlgen.Insert, + Table: sqlgen.TableWithName(t.MainTableName()), + Columns: sqlgenCols, + Values: sqlgenVals, } var res sql.Result - if res, err = c.source.doExec(stmt, arguments...); err != nil { + if res, err = t.database.Exec(stmt, sqlgenArgs...); err != nil { return nil, err } @@ -294,10 +116,10 @@ func (c *table) Append(item interface{}) (interface{}, error) { // were given for constructing the composite key. keyMap := make(map[string]interface{}) - for i := range cols { + for i := range columnNames { for j := 0; j < len(pKey); j++ { - if pKey[j] == cols[i] { - keyMap[pKey[j]] = vals[i] + if pKey[j] == columnNames[i] { + keyMap[pKey[j]] = columnValues[i] } } } @@ -320,14 +142,15 @@ func (c *table) Append(item interface{}) (interface{}, error) { return keyMap, nil } -// Returns true if the collection exists. -func (c *table) Exists() bool { - if err := c.source.tableExists(c.names...); err != 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 } -func (c *table) Name() string { - return strings.Join(c.names, `, `) +// Name returns the name of the table or tables that form the collection. +func (t *table) Name() string { + return strings.Join(t.Tables, `, `) } diff --git a/sqlite/database.go b/sqlite/database.go index d0998b15..d1d575ff 100644 --- a/sqlite/database.go +++ b/sqlite/database.go @@ -24,35 +24,26 @@ package sqlite import ( "database/sql" "fmt" - "os" "strings" "time" - // Importing SQLite3 driver. "github.com/jmoiron/sqlx" - _ "github.com/mattn/go-sqlite3" - "upper.io/cache" + _ "github.com/mattn/go-sqlite3" // SQLite3 driver. "upper.io/db" "upper.io/db/util/schema" "upper.io/db/util/sqlgen" "upper.io/db/util/sqlutil" + "upper.io/db/util/sqlutil/tx" ) -const ( - // Adapter is the public name of the adapter. - Adapter = `sqlite` -) - -var template *sqlgen.Template - var ( - sqlPlaceholder = sqlgen.Value{sqlgen.Raw{`?`}} + sqlPlaceholder = sqlgen.RawValue(`?`) ) -type source struct { +type database struct { connURL db.ConnectionURL session *sqlx.DB - tx *tx + tx *sqltx.Tx schema *schema.DatabaseSchema // columns property was introduced so we could query PRAGMA data only once // and retrieve all the column information we'd need, such as name and if it @@ -60,399 +51,391 @@ type source struct { columns map[string][]columnSchemaT } -type columnSchemaT struct { - Name string `db:"name"` - PK int `db:"pk"` +type tx struct { + *sqltx.Tx + *database } -func debugEnabled() bool { - if os.Getenv(db.EnvEnableDebug) != "" { - return true - } - return false -} +var ( + _ = db.Database(&database{}) + _ = db.Tx(&tx{}) +) -func debugLog(query string, args []interface{}, err error, start int64, end int64) { - if debugEnabled() == true { - d := sqlutil.Debug{query, args, err, start, end} - d.Print() - } +type columnSchemaT struct { + Name string `db:"name"` + PK int `db:"pk"` } -func init() { - - template = &sqlgen.Template{ - sqlColumnSeparator, - sqlIdentifierSeparator, - sqlIdentifierQuote, - sqlValueSeparator, - sqlValueQuote, - sqlAndKeyword, - sqlOrKeyword, - sqlNotKeyword, - sqlDescKeyword, - sqlAscKeyword, - sqlDefaultOperator, - sqlClauseGroup, - sqlClauseOperator, - sqlColumnValue, - sqlTableAliasLayout, - sqlColumnAliasLayout, - sqlSortByColumnLayout, - sqlWhereLayout, - sqlOrderByLayout, - sqlInsertLayout, - sqlSelectLayout, - sqlUpdateLayout, - sqlDeleteLayout, - sqlTruncateLayout, - sqlDropDatabaseLayout, - sqlDropTableLayout, - sqlSelectCountLayout, - sqlGroupByLayout, - cache.NewCache(), - } - - db.Register(Adapter, &source{}) +// Driver returns the underlying *sqlx.DB instance. +func (d *database) Driver() interface{} { + return d.session } -func (s *source) populateSchema() (err error) { - var collections []string +// Open attempts to connect to the database server using already stored settings. +func (d *database) Open() error { + var err error - s.schema = schema.NewDatabaseSchema() + // 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{ + Database: settings.Database, + Options: map[string]string{ + "cache": "shared", + }, + } - var conn ConnectionURL + d.connURL = conn + } - if conn, err = ParseURL(s.connURL.String()); err != nil { + if d.session, err = sqlx.Open(`sqlite3`, d.connURL.String()); err != nil { return err } - s.schema.Name = conn.Database + d.session.Mapper = sqlutil.NewMapper() - // The Collections() call will populate schema if its nil. - if collections, err = s.Collections(); err != nil { + if err = d.populateSchema(); err != nil { return err } - for i := range collections { - // Populate each collection. - if _, err = s.Collection(collections[i]); err != nil { - return err - } - } + return nil +} - return err +// Clone returns a cloned db.Database session, this is typically used for +// transactions. +func (d *database) Clone() (db.Database, error) { + return d.clone() } -func (s *source) doExec(stmt sqlgen.Statement, args ...interface{}) (sql.Result, error) { - var query string - var res sql.Result - var err error - var start, end int64 +func (d *database) clone() (*database, error) { + src := &database{} + src.Setup(d.connURL) - start = time.Now().UnixNano() + if err := src.Open(); err != nil { + return nil, err + } - defer func() { - end = time.Now().UnixNano() - debugLog(query, args, err, start, end) - }() + return src, nil +} - if s.session == nil { - return nil, db.ErrNotConnected +// 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 { + return d.session.Close() } + return nil +} - query = stmt.Compile(template) +// Collection returns a table by name. +func (d *database) Collection(names ...string) (db.Collection, error) { + var err error - if s.tx != nil { - res, err = s.tx.sqlTx.Exec(query, args...) - } else { - res, err = s.session.Exec(query, args...) + if len(names) == 0 { + return nil, db.ErrMissingCollectionName } - return res, err -} + if d.tx != nil { + if d.tx.Done() { + return nil, sql.ErrTxDone + } + } -func (s *source) doQuery(stmt sqlgen.Statement, args ...interface{}) (*sqlx.Rows, error) { - var rows *sqlx.Rows - var query string - var err error - var start, end int64 + col := &table{database: d} + col.Tables = names - start = time.Now().UnixNano() + for _, name := range names { + chunks := strings.SplitN(name, ` `, 2) - defer func() { - end = time.Now().UnixNano() - debugLog(query, args, err, start, end) - }() + if len(chunks) == 0 { + return nil, db.ErrMissingCollectionName + } - if s.session == nil { - return nil, db.ErrNotConnected - } + tableName := chunks[0] - query = stmt.Compile(template) + if err := d.tableExists(tableName); err != nil { + return nil, err + } - if s.tx != nil { - rows, err = s.tx.sqlTx.Queryx(query, args...) - } else { - rows, err = s.session.Queryx(query, args...) + if col.Columns, err = d.tableColumns(tableName); err != nil { + return nil, err + } } - return rows, err + return col, nil } -func (s *source) doQueryRow(stmt sqlgen.Statement, args ...interface{}) (*sqlx.Row, error) { - var query string - var row *sqlx.Row - var err error - var start, end int64 +// Collections returns a list of non-system tables from the database. +func (d *database) Collections() (collections []string, err error) { - start = time.Now().UnixNano() + tablesInSchema := len(d.schema.Tables) - defer func() { - end = time.Now().UnixNano() - debugLog(query, args, err, start, end) - }() - - if s.session == nil { - return nil, db.ErrNotConnected + // Id.schema already populated? + if tablesInSchema > 0 { + // Pulling table names from schema. + return d.schema.Tables, nil } - query = stmt.Compile(template) + // Schema is empty. - if s.tx != nil { - row = s.tx.sqlTx.QueryRowx(query, args...) - } else { - row = s.session.QueryRowx(query, args...) + // Querying table names. + stmt := sqlgen.Statement{ + Type: sqlgen.Select, + Columns: sqlgen.JoinColumns( + sqlgen.ColumnWithName(`tbl_name`), + ), + Table: sqlgen.TableWithName(`sqlite_master`), + Where: sqlgen.WhereConditions( + &sqlgen.ColumnValue{ + Column: sqlgen.ColumnWithName(`type`), + Operator: `=`, + Value: sqlgen.NewValue(`table`), + }, + ), } - return row, err -} - -func (s *source) doRawQuery(query string, args ...interface{}) (*sqlx.Rows, error) { + // Executing statement. var rows *sqlx.Rows - var err error - var start, end int64 + if rows, err = d.Query(stmt); err != nil { + return nil, err + } - start = time.Now().UnixNano() + defer rows.Close() - defer func() { - end = time.Now().UnixNano() - debugLog(query, args, err, start, end) - }() + collections = []string{} - if s.session == nil { - return nil, db.ErrNotConnected - } + var name string - if s.tx != nil { - rows, err = s.tx.sqlTx.Queryx(query, args...) - } else { - rows, err = s.session.Queryx(query, args...) + for rows.Next() { + // Getting table name. + if err = rows.Scan(&name); err != nil { + return nil, err + } + + // Adding table entry to schema. + d.schema.AddTable(name) + + // Adding table to collections array. + collections = append(collections, name) } - return rows, err + return collections, nil } -// Returns the string name of the database. -func (s *source) Name() string { - return s.schema.Name -} +// Use changes the active database. +func (d *database) Use(database string) (err error) { + var conn ConnectionURL + + if conn, err = ParseURL(d.connURL.String()); err != nil { + return err + } + + conn.Database = database + + d.connURL = conn -// Ping verifies a connection to the database is still alive, -// establishing a connection if necessary. -func (s *source) Ping() error { - return s.session.Ping() + return d.Open() } -func (s *source) clone() (*source, error) { - src := &source{} - src.Setup(s.connURL) +// Drop removes all tables from the current database. +func (d *database) Drop() error { - if err := src.Open(); err != nil { - return nil, err - } + _, err := d.Query(sqlgen.Statement{ + Type: sqlgen.DropDatabase, + Database: sqlgen.DatabaseWithName(d.schema.Name), + }) - return src, nil + return err +} + +// Setup stores database settings. +func (d *database) Setup(connURL db.ConnectionURL) error { + d.connURL = connURL + return d.Open() } -func (s *source) Clone() (db.Database, error) { - return s.clone() +// Name returns the name of the database. +func (d *database) Name() string { + return d.schema.Name } -func (s *source) Transaction() (db.Tx, error) { +// Transaction starts a transaction block and returns a db.Tx struct that can +// be used to issue transactional queries. +func (d *database) Transaction() (db.Tx, error) { var err error - var clone *source + var clone *database var sqlTx *sqlx.Tx - if sqlTx, err = s.session.Beginx(); err != nil { + if sqlTx, err = d.session.Beginx(); err != nil { return nil, err } - if clone, err = s.clone(); err != nil { + if clone, err = d.clone(); err != nil { return nil, err } - tx := &tx{source: clone, sqlTx: sqlTx} - - clone.tx = tx - - return tx, nil -} - -// Stores database settings. -func (s *source) Setup(conn db.ConnectionURL) error { - s.connURL = conn - return s.Open() -} + clone.tx = sqltx.New(sqlTx) -// Returns the underlying *sqlx.DB instance. -func (s *source) Driver() interface{} { - return s.session + return tx{Tx: clone.tx, database: clone}, nil } -// Attempts to connect to a database using the stored settings. -func (s *source) Open() error { +// 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 res sql.Result var err error + var start, end int64 - // Before db.ConnectionURL we used a unified db.Settings struct. This - // condition checks for that type and provides backwards compatibility. - if settings, ok := s.connURL.(db.Settings); ok { - // User is providing a db.Settings struct, let's translate it into a - // ConnectionURL{}. - conn := ConnectionURL{ - Database: settings.Database, - Options: map[string]string{ - "cache": "shared", - }, - } + start = time.Now().UnixNano() - s.connURL = conn - } + defer func() { + end = time.Now().UnixNano() + sqlutil.Log(query, args, err, start, end) + }() - if s.session, err = sqlx.Open(`sqlite3`, s.connURL.String()); err != nil { - return err + if d.session == nil { + return nil, db.ErrNotConnected } - s.session.Mapper = sqlutil.NewMapper() + query = stmt.Compile(template) - if err = s.populateSchema(); err != nil { - return err + if d.tx != nil { + res, err = d.tx.Exec(query, args...) + } else { + res, err = d.session.Exec(query, args...) } - return nil + return res, err } -// Closes the current database session. -func (s *source) Close() error { - if s.session != nil { - return s.session.Close() - } - return nil -} +// Query compiles and executes a statement that returns rows. +func (d *database) Query(stmt sqlgen.Statement, args ...interface{}) (*sqlx.Rows, error) { + var rows *sqlx.Rows + var query string + var err error + var start, end int64 -// Changes the active database. -func (s *source) Use(database string) (err error) { - var conn ConnectionURL + start = time.Now().UnixNano() - if conn, err = ParseURL(s.connURL.String()); err != nil { - return err + defer func() { + end = time.Now().UnixNano() + sqlutil.Log(query, args, err, start, end) + }() + + if d.session == nil { + return nil, db.ErrNotConnected } - conn.Database = database + query = stmt.Compile(template) - s.connURL = conn + if d.tx != nil { + rows, err = d.tx.Queryx(query, args...) + } else { + rows, err = d.session.Queryx(query, args...) + } - return s.Open() + return rows, err } -// Drops the currently active database. -func (s *source) Drop() error { - return db.ErrUnsupported -} +// 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 row *sqlx.Row + var err error + var start, end int64 -// Collections() Returns a list of non-system tables/collections contained -// within the currently active database. -func (s *source) Collections() (collections []string, err error) { + start = time.Now().UnixNano() - tablesInSchema := len(s.schema.Tables) + defer func() { + end = time.Now().UnixNano() + sqlutil.Log(query, args, err, start, end) + }() - // Is schema already populated? - if tablesInSchema > 0 { - // Pulling table names from schema. - return s.schema.Tables, nil + if d.session == nil { + return nil, db.ErrNotConnected } - // Schema is empty. + query = stmt.Compile(template) - // Querying table names. - stmt := sqlgen.Statement{ - Type: sqlgen.SqlSelect, - Columns: sqlgen.Columns{ - {`tbl_name`}, - }, - Table: sqlgen.Table{`sqlite_master`}, - Where: sqlgen.Where{ - sqlgen.ColumnValue{ - sqlgen.Column{`type`}, - `=`, - sqlgen.Value{`table`}, - }, - }, + if d.tx != nil { + row = d.tx.QueryRowx(query, args...) + } else { + row = d.session.QueryRowx(query, args...) } - // Executing statement. - var rows *sqlx.Rows - if rows, err = s.doQuery(stmt); err != nil { - return nil, err - } + return row, err +} - defer rows.Close() +// populateSchema looks up for the table info in the database and populates its +// schema for internal use. +func (d *database) populateSchema() (err error) { + var collections []string - collections = []string{} + d.schema = schema.NewDatabaseSchema() - var name string + var conn ConnectionURL - for rows.Next() { - // Getting table name. - if err = rows.Scan(&name); err != nil { - return nil, err - } + if conn, err = ParseURL(d.connURL.String()); err != nil { + return err + } - // Adding table entry to schema. - s.schema.AddTable(name) + d.schema.Name = conn.Database - // Adding table to collections array. - collections = append(collections, name) + // The Collections() call will populate schema if its nil. + if collections, err = d.Collections(); err != nil { + return err } - return collections, nil + for i := range collections { + // Populate each collection. + if _, err = d.Collection(collections[i]); err != nil { + return err + } + } + + return err } -func (s *source) tableExists(names ...string) error { +func (d *database) tableExists(names ...string) error { var stmt sqlgen.Statement var err error var rows *sqlx.Rows for i := range names { - if s.schema.HasTable(names[i]) { + if d.schema.HasTable(names[i]) { // We already know this table exists. continue } stmt = sqlgen.Statement{ - Type: sqlgen.SqlSelect, - Table: sqlgen.Table{`sqlite_master`}, - Columns: sqlgen.Columns{ - {`tbl_name`}, - }, - Where: sqlgen.Where{ - sqlgen.ColumnValue{sqlgen.Column{`type`}, `=`, sqlPlaceholder}, - sqlgen.ColumnValue{sqlgen.Column{`tbl_name`}, `=`, sqlPlaceholder}, - }, + Type: sqlgen.Select, + Table: sqlgen.TableWithName(`sqlite_master`), + Columns: sqlgen.JoinColumns( + sqlgen.ColumnWithName(`tbl_name`), + ), + Where: sqlgen.WhereConditions( + &sqlgen.ColumnValue{ + Column: sqlgen.ColumnWithName(`type`), + Operator: `=`, + Value: sqlPlaceholder, + }, + &sqlgen.ColumnValue{ + Column: sqlgen.ColumnWithName(`tbl_name`), + Operator: `=`, + Value: sqlPlaceholder, + }, + ), } - if rows, err = s.doQuery(stmt, `table`, names[i]); err != nil { + if rows, err = d.Query(stmt, `table`, names[i]); err != nil { return db.ErrCollectionDoesNotExist } @@ -466,10 +449,10 @@ func (s *source) tableExists(names ...string) error { return nil } -func (s *source) tableColumns(tableName string) ([]string, error) { +func (d *database) tableColumns(tableName string) ([]string, error) { // Making sure this table is allocated. - tableSchema := s.schema.Table(tableName) + tableSchema := d.schema.Table(tableName) if len(tableSchema.Columns) > 0 { return tableSchema.Columns, nil @@ -477,10 +460,10 @@ func (s *source) tableColumns(tableName string) ([]string, error) { q := fmt.Sprintf(`PRAGMA TABLE_INFO('%s')`, tableName) - rows, err := s.doRawQuery(q) + rows, err := d.doRawQuery(q) - if s.columns == nil { - s.columns = make(map[string][]columnSchemaT) + if d.columns == nil { + d.columns = make(map[string][]columnSchemaT) } columns := []columnSchemaT{} @@ -489,81 +472,64 @@ func (s *source) tableColumns(tableName string) ([]string, error) { return nil, err } - s.columns[tableName] = columns + d.columns[tableName] = columns - s.schema.TableInfo[tableName].Columns = make([]string, 0, len(s.columns)) + d.schema.TableInfo[tableName].Columns = make([]string, 0, len(d.columns)) - for i := range s.columns[tableName] { - s.schema.TableInfo[tableName].Columns = append(s.schema.TableInfo[tableName].Columns, s.columns[tableName][i].Name) + for i := range d.columns[tableName] { + d.schema.TableInfo[tableName].Columns = append(d.schema.TableInfo[tableName].Columns, d.columns[tableName][i].Name) } - return s.schema.TableInfo[tableName].Columns, nil + return d.schema.TableInfo[tableName].Columns, nil } -// Returns a collection instance by name. -func (s *source) Collection(names ...string) (db.Collection, error) { - var err error - - if len(names) == 0 { - return nil, db.ErrMissingCollectionName - } - - if s.tx != nil { - if s.tx.done { - return nil, sql.ErrTxDone - } - } +func (d *database) getPrimaryKey(tableName string) ([]string, error) { + tableSchema := d.schema.Table(tableName) - col := &table{ - source: s, - names: names, - } + d.tableColumns(tableName) - for _, name := range names { - chunks := strings.SplitN(name, ` `, 2) + maxValue := -1 - if len(chunks) == 0 { - return nil, db.ErrMissingCollectionName + for i := range d.columns[tableName] { + if d.columns[tableName][i].PK > 0 && d.columns[tableName][i].PK > maxValue { + maxValue = d.columns[tableName][i].PK } + } - tableName := chunks[0] - - if err := s.tableExists(tableName); err != nil { - return nil, err - } + if maxValue > 0 { + tableSchema.PrimaryKey = make([]string, maxValue) - if col.Columns, err = s.tableColumns(tableName); err != nil { - return nil, err + for i := range d.columns[tableName] { + if d.columns[tableName][i].PK > 0 { + tableSchema.PrimaryKey[d.columns[tableName][i].PK-1] = d.columns[tableName][i].Name + } } } - return col, nil + return tableSchema.PrimaryKey, nil } -// getPrimaryKey returns the names of the columns that define the primary key -// of the table. -func (s *source) getPrimaryKey(tableName string) ([]string, error) { - tableSchema := s.schema.Table(tableName) +func (d *database) doRawQuery(query string, args ...interface{}) (*sqlx.Rows, error) { + var rows *sqlx.Rows + var err error + var start, end int64 - s.tableColumns(tableName) + start = time.Now().UnixNano() - maxValue := -1 + defer func() { + end = time.Now().UnixNano() + sqlutil.Log(query, args, err, start, end) + }() - for i := range s.columns[tableName] { - if s.columns[tableName][i].PK > 0 && s.columns[tableName][i].PK > maxValue { - maxValue = s.columns[tableName][i].PK - } + if d.session == nil { + return nil, db.ErrNotConnected } - if maxValue > 0 { - tableSchema.PrimaryKey = make([]string, maxValue) - - for i := range s.columns[tableName] { - if s.columns[tableName][i].PK > 0 { - tableSchema.PrimaryKey[s.columns[tableName][i].PK-1] = s.columns[tableName][i].Name - } - } + if d.tx != nil { + rows, err = d.tx.Queryx(query, args...) + } else { + rows, err = d.session.Queryx(query, args...) } - return tableSchema.PrimaryKey, nil + return rows, err } diff --git a/sqlite/database_test.go b/sqlite/database_test.go index 4480e392..43cf54d6 100644 --- a/sqlite/database_test.go +++ b/sqlite/database_test.go @@ -47,7 +47,7 @@ import ( ) const ( - database = `_dumps/gotest.sqlite3.db` + databaseName = `_dumps/gotest.sqlite3.db` ) const ( @@ -55,7 +55,7 @@ const ( ) var settings = ConnectionURL{ - Database: database, + Database: databaseName, } // Structure for testing conversions and datatypes. @@ -167,7 +167,7 @@ func TestOldSettings(t *testing.T) { var sess db.Database oldSettings := db.Settings{ - Database: database, + Database: databaseName, } // Opening database. @@ -1402,7 +1402,7 @@ func BenchmarkAppendRawSQL(b *testing.B) { defer sess.Close() - driver := sess.Driver().(*sql.DB) + driver := sess.Driver().(*sqlx.DB) if _, err = driver.Exec(`DELETE FROM "artist"`); err != nil { b.Fatal(err) @@ -1456,7 +1456,7 @@ func BenchmarkAppendTxRawSQL(b *testing.B) { defer sess.Close() - driver := sess.Driver().(*sql.DB) + driver := sess.Driver().(*sqlx.DB) if tx, err = driver.Begin(); err != nil { b.Fatal(err) diff --git a/sqlite/result.go b/sqlite/result.go deleted file mode 100644 index 6504200c..00000000 --- a/sqlite/result.go +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam -// -// 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 sqlite - -import ( - "fmt" - "strings" - - "github.com/jmoiron/sqlx" - "upper.io/db" - "upper.io/db/util/sqlgen" - "upper.io/db/util/sqlutil" -) - -type counter struct { - Total uint64 `db:"_t"` -} - -type result struct { - table *table - cursor *sqlx.Rows // This is the main query cursor. It starts as a nil value. - limit sqlgen.Limit - offset sqlgen.Offset - columns sqlgen.Columns - where sqlgen.Where - orderBy sqlgen.OrderBy - groupBy sqlgen.GroupBy - arguments []interface{} -} - -// Executes a SELECT statement that can feed Next(), All() or One(). -func (r *result) setCursor() error { - var err error - // We need a cursor, if the cursor does not exists yet then we create one. - if r.cursor == nil { - r.cursor, err = r.table.source.doQuery(sqlgen.Statement{ - Type: sqlgen.SqlSelect, - Table: sqlgen.Table{r.table.Name()}, - Columns: r.columns, - Limit: r.limit, - Offset: r.offset, - Where: r.where, - OrderBy: r.orderBy, - GroupBy: r.groupBy, - }, r.arguments...) - } - return err -} - -// Sets conditions for reducing the working set. -func (r *result) Where(terms ...interface{}) db.Result { - r.where, r.arguments = whereValues(terms) - return r -} - -// Determines the maximum limit of results to be returned. -func (r *result) Limit(n uint) db.Result { - r.limit = sqlgen.Limit(n) - return r -} - -// Determines how many documents will be skipped before starting to grab -// results. -func (r *result) Skip(n uint) db.Result { - r.offset = sqlgen.Offset(n) - return r -} - -// Used to group results that have the same value in the same column or -// columns. -func (r *result) Group(fields ...interface{}) db.Result { - - groupByColumns := make(sqlgen.GroupBy, 0, len(fields)) - - l := len(fields) - - for i := 0; i < l; i++ { - switch value := fields[i].(type) { - // Maybe other types? - default: - groupByColumns = append(groupByColumns, sqlgen.Column{value}) - } - } - - r.groupBy = groupByColumns - - return r -} - -// Determines sorting of results according to the provided names. Fields may be -// prefixed by - (minus) which means descending order, ascending order would be -// used otherwise. -func (r *result) Sort(fields ...interface{}) db.Result { - - sortColumns := make(sqlgen.SortColumns, 0, len(fields)) - - l := len(fields) - for i := 0; i < l; i++ { - var sort sqlgen.SortColumn - - switch value := fields[i].(type) { - case db.Raw: - sort = sqlgen.SortColumn{ - sqlgen.Column{sqlgen.Raw{fmt.Sprintf(`%v`, value.Value)}}, - sqlgen.SqlSortAsc, - } - case string: - if strings.HasPrefix(value, `-`) { - // Explicit descending order. - sort = sqlgen.SortColumn{ - sqlgen.Column{value[1:]}, - sqlgen.SqlSortDesc, - } - } else { - // Ascending order. - sort = sqlgen.SortColumn{ - sqlgen.Column{value}, - sqlgen.SqlSortAsc, - } - } - } - sortColumns = append(sortColumns, sort) - } - - r.orderBy.SortColumns = sortColumns - - return r -} - -// Retrieves only the given fields. -func (r *result) Select(fields ...interface{}) db.Result { - - r.columns = make(sqlgen.Columns, 0, len(fields)) - - l := len(fields) - for i := 0; i < l; i++ { - var col sqlgen.Column - switch value := fields[i].(type) { - case db.Func: - v := interfaceArgs(value.Args) - var s string - if len(v) == 0 { - s = fmt.Sprintf(`%s()`, value.Name) - } else { - ss := make([]string, 0, len(v)) - for j := range v { - ss = append(ss, fmt.Sprintf(`%v`, v[j])) - } - s = fmt.Sprintf(`%s(%s)`, value.Name, strings.Join(ss, `, `)) - } - col = sqlgen.Column{sqlgen.Raw{s}} - case db.Raw: - col = sqlgen.Column{sqlgen.Raw{fmt.Sprintf(`%v`, value.Value)}} - default: - col = sqlgen.Column{value} - } - r.columns = append(r.columns, col) - } - - return r -} - -// Dumps all results into a pointer to an slice of structs or maps. -func (r *result) All(dst interface{}) error { - var err error - - if r.cursor != nil { - return db.ErrQueryIsPending - } - - // Current cursor. - err = r.setCursor() - - if err != nil { - return err - } - - defer r.Close() - - // Fetching all results within the cursor. - err = sqlutil.FetchRows(r.cursor, dst) - - return err -} - -// Fetches only one result from the resultset. -func (r *result) One(dst interface{}) error { - var err error - - if r.cursor != nil { - return db.ErrQueryIsPending - } - - defer r.Close() - - err = r.Next(dst) - - return err -} - -// Fetches the next result from the resultset. -func (r *result) Next(dst interface{}) (err error) { - - if err = r.setCursor(); err != nil { - r.Close() - return err - } - - if err = sqlutil.FetchRow(r.cursor, dst); err != nil { - r.Close() - return err - } - - return nil -} - -// Removes the matching items from the collection. -func (r *result) Remove() error { - var err error - - _, err = r.table.source.doExec(sqlgen.Statement{ - Type: sqlgen.SqlDelete, - Table: sqlgen.Table{r.table.Name()}, - Where: r.where, - }, r.arguments...) - - return err - -} - -// Updates matching items from the collection with values of the given map or -// struct. -func (r *result) Update(values interface{}) error { - - ff, vv, err := r.table.FieldValues(values) - if err != nil { - return err - } - - total := len(ff) - - cvs := make(sqlgen.ColumnValues, 0, total) - - for i := 0; i < total; i++ { - cvs = append(cvs, sqlgen.ColumnValue{sqlgen.Column{ff[i]}, "=", sqlPlaceholder}) - } - - vv = append(vv, r.arguments...) - - _, err = r.table.source.doExec(sqlgen.Statement{ - Type: sqlgen.SqlUpdate, - Table: sqlgen.Table{r.table.Name()}, - ColumnValues: cvs, - Where: r.where, - }, vv...) - - return err -} - -// Closes the result set. -func (r *result) Close() (err error) { - if r.cursor != nil { - err = r.cursor.Close() - r.cursor = nil - } - return err -} - -// Counts the elements within the main conditions of the set. -func (r *result) Count() (uint64, error) { - var count counter - - row, err := r.table.source.doQueryRow(sqlgen.Statement{ - Type: sqlgen.SqlSelectCount, - Table: sqlgen.Table{r.table.Name()}, - Where: r.where, - }, r.arguments...) - - if err != nil { - return 0, err - } - - err = row.Scan(&count.Total) - if err != nil { - return 0, err - } - - return count.Total, nil -} diff --git a/sqlite/sqlite.go b/sqlite/sqlite.go new file mode 100644 index 00000000..eecef2d3 --- /dev/null +++ b/sqlite/sqlite.go @@ -0,0 +1,69 @@ +// Copyright (c) 2012-2015 José Carlos Nieto, https://menteslibres.net/xiam +// +// 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 sqlite + +import ( + "upper.io/cache" + "upper.io/db" + "upper.io/db/util/sqlgen" +) + +// Adapter is the public name of the adapter. +const Adapter = `sqlite` + +var template *sqlgen.Template + +func init() { + template = &sqlgen.Template{ + ColumnSeparator: adapterColumnSeparator, + IdentifierSeparator: adapterIdentifierSeparator, + IdentifierQuote: adapterIdentifierQuote, + ValueSeparator: adapterValueSeparator, + ValueQuote: adapterValueQuote, + AndKeyword: adapterAndKeyword, + OrKeyword: adapterOrKeyword, + NotKeyword: adapterNotKeyword, + DescKeyword: adapterDescKeyword, + AscKeyword: adapterAscKeyword, + DefaultOperator: adapterDefaultOperator, + 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/sqlite/layout.go b/sqlite/template.go similarity index 66% rename from sqlite/layout.go rename to sqlite/template.go index 9d937696..48c1ce6d 100644 --- a/sqlite/layout.go +++ b/sqlite/template.go @@ -22,37 +22,37 @@ package sqlite const ( - sqlColumnSeparator = `.` - sqlIdentifierSeparator = `, ` - sqlIdentifierQuote = `"{{.Raw}}"` - sqlValueSeparator = `, ` - sqlValueQuote = `'{{.}}'` - sqlAndKeyword = `AND` - sqlOrKeyword = `OR` - sqlNotKeyword = `NOT` - sqlDescKeyword = `DESC` - sqlAscKeyword = `ASC` - sqlDefaultOperator = `=` - sqlClauseGroup = `({{.}})` - sqlClauseOperator = ` {{.}} ` - sqlColumnValue = `{{.Column}} {{.Operator}} {{.Value}}` - sqlTableAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` - sqlColumnAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` - sqlSortByColumnLayout = `{{.Column}} {{.Sort}}` - - sqlOrderByLayout = ` + adapterColumnSeparator = `.` + adapterIdentifierSeparator = `, ` + adapterIdentifierQuote = `"{{.Value}}"` + adapterValueSeparator = `, ` + adapterValueQuote = `'{{.}}'` + adapterAndKeyword = `AND` + adapterOrKeyword = `OR` + adapterNotKeyword = `NOT` + adapterDescKeyword = `DESC` + adapterAscKeyword = `ASC` + adapterDefaultOperator = `=` + 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}} ` - sqlWhereLayout = ` + adapterWhereLayout = ` {{if .Conds}} WHERE {{.Conds}} {{end}} ` - sqlSelectLayout = ` + adapterSelectLayout = ` SELECT {{if .Columns}} @@ -75,24 +75,24 @@ const ( {{if .Offset}} {{if not .Limit}} - LIMIT -1 + LIMIT -1 {{end}} OFFSET {{.Offset}} {{end}} ` - sqlDeleteLayout = ` + adapterDeleteLayout = ` DELETE FROM {{.Table}} {{.Where}} ` - sqlUpdateLayout = ` + adapterUpdateLayout = ` UPDATE {{.Table}} SET {{.ColumnValues}} {{ .Where }} ` - sqlSelectCountLayout = ` + adapterSelectCountLayout = ` SELECT COUNT(1) AS _t FROM {{.Table}} @@ -104,13 +104,13 @@ const ( {{if .Offset}} {{if not .Limit}} - LIMIT -1 + LIMIT -1 {{end}} OFFSET {{.Offset}} {{end}} ` - sqlInsertLayout = ` + adapterInsertLayout = ` INSERT INTO {{.Table}} ({{.Columns}}) VALUES @@ -118,23 +118,21 @@ const ( {{.Extra}} ` - sqlTruncateLayout = ` + adapterTruncateLayout = ` DELETE FROM {{.Table}} ` - sqlDropDatabaseLayout = ` + adapterDropDatabaseLayout = ` DROP DATABASE {{.Database}} ` - sqlDropTableLayout = ` + adapterDropTableLayout = ` DROP TABLE {{.Table}} ` - sqlGroupByLayout = ` + adapterGroupByLayout = ` {{if .GroupColumns}} GROUP BY {{.GroupColumns}} {{end}} ` - - sqlNull = `NULL` ) diff --git a/sqlite/tx.go b/sqlite/tx.go deleted file mode 100644 index dbfc7698..00000000 --- a/sqlite/tx.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam -// -// 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 sqlite - -import ( - "github.com/jmoiron/sqlx" -) - -type tx struct { - *source - sqlTx *sqlx.Tx - done bool -} - -func (t *tx) Commit() (err error) { - err = t.sqlTx.Commit() - if err == nil { - t.done = true - } - return err -} - -func (t *tx) Rollback() error { - return t.sqlTx.Rollback() -} -- GitLab