diff --git a/git/blobs_test.go b/git/blobs_test.go index c0e22478b..037a6c326 100644 --- a/git/blobs_test.go +++ b/git/blobs_test.go @@ -3,38 +3,41 @@ package git import ( "testing" + "github.com/gitql/gitql/sql" + "github.com/stretchr/testify/assert" - "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/fixtures" ) -func TestBlobsTable(t *testing.T) { +func TestBlobsTable_Name(t *testing.T) { assert := assert.New(t) f := fixtures.Basic().One() - r, err := git.NewFilesystemRepository(f.DotGit().Base()) - assert.Nil(err) + table := getTable(assert, f, blobsTableName) + assert.Equal(blobsTableName, table.Name()) +} - db := NewDatabase("foo", r) - assert.NotNil(db) +func TestBlobsTable_Children(t *testing.T) { + assert := assert.New(t) - tables := db.Tables() - table, ok := tables[blobsTableName] - assert.True(ok) - assert.NotNil(table) - assert.Equal(blobsTableName, table.Name()) + f := fixtures.Basic().One() + table := getTable(assert, f, blobsTableName) assert.Equal(0, len(table.Children())) +} - iter, err := table.RowIter() - assert.Nil(err) - assert.NotNil(iter) +func TestBlobsTable_RowIter(t *testing.T) { + assert := assert.New(t) + + f := fixtures.Basic().One() + table := getTable(assert, f, blobsTableName) - row, err := iter.Next() + rows, err := sql.NodeToRows(table) assert.Nil(err) - assert.NotNil(row) + assert.Len(rows, 10) - fields := row.Fields() - assert.NotNil(fields) - assert.IsType("", fields[0]) - assert.IsType(int64(0), fields[1]) + schema := table.Schema() + for idx, row := range rows { + err := schema.CheckRow(row) + assert.Nil(err, "row %d doesn't conform to schema", idx) + } } diff --git a/git/commits.go b/git/commits.go index d0c294151..7e0a2f6e7 100644 --- a/git/commits.go +++ b/git/commits.go @@ -27,10 +27,10 @@ func (commitsTable) Schema() sql.Schema { sql.Field{"hash", sql.String}, sql.Field{"author_name", sql.String}, sql.Field{"author_email", sql.String}, - sql.Field{"author_time", sql.Timestamp}, + sql.Field{"author_when", sql.TimestampWithTimezone}, sql.Field{"comitter_name", sql.String}, sql.Field{"comitter_email", sql.String}, - sql.Field{"comitter_time", sql.Timestamp}, + sql.Field{"comitter_when", sql.TimestampWithTimezone}, sql.Field{"message", sql.String}, } } @@ -73,10 +73,10 @@ func commitToRow(c *git.Commit) sql.Row { c.Hash.String(), c.Author.Name, c.Author.Email, - c.Author.When.Unix(), + c.Author.When, c.Committer.Name, c.Committer.Email, - c.Committer.When.Unix(), + c.Committer.When, c.Message, ) } diff --git a/git/commits_test.go b/git/commits_test.go index fbbda0d6c..711a32742 100644 --- a/git/commits_test.go +++ b/git/commits_test.go @@ -3,39 +3,41 @@ package git import ( "testing" + "github.com/gitql/gitql/sql" + "github.com/stretchr/testify/assert" - "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/fixtures" ) -func TestCommitsTable(t *testing.T) { +func TestCommitsTable_Name(t *testing.T) { assert := assert.New(t) f := fixtures.Basic().One() - r, err := git.NewFilesystemRepository(f.DotGit().Base()) - assert.Nil(err) + table := getTable(assert, f, commitsTableName) + assert.Equal(commitsTableName, table.Name()) +} - db := NewDatabase("foo", r) - assert.NotNil(db) +func TestCommitsTable_Children(t *testing.T) { + assert := assert.New(t) - tables := db.Tables() - table, ok := tables[commitsTableName] - assert.True(ok) - assert.NotNil(table) - assert.Equal(commitsTableName, table.Name()) + f := fixtures.Basic().One() + table := getTable(assert, f, commitsTableName) assert.Equal(0, len(table.Children())) +} - iter, err := table.RowIter() - assert.Nil(err) - assert.NotNil(iter) +func TestCommitsTable_RowIter(t *testing.T) { + assert := assert.New(t) + + f := fixtures.Basic().One() + table := getTable(assert, f, commitsTableName) - row, err := iter.Next() + rows, err := sql.NodeToRows(table) assert.Nil(err) - assert.NotNil(row) + assert.Len(rows, 9) - fields := row.Fields() - assert.NotNil(fields) - assert.IsType("", fields[1]) - assert.IsType("", fields[2]) - assert.IsType(int64(0), fields[3]) + schema := table.Schema() + for idx, row := range rows { + err := schema.CheckRow(row) + assert.Nil(err, "row %d doesn't conform to schema", idx) + } } diff --git a/git/database_test.go b/git/database_test.go index 9b091b7fc..b4c113faf 100644 --- a/git/database_test.go +++ b/git/database_test.go @@ -1,8 +1,11 @@ package git import ( + "sort" "testing" + "github.com/gitql/gitql/sql" + "github.com/stretchr/testify/assert" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/fixtures" @@ -12,29 +15,66 @@ func init() { fixtures.RootFolder = "../../../../gopkg.in/src-d/go-git.v4/fixtures/" } +const ( + testDBName = "foo" +) + func TestDatabase_Tables(t *testing.T) { assert := assert.New(t) f := fixtures.Basic().One() - r, err := git.NewFilesystemRepository(f.DotGit().Base()) - assert.Nil(err) - - db := NewDatabase("foo", r) - assert.NotNil(db) + db := getDB(assert, f, testDBName) tables := db.Tables() - _, ok := tables[commitsTableName] - assert.True(ok) + var tableNames []string + for key := range tables { + tableNames = append(tableNames, key) + } + + sort.Strings(tableNames) + expected := []string{ + commitsTableName, + referencesTableName, + treeEntriesTableName, + tagsTableName, + blobsTableName, + } + sort.Strings(expected) + + assert.Equal(expected, tableNames) } func TestDatabase_Name(t *testing.T) { assert := assert.New(t) f := fixtures.Basic().One() - r, err := git.NewFilesystemRepository(f.DotGit().Base()) + db := getDB(assert, f, testDBName) + assert.Equal(testDBName, db.Name()) +} + +func getDB(assert *assert.Assertions, fixture *fixtures.Fixture, + name string) sql.Database { + + r, err := git.NewFilesystemRepository(fixture.DotGit().Base()) assert.Nil(err) - db := NewDatabase("foo", r) + db := NewDatabase(name, r) + assert.NotNil(db) + + return db +} + +func getTable(assert *assert.Assertions, fixture *fixtures.Fixture, + name string) sql.Table { + + db := getDB(assert, fixture, "foo") assert.NotNil(db) assert.Equal(db.Name(), "foo") + + tables := db.Tables() + table, ok := tables[name] + assert.True(ok, "table %s does not exist", table) + assert.NotNil(table) + + return table } diff --git a/git/references_test.go b/git/references_test.go index df1438764..43920dfdb 100644 --- a/git/references_test.go +++ b/git/references_test.go @@ -3,43 +3,41 @@ package git import ( "testing" + "github.com/gitql/gitql/sql" + "github.com/stretchr/testify/assert" - "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/fixtures" ) -func TestReferencesTable(t *testing.T) { +func TestReferencesTable_Name(t *testing.T) { assert := assert.New(t) f := fixtures.Basic().One() - r, err := git.NewFilesystemRepository(f.DotGit().Base()) - assert.Nil(err) + table := getTable(assert, f, referencesTableName) + assert.Equal(referencesTableName, table.Name()) +} - db := NewDatabase("foo", r) - assert.NotNil(db) +func TestReferencesTable_Children(t *testing.T) { + assert := assert.New(t) - tables := db.Tables() - table, ok := tables[referencesTableName] - assert.True(ok) - assert.NotNil(table) - assert.Equal(referencesTableName, table.Name()) + f := fixtures.Basic().One() + table := getTable(assert, f, referencesTableName) assert.Equal(0, len(table.Children())) +} - iter, err := table.RowIter() - assert.Nil(err) - assert.NotNil(iter) +func TestReferencesTable_RowIter(t *testing.T) { + assert := assert.New(t) - row, err := iter.Next() + f := fixtures.Basic().One() + table := getTable(assert, f, referencesTableName) + + rows, err := sql.NodeToRows(table) assert.Nil(err) - assert.NotNil(row) - - fields := row.Fields() - assert.NotNil(fields) - assert.IsType("", fields[0]) - assert.IsType("", fields[1]) - assert.IsType(true, fields[2]) - assert.IsType(true, fields[3]) - assert.IsType(true, fields[4]) - assert.IsType(true, fields[5]) - assert.IsType("", fields[6]) + assert.Len(rows, 5) + + schema := table.Schema() + for idx, row := range rows { + err := schema.CheckRow(row) + assert.Nil(err, "row %d doesn't conform to schema", idx) + } } diff --git a/git/tags.go b/git/tags.go index 6106edd44..d077b6a7f 100644 --- a/git/tags.go +++ b/git/tags.go @@ -28,7 +28,7 @@ func (tagsTable) Schema() sql.Schema { sql.Field{"name", sql.String}, sql.Field{"tagger_email", sql.String}, sql.Field{"tagger_name", sql.String}, - sql.Field{"tagger_when", sql.Timestamp}, + sql.Field{"tagger_when", sql.TimestampWithTimezone}, sql.Field{"message", sql.String}, sql.Field{"target", sql.String}, } diff --git a/git/tags_test.go b/git/tags_test.go index b67d369fd..27dee6834 100644 --- a/git/tags_test.go +++ b/git/tags_test.go @@ -2,46 +2,42 @@ package git import ( "testing" - "time" + + "github.com/gitql/gitql/sql" "github.com/stretchr/testify/assert" - "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/fixtures" ) -func TestTagsTable(t *testing.T) { +func TestTagsTable_Name(t *testing.T) { assert := assert.New(t) - f := fixtures.ByTag("tags").One() - r, err := git.NewFilesystemRepository(f.DotGit().Base()) - assert.Nil(err) + f := fixtures.Basic().One() + table := getTable(assert, f, tagsTableName) + assert.Equal(tagsTableName, table.Name()) +} - db := NewDatabase("foo", r) - assert.NotNil(db) +func TestTagsTable_Children(t *testing.T) { + assert := assert.New(t) - tables := db.Tables() - table, ok := tables[tagsTableName] - assert.True(ok) - assert.NotNil(table) - assert.Equal(tagsTableName, table.Name()) + f := fixtures.Basic().One() + table := getTable(assert, f, tagsTableName) assert.Equal(0, len(table.Children())) +} - iter, err := table.RowIter() - assert.Nil(err) - assert.NotNil(iter) +func TestTagsTable_RowIter(t *testing.T) { + assert := assert.New(t) - row, err := iter.Next() + f := fixtures.ByURL("https://github.com/git-fixtures/tags.git").One() + table := getTable(assert, f, tagsTableName) + + rows, err := sql.NodeToRows(table) assert.Nil(err) - assert.NotNil(row) - - fields := row.Fields() - assert.NotNil(fields) - - assert.IsType("", fields[0]) - assert.IsType("", fields[1]) - assert.IsType("", fields[2]) - assert.IsType("", fields[3]) - assert.IsType(time.Time{}, fields[4]) - assert.IsType("", fields[5]) - assert.IsType("", fields[6]) + assert.Len(rows, 4) + + schema := table.Schema() + for idx, row := range rows { + err := schema.CheckRow(row) + assert.Nil(err, "row %d doesn't conform to schema", idx) + } } diff --git a/git/tree_entries.go b/git/tree_entries.go index 24816f5b8..70c8bdb95 100644 --- a/git/tree_entries.go +++ b/git/tree_entries.go @@ -74,6 +74,7 @@ func (i *treeEntryIter) Next() (sql.Row, error) { if i.ei >= len(i.t.Entries) { i.t = nil + continue } e := i.t.Entries[i.ei] diff --git a/git/tree_entries_test.go b/git/tree_entries_test.go index 332460511..abef5e957 100644 --- a/git/tree_entries_test.go +++ b/git/tree_entries_test.go @@ -3,41 +3,41 @@ package git import ( "testing" + "github.com/gitql/gitql/sql" + "github.com/stretchr/testify/assert" - "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/fixtures" ) -func TestTreeEntriesTable(t *testing.T) { +func TestTreeEntriesTable_Name(t *testing.T) { assert := assert.New(t) f := fixtures.Basic().One() - r, err := git.NewFilesystemRepository(f.DotGit().Base()) - assert.Nil(err) + table := getTable(assert, f, treeEntriesTableName) + assert.Equal(treeEntriesTableName, table.Name()) +} - db := NewDatabase("foo", r) - assert.NotNil(db) +func TestTreeEntriesTable_Children(t *testing.T) { + assert := assert.New(t) - tables := db.Tables() - table, ok := tables[treeEntriesTableName] - assert.True(ok) - assert.NotNil(table) - assert.Equal(treeEntriesTableName, table.Name()) + f := fixtures.Basic().One() + table := getTable(assert, f, treeEntriesTableName) assert.Equal(0, len(table.Children())) +} - iter, err := table.RowIter() - assert.Nil(err) - assert.NotNil(iter) +func TestTreeEntriesTable_RowIter(t *testing.T) { + assert := assert.New(t) - row, err := iter.Next() + f := fixtures.Basic().One() + table := getTable(assert, f, treeEntriesTableName) + + rows, err := sql.NodeToRows(table) assert.Nil(err) - assert.NotNil(row) - - fields := row.Fields() - assert.NotNil(fields) - assert.Len(fields, 4) - assert.IsType("", fields[0]) - assert.IsType("", fields[1]) - assert.IsType("", fields[2]) - assert.IsType("", fields[3]) + assert.Len(rows, 45) + + schema := table.Schema() + for idx, row := range rows { + err := schema.CheckRow(row) + assert.Nil(err, "row %d doesn't conform to schema", idx) + } } diff --git a/mem/table_test.go b/mem/table_test.go index 951099927..2fb9a6c0c 100644 --- a/mem/table_test.go +++ b/mem/table_test.go @@ -1,7 +1,6 @@ package mem import ( - "io" "testing" "github.com/gitql/gitql/sql" @@ -23,24 +22,23 @@ func TestTable_Insert_RowIter(t *testing.T) { s := sql.Schema{ sql.Field{"col1", sql.String}, } + table := NewTable("test", s) - iter, err := table.RowIter() + + rows, err := sql.NodeToRows(table) assert.Nil(err) - assert.NotNil(iter) - _, err = iter.Next() - assert.Equal(io.EOF, err) + assert.Len(rows, 0) + err = table.Insert("foo") + rows, err = sql.NodeToRows(table) assert.Nil(err) + assert.Len(rows, 1) + assert.Nil(s.CheckRow(rows[0])) + err = table.Insert("bar") + rows, err = sql.NodeToRows(table) assert.Nil(err) - iter, err = table.RowIter() - row, err := iter.Next() - assert.NotNil(row) - assert.Nil(err) - assert.Equal("foo", row.Fields()[0]) - row, err = iter.Next() - assert.Equal("bar", row.Fields()[0]) - row, err = iter.Next() - assert.Nil(row) - assert.Equal(io.EOF, err) + assert.Len(rows, 2) + assert.Nil(s.CheckRow(rows[0])) + assert.Nil(s.CheckRow(rows[1])) } diff --git a/sql/row.go b/sql/row.go index 2e5495f96..8629040c9 100644 --- a/sql/row.go +++ b/sql/row.go @@ -1,5 +1,7 @@ package sql +import "io" + type Row interface { Fields() []interface{} } @@ -17,3 +19,30 @@ func NewMemoryRow(fields ...interface{}) MemoryRow { func (r MemoryRow) Fields() []interface{} { return []interface{}(r) } + +func RowIterToRows(i RowIter) ([]Row, error) { + var rows []Row + for { + row, err := i.Next() + if err == io.EOF { + break + } + + if err != nil { + return nil, err + } + + rows = append(rows, row) + } + + return rows, nil +} + +func NodeToRows(n Node) ([]Row, error) { + i, err := n.RowIter() + if err != nil { + return nil, err + } + + return RowIterToRows(i) +} diff --git a/sql/type.go b/sql/type.go index 6c1ad1d98..ba7df9526 100644 --- a/sql/type.go +++ b/sql/type.go @@ -5,10 +5,34 @@ import ( "reflect" "strconv" "strings" + "time" ) type Schema []Field +func (s Schema) CheckRow(row Row) error { + fields := row.Fields() + expected := len(s) + got := len(fields) + if expected != got { + return fmt.Errorf("expected %d values, got %d", expected, got) + } + + for idx, f := range s { + v := fields[idx] + if f.Type.Check(v) { + continue + } + + typ := reflect.TypeOf(v).String() + return fmt.Errorf("value at %d has unexpected type: %s", + idx, typ) + + } + + return nil +} + type Field struct { Name string Type Type @@ -100,28 +124,29 @@ func (t bigIntegerType) Compare(a interface{}, b interface{}) int { return compareInt64(a, b) } -var Timestamp = timestampType{} +// TimestampWithTimezone is a timestamp with timezone. +var TimestampWithTimezone = timestampWithTimeZoneType{} -type timestampType struct{} +type timestampWithTimeZoneType struct{} -func (t timestampType) Name() string { - return "timestamp" +func (t timestampWithTimeZoneType) Name() string { + return "timestamp with timezone" } -func (t timestampType) InternalType() reflect.Kind { - return reflect.Int64 +func (t timestampWithTimeZoneType) InternalType() reflect.Kind { + return reflect.Struct } -func (t timestampType) Check(v interface{}) bool { - return checkInt64(v) +func (t timestampWithTimeZoneType) Check(v interface{}) bool { + return checkTimestamp(v) } -func (t timestampType) Convert(v interface{}) (interface{}, error) { - return convertToInt64(v) +func (t timestampWithTimeZoneType) Convert(v interface{}) (interface{}, error) { + return convertToTimestamp(v) } -func (t timestampType) Compare(a interface{}, b interface{}) int { - return compareInt64(a, b) +func (t timestampWithTimeZoneType) Compare(a interface{}, b interface{}) int { + return compareTimestamp(a, b) } var String = stringType{} @@ -388,3 +413,43 @@ func compareFloat64(a interface{}, b interface{}) int { } return 0 } + +func checkTimestamp(v interface{}) bool { + _, ok := v.(time.Time) + return ok +} + +const timestampLayout = "2006-01-02 15:04:05.000000" + +func convertToTimestamp(v interface{}) (interface{}, error) { + switch v.(type) { + case string: + t, err := time.Parse(timestampLayout, v.(string)) + if err != nil { + return nil, fmt.Errorf("value %q can't be converted to int64", v) + } + return t, nil + default: + if !BigInteger.Check(v) { + return nil, ErrInvalidType + } + + bi, err := BigInteger.Convert(v) + if err != nil { + return nil, ErrInvalidType + } + + return time.Unix(bi.(int64), 0), nil + } +} + +func compareTimestamp(a interface{}, b interface{}) int { + av := a.(time.Time) + bv := b.(time.Time) + if av.Before(bv) { + return -1 + } else if av.After(bv) { + return 1 + } + return 0 +}