diff --git a/bob.go b/bob.go index a8a4470..18d48d2 100644 --- a/bob.go +++ b/bob.go @@ -40,6 +40,16 @@ func (b BobBuilderType) CreateTableIfNotExists(table string) CreateBuilder { return CreateBuilder(b).name(table).ifNotExists() } +// CreateIndex creates an index with CreateIndexBuilder interface. +func (b BobBuilderType) CreateIndex(name string) IndexBuilder { + return IndexBuilder(b).name(name) +} + +// CreateIndexIfNotExists creates an index with CreateIndexBuilder interface, if the index doesn't exists. +func (b BobBuilderType) CreateIndexIfNotExists(name string) IndexBuilder { + return IndexBuilder(b).name(name).ifNotExists() +} + // HasTable checks if a table exists with HasBuilder interface func (b BobBuilderType) HasTable(table string) HasBuilder { return HasBuilder(b).HasTable(table) @@ -172,3 +182,13 @@ func Truncate(table string) TruncateBuilder { func Upsert(table string, dialect int) UpsertBuilder { return BobStmtBuilder.Upsert(table, dialect) } + +// CreateIndex creates an index with CreateIndexBuilder interface. +func CreateIndex(name string) IndexBuilder { + return BobStmtBuilder.CreateIndex(name) +} + +// CreateIndexIfNotExists creates an index with CreateIndexBuilder interface, if the index doesn't exists. +func CreateIndexIfNotExists(name string) IndexBuilder { + return BobStmtBuilder.CreateIndexIfNotExists(name) +} diff --git a/create_index.go b/create_index.go new file mode 100644 index 0000000..3885361 --- /dev/null +++ b/create_index.go @@ -0,0 +1,127 @@ +package bob + +import ( + "errors" + "strings" + + "github.com/lann/builder" +) + +type IndexBuilder builder.Builder + +type indexData struct { + Unique bool + Spatial bool + Fulltext bool + Name string + TableName string + Columns []IndexColumn + IfNotExists bool +} + +type IndexColumn struct { + Name string + Extras []string + Collate string +} + +func init() { + builder.Register(IndexBuilder{}, indexData{}) +} + +func (i IndexBuilder) Unique() IndexBuilder { + return builder.Set(i, "Unique", true).(IndexBuilder) +} + +func (i IndexBuilder) Spatial() IndexBuilder { + return builder.Set(i, "Spatial", true).(IndexBuilder) +} + +func (i IndexBuilder) Fulltext() IndexBuilder { + return builder.Set(i, "Fulltext", true).(IndexBuilder) +} + +func (i IndexBuilder) name(name string) IndexBuilder { + return builder.Set(i, "Name", name).(IndexBuilder) +} + +func (i IndexBuilder) ifNotExists() IndexBuilder { + return builder.Set(i, "IfNotExists", true).(IndexBuilder) +} + +func (i IndexBuilder) On(table string) IndexBuilder { + return builder.Set(i, "TableName", table).(IndexBuilder) +} + +func (i IndexBuilder) Columns(column IndexColumn) IndexBuilder { + return builder.Append(i, "Columns", column).(IndexBuilder) +} + +func (i IndexBuilder) ToSql() (string, []interface{}, error) { + data := builder.GetStruct(i).(indexData) + return data.ToSql() +} + +func (i *indexData) ToSql() (sqlStr string, args []interface{}, err error) { + if i.Name == "" { + err = errors.New("index name is required on create index statement") + return + } + if i.TableName == "" { + err = errors.New("a table name must be specified on create index statement") + return + } + + if len(i.Columns) == 0 { + err = errors.New("should at least specify one column for create index statement") + return + } + + var sql strings.Builder + + sql.WriteString("CREATE ") + + if i.Unique { + sql.WriteString("UNIQUE ") + } + + if i.Fulltext { + sql.WriteString("FULLTEXT ") + } + + if i.Spatial { + sql.WriteString("SPATIAL ") + } + + sql.WriteString("INDEX ") + + if i.IfNotExists { + sql.WriteString("IF NOT EXISTS ") + } + + sql.WriteString(i.Name + " ") + + sql.WriteString("ON ") + + sql.WriteString(i.TableName + " ") + + var columns []string + for _, column := range i.Columns { + var colBuilder strings.Builder + colBuilder.WriteString(column.Name) + if column.Collate != "" { + colBuilder.WriteString(" COLLATE " + column.Collate) + } + if len(column.Extras) > 0 { + colBuilder.WriteString(" " + strings.Join(column.Extras, " ")) + } + columns = append(columns, colBuilder.String()) + } + + sql.WriteString("(") + sql.WriteString(strings.Join(columns, ", ")) + sql.WriteString(");") + + sqlStr = sql.String() + return +} diff --git a/create_index_test.go b/create_index_test.go new file mode 100644 index 0000000..ef32966 --- /dev/null +++ b/create_index_test.go @@ -0,0 +1,91 @@ +package bob_test + +import ( + "testing" + + "github.com/aldy505/bob" +) + +func TestCreateIndex(t *testing.T) { + sql, _, err := bob. + CreateIndexIfNotExists("email_idx"). + On("users"). + Unique(). + Spatial(). + Fulltext(). + Columns(bob.IndexColumn{Name: "email"}). + ToSql() + if err != nil { + t.Fatal(err.Error()) + } + + result := "CREATE UNIQUE FULLTEXT SPATIAL INDEX IF NOT EXISTS email_idx ON users (email);" + if sql != result { + t.Fatal("sql is not equal to result:", sql) + } +} + +func TestCreateIndex_Simple(t *testing.T) { + sql, _, err := bob. + CreateIndex("idx_email"). + On("users"). + Columns(bob.IndexColumn{Name: "email", Collate: "DEFAULT", Extras: []string{"ASC"}}). + Columns(bob.IndexColumn{Name: "name", Extras: []string{"DESC"}}). + ToSql() + if err != nil { + t.Fatal(err.Error()) + } + + result := "CREATE INDEX idx_email ON users (email COLLATE DEFAULT ASC, name DESC);" + if sql != result { + t.Fatal("sql is not equal to result:", sql) + } +} + +func TestCreateIndex_Error(t *testing.T) { + t.Run("without index", func(t *testing.T) { + _, _, err := bob. + CreateIndex(""). + On("users"). + Columns(bob.IndexColumn{Name: "email", Collate: "DEFAULT", Extras: []string{"ASC"}}). + Columns(bob.IndexColumn{Name: "name", Extras: []string{"DESC"}}). + ToSql() + if err == nil { + t.Fatal("error is nil") + } + + if err.Error() != "index name is required on create index statement" { + t.Fatal("error is not equal to result:", err.Error()) + } + }) + + t.Run("without table name", func(t *testing.T) { + _, _, err := bob. + CreateIndex("name"). + On(""). + Columns(bob.IndexColumn{Name: "email", Collate: "DEFAULT", Extras: []string{"ASC"}}). + Columns(bob.IndexColumn{Name: "name", Extras: []string{"DESC"}}). + ToSql() + if err == nil { + t.Fatal("error is nil") + } + + if err.Error() != "a table name must be specified on create index statement" { + t.Fatal("error is not equal to result:", err.Error()) + } + }) + + t.Run("without columns", func(t *testing.T) { + _, _, err := bob. + CreateIndex("name"). + On("users"). + ToSql() + if err == nil { + t.Fatal("error is nil") + } + + if err.Error() != "should at least specify one column for create index statement" { + t.Fatal("error is not equal to result:", err.Error()) + } + }) +} diff --git a/create.go b/create_table.go similarity index 100% rename from create.go rename to create_table.go diff --git a/create_table_test.go b/create_table_test.go new file mode 100644 index 0000000..0942e90 --- /dev/null +++ b/create_table_test.go @@ -0,0 +1,102 @@ +package bob_test + +import ( + "testing" + + "github.com/aldy505/bob" +) + +func TestCreateTable(t *testing.T) { + sql, _, err := bob. + CreateTable("users"). + UUIDColumn("uuid"). + StringColumn("string"). + TextColumn("text"). + DateColumn("date"). + BooleanColumn("boolean"). + IntegerColumn("integer"). + IntColumn("int"). + TimeStampColumn("timestamp"). + TimeColumn("time"). + DateColumn("date"). + DateTimeColumn("datetime"). + JSONColumn("json"). + JSONBColumn("jsonb"). + BlobColumn("blob"). + RealColumn("real"). + FloatColumn("float"). + AddColumn(bob.ColumnDef{Name: "custom", Type: "custom"}). + ToSql() + if err != nil { + t.Fatal(err.Error()) + } + result := "CREATE TABLE \"users\" (\"uuid\" UUID, \"string\" VARCHAR(255), \"text\" TEXT, \"date\" DATE, \"boolean\" BOOLEAN, \"integer\" INTEGER, \"int\" INT, \"timestamp\" TIMESTAMP, \"time\" TIME, \"date\" DATE, \"datetime\" DATETIME, \"json\" JSON, \"jsonb\" JSONB, \"blob\" BLOB, \"real\" REAL, \"float\" FLOAT, \"custom\" custom);" + if sql != result { + t.Fatal("sql is not equal to result:", sql) + } +} + +func TestCreateTable_Extras(t *testing.T) { + sql, _, err := bob.CreateTable("users"). + UUIDColumn("id", "PRIMARY KEY"). + StringColumn("email", "NOT NULL", "UNIQUE"). + ToSql() + + if err != nil { + t.Fatal(err.Error()) + } + result := "CREATE TABLE \"users\" (\"id\" UUID PRIMARY KEY, \"email\" VARCHAR(255) NOT NULL UNIQUE);" + if sql != result { + t.Fatal("sql is not equal to result:", sql) + } +} + +func TestCreateTable_Schema(t *testing.T) { + sql, _, err := bob. + CreateTable("users"). + WithSchema("private"). + StringColumn("name"). + ToSql() + if err != nil { + t.Fatal(err.Error()) + } + result := "CREATE TABLE \"private\".\"users\" (\"name\" VARCHAR(255));" + if sql != result { + t.Fatal("sql is not equal to result:", sql) + } +} + +func TestCreateTable_Error(t *testing.T) { + t.Run("should emit error on empty table name", func(t *testing.T) { + _, _, err := bob. + CreateTable(""). + StringColumn("name"). + ToSql() + if err.Error() != "create statements must specify a table" { + t.Fatal("should throw an error, it didn't:", err.Error()) + } + }) + + t.Run("should emit error if no column were specified", func(t *testing.T) { + _, _, err := bob. + CreateTable("users"). + ToSql() + if err.Error() != "a table should at least have one column" { + t.Fatal("should throw an error, it didn't:", err.Error()) + } + }) +} + +func TestCreateTable_IfNotExists(t *testing.T) { + sql, _, err := bob. + CreateTableIfNotExists("users"). + TextColumn("name"). + ToSql() + if err != nil { + t.Fatal(err.Error()) + } + result := "CREATE TABLE IF NOT EXISTS \"users\" (\"name\" TEXT);" + if sql != result { + t.Fatal("sql is not equal to result: ", sql) + } +} diff --git a/create_test.go b/create_test.go deleted file mode 100644 index 3c92425..0000000 --- a/create_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package bob_test - -import ( - "testing" - - "github.com/aldy505/bob" -) - -func TestCreate(t *testing.T) { - t.Run("should return correct sql string with all columns and types", func(t *testing.T) { - sql, _, err := bob. - CreateTable("users"). - UUIDColumn("uuid"). - StringColumn("string"). - TextColumn("text"). - DateColumn("date"). - BooleanColumn("boolean"). - IntegerColumn("integer"). - IntColumn("int"). - TimeStampColumn("timestamp"). - TimeColumn("time"). - DateColumn("date"). - DateTimeColumn("datetime"). - JSONColumn("json"). - JSONBColumn("jsonb"). - BlobColumn("blob"). - RealColumn("real"). - FloatColumn("float"). - AddColumn(bob.ColumnDef{Name: "custom", Type: "custom"}). - ToSql() - if err != nil { - t.Fatal(err.Error()) - } - result := "CREATE TABLE \"users\" (\"uuid\" UUID, \"string\" VARCHAR(255), \"text\" TEXT, \"date\" DATE, \"boolean\" BOOLEAN, \"integer\" INTEGER, \"int\" INT, \"timestamp\" TIMESTAMP, \"time\" TIME, \"date\" DATE, \"datetime\" DATETIME, \"json\" JSON, \"jsonb\" JSONB, \"blob\" BLOB, \"real\" REAL, \"float\" FLOAT, \"custom\" custom);" - if sql != result { - t.Fatal("sql is not equal to result:", sql) - } - }) - - t.Run("should return correct sql with extras", func(t *testing.T) { - sql, _, err := bob.CreateTable("users"). - UUIDColumn("id", "PRIMARY KEY"). - StringColumn("email", "NOT NULL", "UNIQUE"). - ToSql() - - if err != nil { - t.Fatal(err.Error()) - } - result := "CREATE TABLE \"users\" (\"id\" UUID PRIMARY KEY, \"email\" VARCHAR(255) NOT NULL UNIQUE);" - if sql != result { - t.Fatal("sql is not equal to result:", sql) - } - }) - - t.Run("should be able to have a schema name", func(t *testing.T) { - sql, _, err := bob. - CreateTable("users"). - WithSchema("private"). - StringColumn("name"). - ToSql() - if err != nil { - t.Fatal(err.Error()) - } - result := "CREATE TABLE \"private\".\"users\" (\"name\" VARCHAR(255));" - if sql != result { - t.Fatal("sql is not equal to result:", sql) - } - }) - - t.Run("should emit error on empty table name", func(t *testing.T) { - _, _, err := bob. - CreateTable(""). - StringColumn("name"). - ToSql() - if err.Error() != "create statements must specify a table" { - t.Fatal("should throw an error, it didn't:", err.Error()) - } - }) - - t.Run("should emit error if no column were specified", func(t *testing.T) { - _, _, err := bob. - CreateTable("users"). - ToSql() - if err.Error() != "a table should at least have one column" { - t.Fatal("should throw an error, it didn't:", err.Error()) - } - }) - - t.Run("should emit create if not exists", func(t *testing.T) { - sql, _, err := bob. - CreateTableIfNotExists("users"). - TextColumn("name"). - ToSql() - if err != nil { - t.Fatal(err.Error()) - } - result := "CREATE TABLE IF NOT EXISTS \"users\" (\"name\" TEXT);" - if sql != result { - t.Fatal("sql is not equal to result: ", sql) - } - }) -} diff --git a/upsert_test.go b/upsert_test.go index 317ae8a..3dcb52d 100644 --- a/upsert_test.go +++ b/upsert_test.go @@ -189,10 +189,10 @@ func TestUpsert_WithoutReplacePlaceHolder(t *testing.T) { if err != nil { t.Error(err) } - + desiredSql := "IF NOT EXISTS (SELECT * FROM \"users\" WHERE \"email\" = @p1) INSERT INTO \"users\" (\"name\", \"email\") VALUES (@p2, @p3) ELSE UPDATE \"users\" SET \"name\" = @p4 WHERE \"email\" = @p5;" desiredArgs := []interface{}{"john@doe.com", "John Doe", "john@doe.com", "John Does", "john@doe.com"} - + if sql != desiredSql { t.Error("sql is not the same as result: ", sql) } @@ -200,7 +200,7 @@ func TestUpsert_WithoutReplacePlaceHolder(t *testing.T) { t.Error("args is not the same as result: ", args) } }) - + t.Run("SQLite", func(t *testing.T) { sql, args, err := bob. Upsert("users", bob.SQLite). @@ -223,4 +223,4 @@ func TestUpsert_WithoutReplacePlaceHolder(t *testing.T) { t.Error("args is not the same as result: ", args) } }) -} \ No newline at end of file +}