commit 1268d4b2efa11a6f614c9dc7b6e56ac75ae0d179 Author: lqs Date: Sun Nov 18 22:11:43 2018 +0800 initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e5a74e6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Liu Qishuai + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5902b5f --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ + + +**sqlingo** is a SQL DSL & ORM library in Go. It generates code from your database and lets you write SQL queries easily. + +WARNING: sqlingo is still under development. It's expected to be released by end of November 2018. + +## Tutorial + +### Prepare your database +In order to generate code, sqlingo requires your tables are already created in the database. + +### Install sqlingo code generator +``` +$ go get -u github.com/lqs/sqlingo/sqlingo-gen +``` + +### Generate code for your database +``` +$ mkdir -p generated/sqlingo +$ sqlingo-gen root:123456@/database_name >generated/sqlingo/database_name.dsl.go +``` + +### Start using sqlingo +Create `main.go` to use the generated code +``` +import ( + "github.com/lqs/sqlingo" + "./generated/sqlingo" +) + +func main() { + db, err := sqlingo.Open("mysql", "root:123456@/database_name") + if err != nil { + panic(err) + } + + # insert some rows + customer1 := &CustomerModel{name: "Customer One"} + customer2 := &CustomerModel{name: "Customer Two"} + db.InsertInto(Customer). + Values(customer1, customer2). + Execute() + + # do some queries + var customers []*CustomerModel + db.SelectFrom(Customer). + Where(Customer.Id.In(1, 2)). + Fetch(&customers) + + # more examples + var customerId int64 + var orderId int64 + db.Select(Customer.Id, Order.Id). + From(Customer, Order). + Where(Customer.Id.Equals(Order.CustomerId), Order.Id.Equals(1)). + Fetch(&customerId, &orderId) +} +``` \ No newline at end of file diff --git a/alias.go b/alias.go new file mode 100644 index 0000000..d1335d9 --- /dev/null +++ b/alias.go @@ -0,0 +1,14 @@ +package sqlingo + +type Alias interface { + GetSQL() string +} + +type alias struct { + expression Expression + name string +} + +func (a *alias) GetSQL() string { + return a.expression.GetSQL() + " AS " + getSQLForName(a.name) +} diff --git a/common.go b/common.go new file mode 100644 index 0000000..ed81997 --- /dev/null +++ b/common.go @@ -0,0 +1,148 @@ +package sqlingo + +import ( + "fmt" + "runtime" + "strings" +) + +type Model interface { + GetTable() Table + GetValues() []interface{} +} + +type Assignment interface { + GetSQL() string +} + +type assignment struct { + Assignment + field Field + value interface{} +} + +func (a *assignment) GetSQL() string { + value, _ := getSQLFromWhatever(a.value) + return a.field.GetSQL() + " = " + value +} + +func Raw(sql string) UnknownExpression { + return &expression{sql: sql, priority: 99} +} + +func And(expressions ...BooleanExpression) (result BooleanExpression) { + if len(expressions) == 0 { + result = &expression{sql: "TRUE"} + return + } + for i, condition := range expressions { + if i == 0 { + result = condition + } else { + result = result.And(condition) + } + } + return +} + +func Or(expressions ...BooleanExpression) (result BooleanExpression) { + if len(expressions) == 0 { + result = &expression{sql: "FALSE"} + return + } + for i, condition := range expressions { + if i == 0 { + result = condition + } else { + result = result.Or(condition) + } + } + return +} + +func Function(name string, args ...Expression) Expression { + return &expression{sql: name + "(" + commaExpressions(args) + ")"} +} + +func If(predicate Expression, trueValue Expression, falseValue Expression) (result Expression) { + return Function("IF", predicate, trueValue, falseValue) +} + +func commaFields(fields []Field) string { + sql := "" + for i, item := range fields { + if i > 0 { + sql += ", " + } + sql += item.GetSQL() + } + return sql +} + +func commaExpressions(expressions []Expression) string { + sql := "" + for i, item := range expressions { + if i > 0 { + sql += ", " + } + sql += item.GetSQL() + } + return sql +} + +func commaValues(values []interface{}) string { + sql := "" + for i, item := range values { + if i > 0 { + sql += ", " + } + value, _ := getSQLFromWhatever(item) + sql += value + } + return sql +} + +func commaAssignments(assignments []assignment) string { + sql := "" + for i, item := range assignments { + if i > 0 { + sql += ", " + } + sql += item.GetSQL() + } + return sql +} + +func commaOrderBys(orderBys []OrderBy) string { + sql := "" + for i, item := range orderBys { + if i > 0 { + sql += ", " + } + sql += item.GetSQL() + } + return sql +} + +func getSQLForName(name string) string { + // TODO: check reserved words + return "`" + name + "`" +} + +func getCallerInfo() string { + for i := 0; true; i++ { + _, file, line, ok := runtime.Caller(i) + if !ok { + break + } + segs := strings.Split(file, "/") + name := segs[len(segs)-1] + switch name { + case "common.go", "select.go", "insert.go", "update.go", "delete.go": + continue + default: + return fmt.Sprintf("/* %s:%d */ ", name, line) + } + } + return "" +} diff --git a/cursor.go b/cursor.go new file mode 100644 index 0000000..7f1e701 --- /dev/null +++ b/cursor.go @@ -0,0 +1,81 @@ +package sqlingo + +import ( + "database/sql" + "fmt" + "reflect" +) + +type Cursor interface { + Next() bool + Scan(dest ...interface{}) error + Close() error +} + +type cursor struct { + rows *sql.Rows +} + +func (c *cursor) Next() bool { + return c.rows.Next() +} + +func preparePointers(val reflect.Value, scans *[]interface{}) error { + kind := val.Kind() + switch kind { + case reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, + reflect.String: + *scans = append(*scans, val.Addr().Interface()) + case reflect.Slice: + case reflect.Struct: + for j := 0; j < val.NumField(); j++ { + field := val.Field(j) + if field.Kind() == reflect.Interface { + continue + } + *scans = append(*scans, field.Addr().Interface()) + } + case reflect.Ptr: + toType := val.Type().Elem() + switch toType.Kind() { + case reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, + reflect.String: + *scans = append(*scans, val.Addr().Interface()) + default: + to := reflect.New(toType).Elem() + val.Set(to.Addr()) + preparePointers(to, scans) + } + default: + println("unknown type %s", kind.String()) + return fmt.Errorf("unknown type %s", kind.String()) + } + return nil +} + +func (c *cursor) Scan(dest ...interface{}) error { + + var scans []interface{} + for i, item := range dest { + if reflect.ValueOf(item).Kind() != reflect.Ptr { + return fmt.Errorf("argument %d is not pointer", i) + } + + val := reflect.Indirect(reflect.ValueOf(item)) + + preparePointers(val, &scans) + } + + err := c.rows.Scan(scans...) + return err +} + +func (c *cursor) Close() error { + return c.rows.Close() +} diff --git a/database.go b/database.go new file mode 100644 index 0000000..4216de3 --- /dev/null +++ b/database.go @@ -0,0 +1,62 @@ +package sqlingo + +import ( + "database/sql" + "golang.org/x/text/language" + "golang.org/x/text/message" + "time" +) + +type Database struct { + db *sql.DB + debugMode bool + dialect string +} + +func (d *Database) SetDebugMode(debugMode bool) { + d.debugMode = debugMode +} + +func Open(driverName string, dataSourceName string) (db *Database, err error) { + var sqlDB *sql.DB + if dataSourceName != "" { + sqlDB, err = sql.Open(driverName, dataSourceName) + if err != nil { + return + } + } + db = &Database{ + dialect: driverName, + db: sqlDB, + } + return +} + +func (d *Database) GetDB() *sql.DB { + return d.db +} + +func (d *Database) Query(sql string) (Cursor, error) { + startTime := time.Now().UnixNano() + rows, err := d.db.Query(sql) + endTime := time.Now().UnixNano() + printLog(endTime-startTime, sql) + if err != nil { + return nil, err + } + return &cursor{rows: rows}, nil +} + +func (d *Database) Execute(sql string) (sql.Result, error) { + startTime := time.Now().UnixNano() + result, err := d.db.Exec(sql) + endTime := time.Now().UnixNano() + printLog(endTime-startTime, sql) + return result, err +} + +var printer = message.NewPrinter(language.English) + +func printLog(duration int64, sql string) { + printer.Printf("[%9d µs] %s\n", duration/1000, sql) +} diff --git a/delete.go b/delete.go new file mode 100644 index 0000000..733e9e1 --- /dev/null +++ b/delete.go @@ -0,0 +1,52 @@ +package sqlingo + +import ( + "database/sql" +) + +type DeleteWithTable interface { + Where(conditions ...BooleanExpression) DeleteWithWhere +} + +type DeleteWithWhere interface { + GetSQL() (string, error) + Execute() (result sql.Result, err error) +} + +type deleteStatus struct { + database *Database + table *Table + where *BooleanExpression +} + +func (s *deleteStatus) copy() *deleteStatus { + delete_ := *s + return &delete_ +} + +func (d *Database) DeleteFrom(table Table) DeleteWithTable { + return &deleteStatus{database: d, table: &table} +} + +func (s *deleteStatus) Where(conditions ...BooleanExpression) DeleteWithWhere { + delete_ := s.copy() + condition := And(conditions...) + delete_.where = &condition + return delete_ +} + +func (s *deleteStatus) GetSQL() (string, error) { + sqlString := getCallerInfo() + "DELETE FROM " + (*s.table).GetSQL() + " WHERE " + (*s.where).GetSQL() + + + + return sqlString, nil +} + +func (s *deleteStatus) Execute() (sql.Result, error) { + sqlString, err := s.GetSQL() + if err != nil { + return nil, err + } + return s.database.Execute(sqlString) +} diff --git a/expression.go b/expression.go new file mode 100644 index 0000000..8fac0b3 --- /dev/null +++ b/expression.go @@ -0,0 +1,272 @@ +package sqlingo + +import ( + "reflect" + "strconv" + "strings" +) + +type Expression interface { + GetSQL() string + getOperatorPriority() int + + NotEquals(other interface{}) BooleanExpression + Equals(other interface{}) BooleanExpression + LessThan(other interface{}) BooleanExpression + LessThanOrEquals(other interface{}) BooleanExpression + GreaterThan(other interface{}) BooleanExpression + GreaterThanOrEquals(other interface{}) BooleanExpression + + IsNull() BooleanExpression + IsNotNull() BooleanExpression + In(values ...interface{}) BooleanExpression + Between(min interface{}, max interface{}) BooleanExpression + Desc() OrderBy + + As(alias string) Alias +} + +type BooleanExpression interface { + Expression + And(other interface{}) BooleanExpression + Or(other interface{}) BooleanExpression + Not() BooleanExpression +} + +type NumberExpression interface { + Expression + Add(other interface{}) NumberExpression + Sub(other interface{}) NumberExpression + Mul(other interface{}) NumberExpression + Div(other interface{}) NumberExpression + IntDiv(other interface{}) NumberExpression + Mod(other interface{}) NumberExpression + + Sum() NumberExpression +} + +type StringExpression interface { + Expression +} + +type UnknownExpression interface { + Expression + And(other interface{}) BooleanExpression + Or(other interface{}) BooleanExpression + Not() BooleanExpression + Add(other interface{}) NumberExpression + Sub(other interface{}) NumberExpression + Mul(other interface{}) NumberExpression + Div(other interface{}) NumberExpression + IntDiv(other interface{}) NumberExpression + Mod(other interface{}) NumberExpression + + Sum() NumberExpression +} + +type expression struct { + sql string + priority int +} + +func (e *expression) As(name string) Alias { + return &alias{expression: e, name: name} +} + +func (e *expression) GetSQL() string { + return e.sql +} + +func getSQLFromWhatever(value interface{}) (sql string, priority int) { + switch value.(type) { + case Expression: + return value.(Expression).GetSQL(), value.(Expression).getOperatorPriority() + case Assignment: + return value.(Assignment).GetSQL(), 0 + case int, int8, int16, int32, int64: + return strconv.FormatInt(reflect.ValueOf(value).Int(), 10), 0 + case uint, uint8, uint16, uint32, uint64: + return strconv.FormatUint(reflect.ValueOf(value).Uint(), 10), 0 + case string: + return "\"" + strings.Replace(value.(string), "\"", "\\\"", -1) + "\"", 0 + case []interface{}: + return "(" + commaValues(value.([]interface{})) + ")", 0 + default: + if value == nil { + return "NULL", 0 + } + v := reflect.ValueOf(value) + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "NULL", 0 + } + return getSQLFromWhatever(reflect.Indirect(v).Interface()) + } + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return getSQLFromWhatever(v.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return getSQLFromWhatever(v.Uint()) + default: + return "[invalid type " + v.Kind().String() + "]", 99 + } + } +} + +/* +1 INTERVAL +2 BINARY, COLLATE +3 ! +4 - (unary minus), ~ (unary bit inversion) +5 ^ +6 *, /, DIV, %, MOD +7 -, + +8 <<, >> +9 & +10 | +11 = (comparison), <=>, >=, >, <=, <, <>, !=, IS, LIKE, REGEXP, IN +12 BETWEEN, CASE, WHEN, THEN, ELSE +13 NOT +14 AND, && +15 XOR +16 OR, || +17 = (assignment), := + */ +func (e *expression) NotEquals(other interface{}) BooleanExpression { + return e.binaryOperation("<>", other, 11) +} + +func (e *expression) Equals(other interface{}) BooleanExpression { + return e.binaryOperation("=", other, 11) +} + +func (e *expression) LessThan(other interface{}) BooleanExpression { + return e.binaryOperation("<", other, 11) +} + +func (e *expression) LessThanOrEquals(other interface{}) BooleanExpression { + return e.binaryOperation("<=", other, 11) +} + +func (e *expression) GreaterThan(other interface{}) BooleanExpression { + return e.binaryOperation(">", other, 11) +} + +func (e *expression) GreaterThanOrEquals(other interface{}) BooleanExpression { + return e.binaryOperation(">=", other, 11) +} + +func (e *expression) And(other interface{}) BooleanExpression { + return e.binaryOperation("AND", other, 14) +} + +func (e *expression) Or(other interface{}) BooleanExpression { + return e.binaryOperation("OR", other, 16) +} + +func (e *expression) Add(other interface{}) NumberExpression { + return e.binaryOperation("+", other, 7) +} + +func (e *expression) Sub(other interface{}) NumberExpression { + return e.binaryOperation("-", other, 7) +} + +func (e *expression) Mul(other interface{}) NumberExpression { + return e.binaryOperation("*", other, 6) +} + +func (e *expression) Div(other interface{}) NumberExpression { + return e.binaryOperation("/", other, 6) +} + +func (e *expression) IntDiv(other interface{}) NumberExpression { + return e.binaryOperation("DIV", other, 6) +} + +func (e *expression) Mod(other interface{}) NumberExpression { + return e.binaryOperation("%", other, 6) +} + +func (e *expression) Sum() NumberExpression { + return e.function("SUM") +} + +func (e *expression) binaryOperation(operator string, value interface{}, priority int) *expression { + left := e.GetSQL() + leftLevel := e.priority + right, rightLevel := getSQLFromWhatever(value) + if leftLevel > priority { + left = "(" + left + ")" + } + if rightLevel >= priority { + right = "(" + right + ")" + } + return &expression{ + sql: left + " " + operator + " " + right, + priority: priority, + } +} + +func (e *expression) function(name string) *expression { + return &expression{sql: name + "(" + e.GetSQL() + ")", priority: 0} +} + +func (e *expression) IsNull() BooleanExpression { + return &expression{sql: e.GetSQL() + " IS NULL", priority: 11} +} + +func (e *expression) Not() BooleanExpression { + return &expression{sql: "NOT " + e.GetSQL(), priority: 13} +} + +func (e *expression) IsNotNull() BooleanExpression { + return &expression{sql: e.GetSQL() + " IS NOT NULL", priority: 11} +} + +func (e *expression) In(values ...interface{}) BooleanExpression { + if len(values) == 1 { + firstValue := values[0] + valueOfFirstValue := reflect.ValueOf(firstValue) + if valueOfFirstValue.Kind() == reflect.Slice { + length := valueOfFirstValue.Len() + values = make([]interface{}, length) + for i := 0; i < length; i++ { + value := valueOfFirstValue.Index(i) + values[i] = value.Interface() + } + } + } + if len(values) == 0 { + return &expression{sql: "FALSE", priority: 0} + } + + sql := e.GetSQL() + " IN (" + for i, value := range values { + if i > 0 { + sql += ", " + } + valueSql, _ := getSQLFromWhatever(value) + sql += valueSql + } + sql += ")" + return &expression{sql: sql, priority: 11} +} + +func (e *expression) Between(min interface{}, max interface{}) BooleanExpression { + minSql, _ := getSQLFromWhatever(min) + maxSql, _ := getSQLFromWhatever(max) + sql := e.GetSQL() + " BETWEEN " + minSql + " AND " + maxSql + return &expression{sql: sql, priority: 12} +} + +func (e *expression) getOperatorPriority() int { + return e.priority +} + +func (e *expression) Desc() OrderBy { + return &orderBy{by: e, desc: true} +} + +func NewExpression(sql string, priority int) { +} diff --git a/field.go b/field.go new file mode 100644 index 0000000..90658f7 --- /dev/null +++ b/field.go @@ -0,0 +1,37 @@ +package sqlingo + +type Field interface { + Expression +} + +type NumberField interface { + NumberExpression +} + +type BooleanField interface { + BooleanExpression +} + +type StringField interface { + StringExpression +} + +func newFieldExpression(tableName string, fieldName string) *expression { + sql := getSQLForName(fieldName) + if tableName != "" { + sql = getSQLForName(tableName) + "." + sql + } + return &expression{sql: sql, priority: 0} +} + +func NewNumberField(tableName string, fieldName string) NumberField { + return newFieldExpression(tableName, fieldName) +} + +func NewBooleanField(tableName string, fieldName string) BooleanField { + return newFieldExpression(tableName, fieldName) +} + +func NewStringField(tableName string, fieldName string) StringField { + return newFieldExpression(tableName, fieldName) +} diff --git a/generator.go b/generator.go new file mode 100644 index 0000000..89d85c6 --- /dev/null +++ b/generator.go @@ -0,0 +1,213 @@ +package sqlingo + +import ( + "database/sql" + "errors" + "go/format" + "regexp" + "strconv" + "unicode" +) + +func convertCase(s string) (result string) { + nextCharShouldBeUpperCase := true + for _, ch := range s { + if ch == '_' { + nextCharShouldBeUpperCase = true + } else { + if nextCharShouldBeUpperCase { + result += string(unicode.ToUpper(ch)) + nextCharShouldBeUpperCase = false + } else { + result += string(ch) + } + } + } + return +} + +func getType(s string, nullable bool) (goType string, fieldClass string, err error) { + r, _ := regexp.Compile("([a-z]+)(\\(([0-9]+)\\))?") + + submatches := r.FindStringSubmatch(s) + fieldType := submatches[1] + fieldSize := 0 + if submatches[3] != "" { + fieldSize, err = strconv.Atoi(submatches[3]) + if err != nil { + return + } + } + switch fieldType { + case "tinyint": + goType = "int8" + fieldClass = "NumberField" + case "smallint": + goType = "int16" + fieldClass = "NumberField" + case "int": + goType = "int32" + fieldClass = "NumberField" + case "bigint": + goType = "int64" + fieldClass = "NumberField" + case "float", "double": + goType = "float64" + fieldClass = "NumberField" + case "char", "varchar", "text", "mediumtext", "longtext", "enum", "datetime": + goType = "string" + fieldClass = "StringField" + case "blob": + goType = "[]byte" + fieldClass = "StringField" + case "bit": + if fieldSize == 1 { + goType = "bool" + fieldClass = "BooleanField" + } else { + goType = "string" + fieldClass = "StringField" + } + } + if nullable { + goType = "*" + goType + } + return +} + +func wrapQuote(s string) string { + return "\"" + s + "\"" +} + +func Generate(driverName string, dataSourceName string, tableNames *[]string) (string, error) { + + mysql, err := sql.Open(driverName, dataSourceName) + if err != nil { + return "", err + } + + rows, err := mysql.Query("SELECT DATABASE()") + if err != nil { + return "", err + } + var dbName *string + if rows.Next() { + err := rows.Scan(&dbName) + if err != nil { + return "", err + } + } + + if dbName == nil { + return "", errors.New("no database selected") + } + + code := "package " + *dbName + "_dsl\n" + code += "import . \"github.com/lqs/sqlingo\"\n" + + if tableNames == nil { + tableNames = &[]string{} + rows, err := mysql.Query("SHOW TABLES") + if err != nil { + return "", err + } + for rows.Next() { + var name string + rows.Scan(&name) + *tableNames = append(*tableNames, name) + } + } + + for _, tableName := range *tableNames { + rows, err := mysql.Query("SHOW FULL COLUMNS FROM " + getSQLForName(tableName)) + if err != nil { + return "", err + } + + tableLines := "" + modelLines := "" + objectLines := "\tTable: NewTable(\"" + tableName + "\"),\n" + classLines := "" + + className := convertCase(tableName) + tableStructName := "t" + className + + modelClassName := className + "Model" + + fields := "" + values := "" + + for rows.Next() { + columns, err := rows.Columns() + if err != nil { + return "", err + } + var pointers []interface{} + for i := 0; i < len(columns); i++ { + var value *string + pointers = append(pointers, &value) + } + err = rows.Scan(pointers...) + if err != nil { + return "", err + } + row := make(map[string]string) + for i, column := range columns { + pointer := *pointers[i].(**string) + if pointer != nil { + row[column] = *pointer + } + } + + goName := convertCase(row["Field"]) + goType, fieldClass, _ := getType(row["Type"], row["Null"] == "YES") + + fieldStructName := "f" + className + goName + + tableLines += "\t" + goName + " *" + fieldStructName + "\n" + modelLines += "\t" + goName + " " + goType + "\n" + objectLines += "\t" + goName + ": &" + fieldStructName + "{" + objectLines += "New" + fieldClass + "(" + wrapQuote(tableName) + ", " + wrapQuote(row["Field"]) + ")},\n" + classLines += "type " + fieldStructName + " struct{ " + fieldClass + " }\n" + + fields += "t." + goName + ", " + values += "m." + goName + ", " + } + + //println(tableLines, recordLines, objectLines, classLines) + + code += "type " + tableStructName + " struct {\n\tTable\n" + code += tableLines + code += "}\n\n" + + code += classLines + + code += "var " + className + " = &" + tableStructName + "{\n" + code += objectLines + code += "}\n\n" + + code += "func (t t" + className + ") GetFields() []Field {\n" + code += "\treturn []Field{" + fields + "}\n" + code += "}\n\n" + + code += "type " + modelClassName + " struct {\n" + //code += "\tModel\n" + code += modelLines + code += "}\n\n" + + code += "func (m " + modelClassName + ") GetTable() Table {\n" + code += "\treturn " + className + "\n" + code += "}\n\n" + + code += "func (m " + modelClassName + ") GetValues() []interface{} {\n" + code += "\treturn []interface{}{" + values + "}\n" + code += "}\n\n" + } + + codeOut, err := format.Source([]byte(code)) + if err != nil { + return "", err + } + + return string(codeOut), nil +} diff --git a/insert.go b/insert.go new file mode 100644 index 0000000..739bd27 --- /dev/null +++ b/insert.go @@ -0,0 +1,147 @@ +package sqlingo + +import ( + "database/sql" + "reflect" +) + +type insertStatus struct { + database *Database + table *Table + fields []Field + values []interface{} + models []Model + onDuplicateKeyUpdateAssignments []assignment +} + +func (s *insertStatus) copy() *insertStatus { + insert := *s + s.fields = append([]Field{}, s.fields...) + s.values = append([]interface{}{}, s.values...) + s.onDuplicateKeyUpdateAssignments = append([]assignment{}, s.onDuplicateKeyUpdateAssignments...) + return &insert +} + +type InsertWithTable interface { + Fields(fields ... Field) InsertWithValues + Values(values ... interface{}) InsertWithValues + Models(models ... interface{}) InsertWithModels +} + +type InsertWithValues interface { + Values(values ... interface{}) InsertWithValues + OnDuplicateKeyUpdate() InsertWithOnDuplicateKeyUpdateBegin + Execute() (result sql.Result, err error) +} + +type InsertWithModels interface { + Models(models ... interface{}) InsertWithModels + OnDuplicateKeyUpdate() InsertWithOnDuplicateKeyUpdateBegin + GetSQL() (string, error) + Execute() (result sql.Result, err error) +} + +type InsertWithOnDuplicateKeyUpdateBegin interface { + Set(Field Field, value interface{}) InsertWithOnDuplicateKeyUpdate +} + +type InsertWithOnDuplicateKeyUpdate interface { + Set(Field Field, value interface{}) InsertWithOnDuplicateKeyUpdate + GetSQL() (string, error) + Execute() (result sql.Result, err error) +} + +func (d *Database) InsertInto(table Table) InsertWithTable { + return &insertStatus{database: d, table: &table} +} + +func (s *insertStatus) Fields(fields ... Field) InsertWithValues { + insert := s.copy() + insert.fields = fields + return insert +} + +func (s *insertStatus) Values(values ... interface{}) InsertWithValues { + insert := s.copy() + insert.values = append(insert.values, values) + return insert +} + +func (s *insertStatus) addModel(model interface{}) { + m0, ok := model.(Model) + if ok { + s.models = append(s.models, m0) + return + } + + value := reflect.ValueOf(model) + if value.Kind() == reflect.Ptr { + value = reflect.Indirect(value) + s.addModel(value.Interface()) + } + if value.Kind() == reflect.Slice { + for i := 0; i < value.Len(); i++ { + elem := value.Index(i) + addr := elem.Addr() + inter := addr.Interface() + s.addModel(inter) + } + return + } +} + +func (s *insertStatus) Models(models ... interface{}) InsertWithModels { + if len(models) == 0 { + return s + } + + insert := s.copy() + for _, model := range models { + insert.addModel(&model) + } + return insert +} + +func (s *insertStatus) OnDuplicateKeyUpdate() InsertWithOnDuplicateKeyUpdateBegin { + insert := s.copy() + return insert +} + +func (s *insertStatus) Set(field Field, value interface{}) InsertWithOnDuplicateKeyUpdate { + insert := s.copy() + insert.onDuplicateKeyUpdateAssignments = append(insert.onDuplicateKeyUpdateAssignments, assignment{ + field: field, + value: value, + }) + return insert +} + +func (s *insertStatus) GetSQL() (string, error) { + var fields []Field + var values []interface{} + if len(s.models) > 0 { + fields = s.models[0].GetTable().GetFields() + for _, model := range s.models { + values = append(values, model.GetValues()) + } + } else { + fields = s.fields + values = s.values + } + + sqlString := getCallerInfo() + "INSERT INTO " + (*s.table).GetSQL() + " (" + commaFields(fields) + ") VALUES " + commaValues(values) + if len(s.onDuplicateKeyUpdateAssignments) > 0 { + sqlString += " ON DUPLICATE KEY UPDATE " + commaAssignments(s.onDuplicateKeyUpdateAssignments) + } + + return sqlString, nil + +} + +func (s *insertStatus) Execute() (result sql.Result, err error) { + sqlString, err := s.GetSQL() + if err != nil { + return nil, err + } + return s.database.Execute(sqlString) +} diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..eed4c18 Binary files /dev/null and b/logo.png differ diff --git a/order.go b/order.go new file mode 100644 index 0000000..6c80587 --- /dev/null +++ b/order.go @@ -0,0 +1,18 @@ +package sqlingo + +type OrderBy interface { + GetSQL() string +} + +type orderBy struct { + by Expression + desc bool +} + +func (o *orderBy) GetSQL() string { + sql := o.by.GetSQL() + if o.desc { + sql += " DESC" + } + return sql +} diff --git a/select.go b/select.go new file mode 100644 index 0000000..53d76ea --- /dev/null +++ b/select.go @@ -0,0 +1,250 @@ +package sqlingo + +import ( + "reflect" + "strconv" +) + +type Select interface { + GetFields() []Field + GetSQL() string + Fetch(out ...interface{}) error + FetchCursor() (Cursor, error) +} + +type SelectWithFields interface { + Select + From(tables ...Table) SelectWithTables +} + +type SelectWithTables interface { + Select + SelectOrderBy + Where(conditions ...BooleanExpression) SelectWithWhere + GroupBy(expressions ...Expression) SelectWithGroupBy +} + +type SelectWithWhere interface { + Select + SelectOrderBy + + GroupBy(expressions ...Expression) SelectWithGroupBy +} + +type SelectWithGroupBy interface { + Select + Having(conditions ...BooleanExpression) SelectWithGroupByHaving + OrderBy(orderBys ...OrderBy) SelectWithOrder +} + +type SelectWithGroupByHaving interface { + SelectWithOrder + OrderBy(orderBys ...OrderBy) SelectWithOrder +} + +type SelectOrderBy interface { + OrderBy(orderBys ...OrderBy) SelectWithOrder +} + +type SelectWithOrder interface { + Limit(limit int) SelectWithLimit + Fetch(out ...interface{}) error + FetchCursor() (Cursor, error) +} + +type SelectWithLimit interface { + Offset(offset int) SelectWithOffset + Fetch(out ...interface{}) error + FetchCursor() (Cursor, error) +} + +type SelectWithOffset interface { + Fetch(out ...interface{}) error + FetchCursor() (Cursor, error) +} + +type selectStatus struct { + database *Database + fields []Field + tables []*Table + where *BooleanExpression + orderBys []OrderBy + groupBys []Expression + having *BooleanExpression + limit *int + offset *int + lock string +} + +func (s *selectStatus) copy() *selectStatus { + select_ := *s + select_.fields = s.GetFields() + return &select_ +} + +func (s *selectStatus) GetFields() []Field { + var fields []Field + fields = append(fields, s.fields...) + return fields +} + +func (d *Database) Select(fields ... interface{}) SelectWithFields { + select_ := &selectStatus{database: d} + for _, field := range fields { + sql, priority := getSQLFromWhatever(field) + expression := &expression{sql: sql, priority: priority} + select_.fields = append(select_.fields, expression) + } + return select_ +} + +func (s *selectStatus) From(tables ...Table) SelectWithTables { + select_ := s.copy() + for _, table := range tables { + select_.tables = append(select_.tables, &table) + } + return select_ +} + +func (d *Database) SelectFrom(tables ...Table) SelectWithTables { + select_ := selectStatus{database: d} + for _, table := range tables { + select_.tables = append(select_.tables, &table) + fields := table.GetFields() + for _, field := range fields { + select_.fields = append(select_.fields, field) + } + } + + return &select_ +} + +func (s *selectStatus) Where(conditions ...BooleanExpression) SelectWithWhere { + select_ := s.copy() + condition := And(conditions...) + select_.where = &condition + return select_ +} + +func (s *selectStatus) GroupBy(expressions ...Expression) SelectWithGroupBy { + select_ := s.copy() + select_.groupBys = expressions + return select_ +} + +func (s *selectStatus) Having(conditions ...BooleanExpression) SelectWithGroupByHaving { + select_ := s.copy() + condition := And(conditions...) + select_.having = &condition + return select_ +} + +func (s *selectStatus) OrderBy(orderBys ...OrderBy) SelectWithOrder { + select_ := s.copy() + select_.orderBys = append(select_.orderBys, orderBys...) + return select_ +} + +func (s *selectStatus) Limit(limit int) SelectWithLimit { + select_ := s.copy() + select_.limit = &limit + return select_ +} + +func (s *selectStatus) Offset(offset int) SelectWithOffset { + select_ := s.copy() + select_.limit = &offset + return select_ +} + +func (s *selectStatus) GetSQL() string { + sql := getCallerInfo() + "SELECT " + commaFields(s.fields) + + if len(s.tables) > 0 { + var values []interface{} + for _, table := range s.tables { + values = append(values, table) + } + sql += " FROM " + commaValues(values) + } + + if s.where != nil { + sql += " WHERE " + (*s.where).GetSQL() + } + + if len(s.groupBys) != 0 { + sql += " GROUP BY " + commaExpressions(s.groupBys) + + if s.having != nil { + sql += " HAVING " + (*s.having).GetSQL() + } + } + + if len(s.orderBys) > 0 { + sql += " ORDER BY " + commaOrderBys(s.orderBys) + } + + if s.limit != nil { + sql += " LIMIT " + strconv.Itoa(*s.limit) + } + + if s.offset != nil { + sql += " OFFSET " + strconv.Itoa(*s.offset) + } + + sql += s.lock + + return sql +} + +func (s *selectStatus) FetchCursor() (Cursor, error) { + sqlString := s.GetSQL() + + cursor, err := s.database.Query(sqlString) + if err != nil { + return nil, err + } + return cursor, nil +} + +func (s *selectStatus) Fetch(dest ...interface{}) error { + if len(dest) == 1 { + if reflect.ValueOf(dest[0]).Kind() == reflect.Ptr { + val := reflect.Indirect(reflect.ValueOf(dest[0])) + if val.Kind() == reflect.Slice { + cursor, err := s.FetchCursor() + if err != nil { + return err + } + defer cursor.Close() + + for cursor.Next() { + if err != nil { + return err + } + elem := reflect.New(val.Type().Elem()) + row := elem.Interface() + cursor.Scan(row) + val.Set(reflect.Append(val, reflect.Indirect(elem))) + } + return nil + } + } + } + + cursor, err := s.FetchCursor() + if err != nil { + return err + } + defer cursor.Close() + + for cursor.Next() { + err = cursor.Scan(dest...) + if err != nil { + return err + } + break + } + + return nil +} diff --git a/sqlingo-gen/main.go b/sqlingo-gen/main.go new file mode 100644 index 0000000..abd734c --- /dev/null +++ b/sqlingo-gen/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + _ "github.com/go-sql-driver/mysql" + "github.com/lqs/sqlingo" + "os" +) + +func main() { + + if len(os.Args) != 2 { + fmt.Printf("Usage: %s username:password@/database\n", os.Args[0]) + return + } + + dataSourceName := os.Args[1] + + code, err := sqlingo.Generate("mysql", dataSourceName, nil) + if err != nil { + panic(err) + } + + fmt.Print(code) +} + diff --git a/table.go b/table.go new file mode 100644 index 0000000..1de8a0c --- /dev/null +++ b/table.go @@ -0,0 +1,29 @@ +package sqlingo + +type Table interface { + GetName() string + GetSQL() string + GetFields() []Field +} + +type table struct { + Table + name string + sql string +} + +func (t *table) GetName() string { + return t.name +} + +func (t *table) GetSQL() string { + return t.sql +} + +func (t *table) getOperatorPriority() int { + return 0 +} + +func NewTable(name string) Table { + return &table{name: name, sql: getSQLForName(name)} +} diff --git a/update.go b/update.go new file mode 100644 index 0000000..1973018 --- /dev/null +++ b/update.go @@ -0,0 +1,80 @@ +package sqlingo + +import ( + "database/sql" +) + +type updateStatus struct { + database *Database + table *Table + assignments []assignment + where *BooleanExpression +} + +func (s *updateStatus) copy() *updateStatus { + update := *s + update.assignments = append([]assignment{}, s.assignments...) + return &update +} + +func (d *Database) Update(table Table) UpdateWithTable { + return &updateStatus{database: d, table: &table} +} + +type UpdateWithTable interface { + Set(field Field, value interface{}) UpdateWithSet + SetMap(map[Field]interface{}) UpdateWithSet +} + +type UpdateWithSet interface { + Set(Field Field, value interface{}) UpdateWithSet + Where(conditions ...BooleanExpression) UpdateWithWhere +} + +type UpdateWithWhere interface { + GetSQL() (string, error) + Execute() (sql.Result, error) +} + +func (s *updateStatus) Set(field Field, value interface{}) UpdateWithSet { + update := s.copy() + update.assignments = append(update.assignments, assignment{ + field: field, + value: value, + }) + return update +} + +func (s *updateStatus) SetMap(values map[Field]interface{}) UpdateWithSet { + update := s.copy() + for field, value := range values { + update.assignments = append(update.assignments, assignment{ + field: field, + value: value, + }) + } + return update +} + +func (s *updateStatus) Where(conditions ...BooleanExpression) UpdateWithWhere { + update := s.copy() + condition := And(conditions...) + update.where = &condition + return update +} + +func (s *updateStatus) GetSQL() (string, error) { + sqlString := getCallerInfo() + "UPDATE " + (*s.table).GetSQL() + + " SET " + commaAssignments(s.assignments) + + " WHERE " + (*s.where).GetSQL() + + return sqlString, nil +} + +func (s *updateStatus) Execute() (sql.Result, error) { + sqlString, err := s.GetSQL() + if err != nil { + return nil, err + } + return s.database.Execute(sqlString) +}