diff --git a/ql/collection.go b/ql/collection.go index a63b7ad89ade057e6342a0e8e8c6ed591cbf00f2..53a5d4c1b7e98e0237a9a42278ab7973bda83495 100644 --- a/ql/collection.go +++ b/ql/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,231 +22,68 @@ package ql import ( - "fmt" + "database/sql" "reflect" "strings" "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 - columnTypes map[string]reflect.Kind - source *source + *database names []string + columnTypes map[string]reflect.Kind } -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 -} +var _ = db.Collection(&table{}) +// Find creates a result set with the given conditions. func (t *table) Find(terms ...interface{}) db.Result { - where, arguments := whereValues(terms) - - result := &result{ - table: t, - where: where, - arguments: arguments, - } - - return result + where, arguments := template.ToWhereWithArguments(terms) + return result.NewResult(template, t, where, arguments) } -func (t *table) tableN(i int) string { - if len(t.names) > i { - chunks := strings.SplitN(t.names[i], " ", 2) - if len(chunks) > 0 { - return chunks[0] - } - } - return "" -} - -// Deletes all the rows within the collection. +// Truncate deletes all rows from the table. func (t *table) Truncate() error { - - _, err := t.source.doExec(sqlgen.Statement{ - Type: sqlgen.SqlTruncate, - Table: sqlgen.Table{t.tableN(0)}, + _, 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. +// Append inserts an item (map or struct) into the collection. func (t *table) Append(item interface{}) (interface{}, error) { - cols, vals, err := t.FieldValues(item) - - var columns sqlgen.Columns - var values sqlgen.Values + columnNames, columnValues, err := t.FieldValues(item) - for _, col := range cols { - columns = append(columns, sqlgen.Column{col}) + if err != nil { + return nil, err } - for i := 0; i < len(vals); i++ { - values = append(values, sqlPlaceholder) - } + sqlgenCols, sqlgenVals, sqlgenArgs, err := template.ToColumnsValuesAndArguments(columnNames, columnValues) - // Error ocurred, stop appending. if err != nil { return nil, err } - res, err := t.source.doExec(sqlgen.Statement{ - Type: sqlgen.SqlInsert, - Table: sqlgen.Table{t.tableN(0)}, - Columns: columns, - Values: values, - }, vals...) + stmt := sqlgen.Statement{ + Type: sqlgen.Insert, + Table: sqlgen.TableWithName(t.MainTableName()), + Columns: sqlgenCols, + Values: sqlgenVals, + } - if err != nil { + var res sql.Result + if res, err = t.database.Exec(stmt, sqlgenArgs...); err != nil { return nil, err } @@ -263,14 +100,15 @@ func (t *table) Append(item interface{}) (interface{}, error) { return id, nil } -// Returns true if the collection exists. +// Exists returns true if the collection exists. func (t *table) Exists() bool { - if err := t.source.tableExists(t.names...); err != nil { + 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.names, `, `) + return strings.Join(t.Tables, `, `) } diff --git a/ql/database.go b/ql/database.go index 1a1c5623b1c1815b373d6cb75f32ffcc6eaa6153..24dd77245e322db3ab5ce824e536d49e2fad058d 100644 --- a/ql/database.go +++ b/ql/database.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 @@ -24,121 +24,249 @@ package ql import ( "database/sql" "fmt" - "os" "strings" "time" _ "github.com/cznic/ql/driver" // QL driver "github.com/jmoiron/sqlx" - "upper.io/cache" "upper.io/db" "upper.io/db/util/schema" "upper.io/db/util/sqlgen" "upper.io/db/util/sqlutil" -) - -const ( - // Adapter is the public name of the adapter. - Adapter = `ql` + "upper.io/db/util/sqlutil/tx" ) var ( - template *sqlgen.Template - - 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 } +type tx struct { + *sqltx.Tx + *database +} + +var ( + _ = db.Database(&database{}) + _ = db.Tx(&tx{}) +) + type columnSchemaT struct { Name string `db:"Name"` } -func debugEnabled() bool { - if os.Getenv(db.EnvEnableDebug) != "" { - return true +// Driver returns the underlying *sqlx.DB instance. +func (d *database) Driver() interface{} { + return d.session +} + +// Open attempts to connect to the database server using already stored settings. +func (d *database) Open() error { + var err error + + // 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, + } + + d.connURL = conn } - return false + + if d.session, err = sqlx.Open(`ql`, d.connURL.String()); err != nil { + return err + } + + d.session.Mapper = sqlutil.NewMapper() + + if err = d.populateSchema(); err != nil { + return err + } + + return nil } -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() +// Clone returns a cloned db.Database session, this is typically used for +// transactions. +func (d *database) Clone() (db.Database, error) { + return d.clone() +} + +func (d *database) clone() (adapter *database, err error) { + adapter = new(database) + + if err = adapter.Setup(d.connURL); err != nil { + return nil, err } + + return adapter, nil } -func init() { - - template = &sqlgen.Template{ - qlColumnSeparator, - qlIdentifierSeparator, - qlIdentifierQuote, - qlValueSeparator, - qlValueQuote, - qlAndKeyword, - qlOrKeyword, - qlNotKeyword, - qlDescKeyword, - qlAscKeyword, - qlDefaultOperator, - qlClauseGroup, - qlClauseOperator, - qlColumnValue, - qlTableAliasLayout, - qlColumnAliasLayout, - qlSortByColumnLayout, - qlWhereLayout, - qlOrderByLayout, - qlInsertLayout, - qlSelectLayout, - qlUpdateLayout, - qlDeleteLayout, - qlTruncateLayout, - qlDropDatabaseLayout, - qlDropTableLayout, - qlSelectCountLayout, - qlGroupByLayout, - cache.NewCache(), - } - - db.Register(Adapter, &source{}) +// 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() } -func (s *source) populateSchema() (err error) { - var collections []string +// Close terminates the current database session. +func (d *database) Close() error { + if d.session != nil { + return d.session.Close() + } + return nil +} + +// 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.Tables = names + + 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 + } + } - s.schema = schema.NewDatabaseSchema() + return col, nil +} + +// 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 + } + + // Schema is empty. + + // Querying table names. + stmt := sqlgen.Statement{ + Type: sqlgen.Select, + Table: sqlgen.TableWithName(`__Table`), + Columns: sqlgen.JoinColumns( + sqlgen.ColumnWithName(`Name`), + ), + } + + // Executing statement. + var rows *sqlx.Rows + if rows, err = d.Query(stmt); err != nil { + return nil, err + } + + defer rows.Close() + + collections = []string{} + + var name string + + 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 collections, nil +} + +// Use changes the active database. +func (d *database) Use(database string) (err error) { var conn ConnectionURL - if conn, err = ParseURL(s.connURL.String()); err != nil { + if conn, err = ParseURL(d.connURL.String()); err != nil { return err } - s.schema.Name = conn.Database + conn.Database = database - // The Collections() call will populate schema if its nil. - if collections, err = s.Collections(); err != nil { - return err + d.connURL = conn + + return d.Open() +} + +// Drop removes all tables from the current database. +func (d *database) Drop() error { + return db.ErrUnsupported +} + +// Setup stores database settings. +func (d *database) Setup(conn db.ConnectionURL) error { + d.connURL = conn + return d.Open() +} + +// Name returns the name of the database. +func (d *database) Name() string { + return d.schema.Name +} + +// 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 *database + var sqlTx *sqlx.Tx + + if clone, err = d.clone(); err != nil { + return nil, err } - for i := range collections { - // Populate each collection. - if _, err = s.Collection(collections[i]); err != nil { - return err - } + if sqlTx, err = clone.session.Beginx(); err != nil { + return nil, err } - return err + clone.tx = sqltx.New(sqlTx) + + return tx{Tx: clone.tx, database: clone}, nil } -func (s *source) doExec(stmt sqlgen.Statement, args ...interface{}) (sql.Result, 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 @@ -148,26 +276,26 @@ func (s *source) doExec(stmt sqlgen.Statement, args ...interface{}) (sql.Result, defer func() { end = time.Now().UnixNano() - debugLog(query, args, err, start, end) + sqlutil.Log(query, args, err, start, end) }() - if s.session == nil { + if d.session == nil { return nil, db.ErrNotConnected } - query = stmt.Compile(template) + query = stmt.Compile(template.Template) l := len(args) for i := 0; i < l; i++ { query = strings.Replace(query, `?`, fmt.Sprintf(`$%d`, i+1), 1) } - if s.tx != nil { - res, err = s.tx.sqlTx.Exec(query, args...) + if d.tx != nil { + res, err = d.tx.Exec(query, args...) } else { var tx *sqlx.Tx - if tx, err = s.session.Beginx(); err != nil { + if tx, err = d.session.Beginx(); err != nil { return nil, err } @@ -183,7 +311,8 @@ func (s *source) doExec(stmt sqlgen.Statement, args ...interface{}) (sql.Result, return res, err } -func (s *source) doQuery(stmt sqlgen.Statement, args ...interface{}) (*sqlx.Rows, error) { +// 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 @@ -193,26 +322,26 @@ func (s *source) doQuery(stmt sqlgen.Statement, args ...interface{}) (*sqlx.Rows defer func() { end = time.Now().UnixNano() - debugLog(query, args, err, start, end) + sqlutil.Log(query, args, err, start, end) }() - if s.session == nil { + if d.session == nil { return nil, db.ErrNotConnected } - query = stmt.Compile(template) + query = stmt.Compile(template.Template) l := len(args) for i := 0; i < l; i++ { query = strings.Replace(query, `?`, fmt.Sprintf(`$%d`, i+1), 1) } - if s.tx != nil { - rows, err = s.tx.sqlTx.Queryx(query, args...) + if d.tx != nil { + rows, err = d.tx.Queryx(query, args...) } else { var tx *sqlx.Tx - if tx, err = s.session.Beginx(); err != nil { + if tx, err = d.session.Beginx(); err != nil { return nil, err } @@ -228,7 +357,8 @@ func (s *source) doQuery(stmt sqlgen.Statement, args ...interface{}) (*sqlx.Rows return rows, err } -func (s *source) doQueryRow(stmt sqlgen.Statement, args ...interface{}) (*sqlx.Row, error) { +// 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 @@ -238,26 +368,26 @@ func (s *source) doQueryRow(stmt sqlgen.Statement, args ...interface{}) (*sqlx.R defer func() { end = time.Now().UnixNano() - debugLog(query, args, err, start, end) + sqlutil.Log(query, args, err, start, end) }() - if s.session == nil { + if d.session == nil { return nil, db.ErrNotConnected } - query = stmt.Compile(template) + query = stmt.Compile(template.Template) l := len(args) for i := 0; i < l; i++ { query = strings.Replace(query, `?`, fmt.Sprintf(`$%d`, i+1), 1) } - if s.tx != nil { - row = s.tx.sqlTx.QueryRowx(query, args...) + if d.tx != nil { + row = d.tx.QueryRowx(query, args...) } else { var tx *sqlx.Tx - if tx, err = s.session.Beginx(); err != nil { + if tx, err = d.session.Beginx(); err != nil { return nil, err } @@ -273,194 +403,64 @@ func (s *source) doQueryRow(stmt sqlgen.Statement, args ...interface{}) (*sqlx.R return row, err } -// Returns the string name of the database. -func (s *source) Name() string { - return s.schema.Name -} - -// Ping verifies a connection to the database is still alive, -// establishing a connection if necessary. -func (s *source) Ping() error { - return s.session.Ping() -} - -func (s *source) clone() (adapter *source, err error) { - adapter = new(source) - - if err = adapter.Setup(s.connURL); err != nil { - return nil, err - } - - return adapter, nil -} - -func (s *source) Clone() (db.Database, error) { - return s.clone() -} - -func (s *source) Transaction() (db.Tx, error) { - var err error - var clone *source - var sqlTx *sqlx.Tx - - if clone, err = s.clone(); err != nil { - return nil, err - } - - if sqlTx, err = s.session.Beginx(); 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() -} - -// Returns the underlying *sqlx.DB instance. -func (s *source) Driver() interface{} { - return s.session -} - -// Attempts to connect to a database using the stored settings. -func (s *source) Open() error { - var err error - - // 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, - } - - s.connURL = conn - } - - if s.session, err = sqlx.Open(`ql`, s.connURL.String()); err != nil { - return err - } - - s.session.Mapper = sqlutil.NewMapper() - - if err = s.populateSchema(); err != nil { - return err - } +// 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 - return nil -} + d.schema = schema.NewDatabaseSchema() -// Closes the current database session. -func (s *source) Close() error { - if s.session != nil { - return s.session.Close() - } - return nil -} - -// Changes the active database. -func (s *source) Use(database string) (err error) { var conn ConnectionURL - if conn, err = ParseURL(s.connURL.String()); err != nil { + if conn, err = ParseURL(d.connURL.String()); err != nil { return err } - conn.Database = database - - s.connURL = conn - - return s.Open() -} - -// Drops the currently active database. -func (s *source) Drop() error { - return db.ErrUnsupported -} - -// Returns a list of all tables within the currently active database. -func (s *source) Collections() (collections []string, err error) { - - tablesInSchema := len(s.schema.Tables) + d.schema.Name = conn.Database - // Is schema already populated? - if tablesInSchema > 0 { - // Pulling table names from schema. - return s.schema.Tables, nil - } - - // Schema is empty. - - // Querying table names. - stmt := sqlgen.Statement{ - Type: sqlgen.SqlSelect, - Table: sqlgen.Table{`__Table`}, - Columns: sqlgen.Columns{ - {`Name`}, - }, - } - - // Executing statement. - var rows *sqlx.Rows - if rows, err = s.doQuery(stmt); err != nil { - return nil, err + // The Collections() call will populate schema if its nil. + if collections, err = d.Collections(); err != nil { + return err } - defer rows.Close() - - collections = []string{} - - var name string - - for rows.Next() { - // Getting table name. - if err = rows.Scan(&name); err != nil { - return nil, err + for i := range collections { + // Populate each collection. + if _, err = d.Collection(collections[i]); err != nil { + return err } - - // Adding table entry to schema. - s.schema.AddTable(name) - - // Adding table to collections array. - collections = append(collections, name) } - return collections, nil + 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{`__Table`}, - Columns: sqlgen.Columns{ - {`Name`}, - }, - Where: sqlgen.Where{ - sqlgen.ColumnValue{sqlgen.Column{`Name`}, `==`, sqlPlaceholder}, - }, + Type: sqlgen.Select, + Table: sqlgen.TableWithName(`__Table`), + Columns: sqlgen.JoinColumns( + sqlgen.ColumnWithName(`Name`), + ), + Where: sqlgen.WhereConditions( + &sqlgen.ColumnValue{ + Column: sqlgen.ColumnWithName(`Name`), + Operator: `==`, + Value: sqlPlaceholder, + }, + ), } - if rows, err = s.doQuery(stmt, names[i]); err != nil { + if rows, err = d.Query(stmt, names[i]); err != nil { return db.ErrCollectionDoesNotExist } @@ -474,31 +474,35 @@ 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 } stmt := sqlgen.Statement{ - Type: sqlgen.SqlSelect, - Table: sqlgen.Table{`__Column`}, - Columns: sqlgen.Columns{ - {`Name`}, - {`Type`}, - }, - Where: sqlgen.Where{ - sqlgen.ColumnValue{sqlgen.Column{`TableName`}, `==`, sqlPlaceholder}, - }, + Type: sqlgen.Select, + Table: sqlgen.TableWithName(`__Column`), + Columns: sqlgen.JoinColumns( + sqlgen.ColumnWithName(`Name`), + sqlgen.ColumnWithName(`Type`), + ), + Where: sqlgen.WhereConditions( + &sqlgen.ColumnValue{ + Column: sqlgen.ColumnWithName(`TableName`), + Operator: `==`, + Value: sqlPlaceholder, + }, + ), } var rows *sqlx.Rows var err error - if rows, err = s.doQuery(stmt, tableName); err != nil { + if rows, err = d.Query(stmt, tableName); err != nil { return nil, err } @@ -508,51 +512,11 @@ func (s *source) tableColumns(tableName string) ([]string, error) { return nil, err } - s.schema.TableInfo[tableName].Columns = make([]string, 0, len(tableFields)) + d.schema.TableInfo[tableName].Columns = make([]string, 0, len(tableFields)) for i := range tableFields { - s.schema.TableInfo[tableName].Columns = append(s.schema.TableInfo[tableName].Columns, tableFields[i].Name) + d.schema.TableInfo[tableName].Columns = append(d.schema.TableInfo[tableName].Columns, tableFields[i].Name) } - return s.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 - } - } - - col := &table{ - source: s, - names: names, - } - - for _, name := range names { - chunks := strings.SplitN(name, ` `, 2) - - if len(chunks) == 0 { - return nil, db.ErrMissingCollectionName - } - - tableName := chunks[0] - - if err := s.tableExists(tableName); err != nil { - return nil, err - } - - if col.Columns, err = s.tableColumns(tableName); err != nil { - return nil, err - } - } - - return col, nil + return d.schema.TableInfo[tableName].Columns, nil } diff --git a/ql/database_test.go b/ql/database_test.go index dd389d34df9da070abd3dde6eee1700c7fd280e6..43957d73fc4f7e13ea2fb1424f0fa1a33ff2832d 100644 --- a/ql/database_test.go +++ b/ql/database_test.go @@ -46,7 +46,7 @@ import ( ) const ( - database = `_dumps/test.db` + databaseName = `_dumps/test.db` ) const ( @@ -54,7 +54,7 @@ const ( ) var settings = db.Settings{ - Database: database, + Database: databaseName, } // Structure for testing conversions and datatypes. @@ -155,7 +155,7 @@ func TestOldSettings(t *testing.T) { var sess db.Database oldSettings := db.Settings{ - Database: database, + Database: databaseName, } // Opening database. @@ -942,6 +942,7 @@ func TestRawQuery(t *testing.T) { var rows *sqlx.Rows var err error var drv *sqlx.DB + var tx *sqlx.Tx type publicationType struct { ID int64 `db:"id,omitempty"` @@ -957,7 +958,11 @@ func TestRawQuery(t *testing.T) { drv = sess.Driver().(*sqlx.DB) - rows, err = drv.Queryx(` + if tx, err = drv.Beginx(); err != nil { + t.Fatal(err) + } + + if rows, err = tx.Queryx(` SELECT p.id AS id, p.title AS publication_title, @@ -967,9 +972,11 @@ func TestRawQuery(t *testing.T) { (SELECT id() AS id, title, author_id FROM publication) AS p WHERE a.id == p.author_id - `) + `); err != nil { + t.Fatal(err) + } - if err != nil { + if err = tx.Commit(); err != nil { t.Fatal(err) } diff --git a/ql/ql.go b/ql/ql.go new file mode 100644 index 0000000000000000000000000000000000000000..acffe02cff797d9a84fcb0e853d938a68dd08d51 --- /dev/null +++ b/ql/ql.go @@ -0,0 +1,72 @@ +// 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 ql + +import ( + "upper.io/cache" + "upper.io/db" + "upper.io/db/util/sqlgen" + "upper.io/db/util/sqlutil" +) + +// Adapter is the public name of the adapter. +const Adapter = `ql` + +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/ql/result.go b/ql/result.go deleted file mode 100644 index 60985da8abcbd586424ff99aa8544b32decfdf74..0000000000000000000000000000000000000000 --- a/ql/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 ql - -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:"total"` -} - -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/ql/layout.go b/ql/template.go similarity index 65% rename from ql/layout.go rename to ql/template.go index d60f32e2d49b1bb55ba2f4dcae6dae061e31c609..61fc47c044905a107393e34539f62e3f19e1cebc 100644 --- a/ql/layout.go +++ b/ql/template.go @@ -22,37 +22,38 @@ package ql const ( - qlColumnSeparator = `.` - qlIdentifierSeparator = `, ` - qlIdentifierQuote = `{{.Raw}}` - qlValueSeparator = `, ` - qlValueQuote = `"{{.}}"` - qlAndKeyword = `&&` - qlOrKeyword = `||` - qlNotKeyword = `!=` - qlDescKeyword = `DESC` - qlAscKeyword = `ASC` - qlDefaultOperator = `==` - qlClauseGroup = `({{.}})` - qlClauseOperator = ` {{.}} ` - qlColumnValue = `{{.Column}} {{.Operator}} {{.Value}}` - qlTableAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` - qlColumnAliasLayout = `{{.Name}}{{if .Alias}} AS {{.Alias}}{{end}}` - qlSortByColumnLayout = `{{.Column}} {{.Sort}}` - - qlOrderByLayout = ` + adapterColumnSeparator = `.` + adapterIdentifierSeparator = `, ` + adapterIdentifierQuote = `{{.Value}}` + adapterValueSeparator = `, ` + adapterValueQuote = `"{{.}}"` + adapterAndKeyword = `&&` + adapterOrKeyword = `||` + adapterNotKeyword = `!=` + 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}} ` - qlWhereLayout = ` + adapterWhereLayout = ` {{if .Conds}} WHERE {{.Conds}} {{end}} ` - qlSelectLayout = ` + adapterSelectLayout = ` SELECT {{if .Columns}} @@ -77,19 +78,19 @@ const ( OFFSET {{.Offset}} {{end}} ` - qlDeleteLayout = ` + adapterDeleteLayout = ` DELETE FROM {{.Table}} {{.Where}} ` - qlUpdateLayout = ` + adapterUpdateLayout = ` UPDATE {{.Table}} SET {{.ColumnValues}} {{ .Where }} ` - qlSelectCountLayout = ` + adapterSelectCountLayout = ` SELECT count(1) AS total FROM {{.Table}} @@ -104,7 +105,7 @@ const ( {{end}} ` - qlInsertLayout = ` + adapterInsertLayout = ` INSERT INTO {{.Table}} ({{.Columns}}) VALUES @@ -112,19 +113,19 @@ const ( {{.Extra}} ` - qlTruncateLayout = ` + adapterTruncateLayout = ` TRUNCATE TABLE {{.Table}} ` - qlDropDatabaseLayout = ` + adapterDropDatabaseLayout = ` DROP DATABASE {{.Database}} ` - qlDropTableLayout = ` + adapterDropTableLayout = ` DROP TABLE {{.Table}} ` - qlGroupByLayout = ` + adapterGroupByLayout = ` {{if .GroupColumns}} GROUP BY {{.GroupColumns}} {{end}} diff --git a/ql/tx.go b/ql/tx.go deleted file mode 100644 index 6093f5aa25f5ae6e53a327dc30a06606f8a7e84e..0000000000000000000000000000000000000000 --- a/ql/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 ql - -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() -}