Commits

Paul Ruane committed cde0dd9

Made database transactions explicit.
Removed transactions from unit-tests.
Removed transactions from VFS (was preventing tagging whilst VFS mounted).
VFS queries are now only saved if they don't already exist.
Added support for implied tags to VFS.

  • Participants
  • Parent commits ec584ba

Comments (0)

Files changed (29)

File src/tmsu/cli/common_test.go

 }
 
 func expectTags(test *testing.T, store *storage.Storage, file *entities.File, tags ...*entities.Tag) {
-	fileTags, err := store.FileTagsByFileId(file.Id)
+	fileTags, err := store.FileTagsByFileId(file.Id, true)
 	if err != nil {
 		test.Fatal(err)
 	}

File src/tmsu/cli/copy.go

 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	sourceTagName := args[0]

File src/tmsu/cli/copy_test.go

 package cli
 
 import (
+	"fmt"
 	"os"
 	"testing"
 	"time"
 )
 
 func TestCopySuccessful(test *testing.T) {
+	fmt.Println("1")
 	// set-up
 
 	databasePath := testDatabase()
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := CopyCommand.Exec(Options{}, []string{"source", "dest"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	err = CopyCommand.Exec(Options{}, []string{"source", "dest"})

File src/tmsu/cli/delete.go

 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	wereErrors := false

File src/tmsu/cli/delete_test.go

 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := DeleteCommand.Exec(Options{}, []string{"deathrow"}); err != nil {
 		test.Fatal("Deleted tag still exists.")
 	}
 
-	fileTagsD, err := store.FileTagsByFileId(fileD.Id)
+	fileTagsD, err := store.FileTagsByFileId(fileD.Id, true)
 	if err != nil {
 		test.Fatal(err)
 	}
 		test.Fatal("Expected no file-tags for file 'd'.")
 	}
 
-	fileTagsF, err := store.FileTagsByFileId(fileF.Id)
+	fileTagsF, err := store.FileTagsByFileId(fileF.Id, true)
 	if err != nil {
 		test.Fatal(err)
 	}
 		test.Fatal("Expected file-tag for tag 'freeman'.")
 	}
 
-	fileTagsB, err := store.FileTagsByFileId(fileB.Id)
+	fileTagsB, err := store.FileTagsByFileId(fileB.Id, true)
 	if err != nil {
 		test.Fatal(err)
 	}

File src/tmsu/cli/dupes_test.go

 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := DupesCommand.Exec(Options{}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := DupesCommand.Exec(Options{}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := DupesCommand.Exec(Options{}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := DupesCommand.Exec(Options{}, []string{path}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := DupesCommand.Exec(Options{}, []string{path}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := DupesCommand.Exec(Options{}, []string{path}); err != nil {

File src/tmsu/cli/files.go

 	print0 := options.HasOption("--print0")
 	showCount := options.HasOption("--count")
 	untagged := options.HasOption("--untagged")
-	explicit := options.HasOption("--explicit")
+	explicitOnly := options.HasOption("--explicit")
 
 	if options.HasOption("--all") {
 		return listAllFiles(dirOnly, fileOnly, topOnly, leafOnly, print0, showCount)
 	}
 
 	queryText := strings.Join(args, " ")
-	return listFilesForQuery(queryText, absPath, dirOnly, fileOnly, topOnly, leafOnly, print0, showCount, untagged, explicit)
+	return listFilesForQuery(queryText, absPath, dirOnly, fileOnly, topOnly, leafOnly, print0, showCount, untagged, explicitOnly)
 }
 
 // unexported
 	return listFiles(files, dirOnly, fileOnly, topOnly, leafOnly, print0, showCount)
 }
 
-func listFilesForQuery(queryText, path string, dirOnly, fileOnly, topOnly, leafOnly, print0, showCount, untagged, explicit bool) error {
+func listFilesForQuery(queryText, path string, dirOnly, fileOnly, topOnly, leafOnly, print0, showCount, untagged, explicitOnly bool) error {
 	if queryText == "" {
 		return fmt.Errorf("query must be specified. Use --all to show all files.")
 	}
 	if untagged && path != "" {
 		log.Info(2, "temporarily adding untagged files")
 
+		if err := store.Begin(); err != nil {
+			return fmt.Errorf("could not begin transaction: %v", err)
+		}
+		defer store.Rollback()
+
 		if err := addUntaggedFiles(store, path); err != nil {
 			return err
 		}
 		return blankError
 	}
 
-	if !explicit {
-		log.Info(2, "applying tag implications")
-
-		implications, err := store.Implications()
-		if err != nil {
-			return fmt.Errorf("could not load tag implications")
-		}
-		expression = applyImplications(expression, implications)
-	}
-
 	log.Info(2, "querying database")
 
-	files, err := store.QueryFiles(expression, path)
+	files, err := store.QueryFiles(expression, path, explicitOnly)
 	if err != nil {
 		return fmt.Errorf("could not query files: %v", err)
 	}
 	return nil
 }
 
-func applyImplications(expression query.Expression, implications entities.Implications) query.Expression {
-	impliersByTag := make(map[string][]string, len(implications))
-
-	for _, implication := range implications {
-		impliers, ok := impliersByTag[implication.ImpliedTag.Name]
-		if !ok {
-			impliers = make([]string, 0, 1)
-		}
-
-		impliersByTag[implication.ImpliedTag.Name] = append(impliers, implication.ImplyingTag.Name)
-	}
-
-	return applyImplicationsRecursive(expression, impliersByTag)
-}
-
-func applyImplicationsRecursive(expression query.Expression, impliersByTag map[string][]string) query.Expression {
-	switch typedExpression := expression.(type) {
-	case query.OrExpression:
-		typedExpression.LeftOperand = applyImplicationsRecursive(typedExpression.LeftOperand, impliersByTag)
-		typedExpression.RightOperand = applyImplicationsRecursive(typedExpression.RightOperand, impliersByTag)
-		return typedExpression
-	case query.AndExpression:
-		typedExpression.LeftOperand = applyImplicationsRecursive(typedExpression.LeftOperand, impliersByTag)
-		typedExpression.RightOperand = applyImplicationsRecursive(typedExpression.RightOperand, impliersByTag)
-		return typedExpression
-	case query.NotExpression:
-		typedExpression.Operand = applyImplicationsRecursive(typedExpression.Operand, impliersByTag)
-		return typedExpression
-	case query.TagExpression:
-		return applyImplicationsForTag(typedExpression, impliersByTag)
-	case query.ValueExpression, query.EmptyExpression:
-		return expression
-	default:
-		panic(fmt.Sprintf("unsupported expression type '%v'.", typedExpression))
-	}
-}
-
-func applyImplicationsForTag(tagExpression query.TagExpression, impliersByTag map[string][]string) query.Expression {
-	implyingTags, ok := impliersByTag[tagExpression.Name]
-	if !ok {
-		return tagExpression
-	}
-
-	var expression query.Expression = tagExpression
-
-	for index := 0; index < len(implyingTags); index++ {
-		implyingTag := implyingTags[index]
-
-		expression = query.OrExpression{expression, query.TagExpression{implyingTag}}
-
-		for _, furtherImplyingTag := range impliersByTag[implyingTag] {
-			if furtherImplyingTag != tagExpression.Name && !containsTag(implyingTags, furtherImplyingTag) {
-				implyingTags = append(implyingTags, furtherImplyingTag)
-			}
-		}
-	}
-
-	return expression
-}
-
 func containsTag(tags []string, tag string) bool {
 	for _, iteratedTag := range tags {
 		if iteratedTag == tag {

File src/tmsu/cli/files_test.go

 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := FilesCommand.Exec(Options{Option{"-a", "--all", "", false, ""}}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := FilesCommand.Exec(Options{}, []string{"b"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := FilesCommand.Exec(Options{}, []string{"not", "b"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := FilesCommand.Exec(Options{}, []string{"b", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := FilesCommand.Exec(Options{}, []string{"b", "and", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := FilesCommand.Exec(Options{}, []string{"b", "not", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := FilesCommand.Exec(Options{}, []string{"b", "and", "not", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := FilesCommand.Exec(Options{}, []string{"b", "or", "c"}); err != nil {

File src/tmsu/cli/imply.go

 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	switch {

File src/tmsu/cli/merge.go

 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	destTagName := args[len(args)-1]

File src/tmsu/cli/merge_test.go

 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := MergeCommand.Exec(Options{}, []string{"a", "b"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := MergeCommand.Exec(Options{}, []string{"a", "b", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := MergeCommand.Exec(Options{}, []string{"a", "b"}); err == nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := MergeCommand.Exec(Options{}, []string{"a", "b"}); err == nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := MergeCommand.Exec(Options{}, []string{"a", "a"}); err == nil {

File src/tmsu/cli/rename.go

 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	if len(args) < 2 {

File src/tmsu/cli/rename_test.go

 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := RenameCommand.Exec(Options{}, []string{"source", "dest"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	err = RenameCommand.Exec(Options{}, []string{"source", "dest"})

File src/tmsu/cli/repair.go

 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	fingerprintAlgorithm, err := store.SettingAsString("fingerprintAlgorithm")

File src/tmsu/cli/repair_test.go

 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := RepairCommand.Exec(Options{}, []string{"/tmp/tmsu"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := RepairCommand.Exec(Options{}, []string{"/tmp/tmsu"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := RepairCommand.Exec(Options{}, []string{}); err != nil {

File src/tmsu/cli/status.go

 		return nil, fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
-	defer store.Commit()
 
 	for _, path := range paths {
 		absPath, err := filepath.Abs(path)

File src/tmsu/cli/tag.go

 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	wereErrors := false
 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	fingerprintAlgorithm, err := store.SettingAsString("fingerprintAlgorithm")
 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	fingerprintAlgorithmSetting, err := store.Setting("fingerprintAlgorithm")
 		return fmt.Errorf("%v: path is not tagged")
 	}
 
-	fileTags, err := store.FileTagsByFileId(file.Id)
+	fileTags, err := store.FileTagsByFileId(file.Id, true)
 	if err != nil {
 		return fmt.Errorf("%v: could not retrieve filetags: %v", fromPath, err)
 	}

File src/tmsu/cli/tag_test.go

 	}
 	defer os.Remove("/tmp/tmsu/a")
 
-	store.Commit()
-
 	// test
 
 	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple"}); err != nil {
 	}
 	defer os.Remove("/tmp/tmsu/a")
 
-	store.Commit()
-
 	// test
 
 	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple", "banana", "clementine"}); err != nil {
 	}
 	defer os.Remove("/tmp/tmsu/b")
 
-	store.Commit()
-
 	// test
 
 	if err := TagCommand.Exec(Options{Option{"--tags", "-t", "", true, "apple banana"}}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {

File src/tmsu/cli/tags.go

 }
 
 func tagNamesForFile(store *storage.Storage, fileId uint, explicitOnly bool) ([]string, error) {
-	fileTags, err := store.FileTagsByFileId(fileId)
+	fileTags, err := store.FileTagsByFileId(fileId, explicitOnly)
 	if err != nil {
 		return nil, fmt.Errorf("could not retrieve file-tags for file '%v': %v", fileId, err)
 	}
 		return nil, err
 	}
 
-	if !explicitOnly {
-		impliedTagNames, err := lookupImpliedTagNames(store, fileTags)
-		if err != nil {
-			return nil, err
-		}
-
-		for _, impliedTagName := range impliedTagNames {
-			if !containsTagName(tagNames, impliedTagName) {
-				tagNames = append(tagNames, impliedTagName)
-			}
-		}
-	}
-
 	sort.Strings(tagNames)
 
 	return tagNames, nil
 
 	return tagNames, nil
 }
-
-func lookupImpliedTagNames(store *storage.Storage, fileTags entities.FileTags) ([]string, error) {
-	tagIds := make([]uint, 0, len(fileTags))
-	for _, fileTag := range fileTags {
-		if !containsTagId(tagIds, fileTag.TagId) {
-			tagIds = append(tagIds, fileTag.TagId)
-		}
-	}
-
-	implications, err := store.ImplicationsForTags(tagIds...)
-	if err != nil {
-		return nil, fmt.Errorf("could not look up tag implications: %v", err)
-	}
-
-	tagNames := make([]string, len(implications))
-	for index, implication := range implications {
-		tagNames[index] = implication.ImpliedTag.Name
-	}
-
-	return tagNames, nil
-}
-
-func containsTagId(tagIds []uint, tagId uint) bool {
-	for index := 0; index < len(tagIds); index++ {
-		if tagIds[index] == tagId {
-			return true
-		}
-	}
-
-	return false
-}
-
-func containsTagName(tagNames []string, tagName string) bool {
-	for index := 0; index < len(tagNames); index++ {
-		if tagNames[index] == tagName {
-			return true
-		}
-	}
-
-	return false
-}

File src/tmsu/cli/tags_test.go

 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := TagsCommand.Exec(Options{}, []string{"/tmp/tmsu/a"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := TagsCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := TagsCommand.Exec(Options{Option{"--all", "-a", "", false, ""}}, []string{}); err != nil {

File src/tmsu/cli/untag.go

 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	wereErrors := false
 		return fmt.Errorf("could not open storage: %v", err)
 	}
 	defer store.Close()
+
+	if err := store.Begin(); err != nil {
+		return fmt.Errorf("could not begin transaction: %v", err)
+	}
 	defer store.Commit()
 
 	tagValuePairs := make([]TagValuePair, 0, 10)

File src/tmsu/cli/untag_test.go

 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := UntagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := UntagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple", "banana"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := UntagCommand.Exec(Options{Option{"--tags", "-t", "", true, "apple"}}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := UntagCommand.Exec(Options{Option{"--all", "-a", "", false, ""}}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {

File src/tmsu/cli/values_test.go

 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := ValuesCommand.Exec(Options{}, []string{"material"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := ValuesCommand.Exec(Options{}, []string{"material", "shape"}); err != nil {
 		test.Fatal(err)
 	}
 
-	store.Commit()
-
 	// test
 
 	if err := ValuesCommand.Exec(Options{Option{"--all", "-a", "", false, ""}}, []string{}); err != nil {

File src/tmsu/storage/database/database.go

 		return nil, fmt.Errorf("could not open database: %v", err)
 	}
 
-	transaction, err := connection.Begin()
-	if err != nil {
-		return nil, fmt.Errorf("could not begin transaciton: %v", err)
+	database := &Database{connection, nil}
+
+	if err := database.Begin(); err != nil {
+		return nil, err
 	}
 
-	database := &Database{connection, transaction}
-
 	if err := database.CreateSchema(); err != nil {
 		return nil, errors.New("could not create database schema: " + err.Error())
 	}
 
+	if err := database.Commit(); err != nil {
+		return nil, err
+	}
+
 	return database, nil
 }
 
 		}
 	}
 
-	return db.transaction.Exec(sql, args...)
+	if db.transaction != nil {
+		return db.transaction.Exec(sql, args...)
+	}
+
+	return db.connection.Exec(sql, args...)
 }
 
 // Executes a SQL query returning rows.
 		}
 	}
 
-	return db.transaction.Query(sql, args...)
+	if db.transaction != nil {
+		return db.transaction.Query(sql, args...)
+	}
+
+	return db.connection.Query(sql, args...)
 }
 
-// Commits the current transaction
-func (db *Database) Commit() error {
-	log.Info(2, "committing transaction")
-
-	if err := db.transaction.Commit(); err != nil {
-		return fmt.Errorf("could not commit transaction: %v", err)
+// Start a transaction
+func (db *Database) Begin() error {
+	if db.transaction != nil {
+		return fmt.Errorf("could not begin transaction: there is already an open transaction")
 	}
 
 	log.Info(2, "beginning new transaction")
 	return nil
 }
 
+// Commits the current transaction
+func (db *Database) Commit() error {
+	if db.transaction == nil {
+		return fmt.Errorf("could not commit transaction: there is no open transaciton")
+	}
+
+	log.Info(2, "committing transaction")
+
+	if err := db.transaction.Commit(); err != nil {
+		return fmt.Errorf("could not commit transaction: %v", err)
+	}
+
+	db.transaction = nil
+
+	return nil
+}
+
+// Rolls back the current transaction
+func (db *Database) Rollback() error {
+	if db.transaction == nil {
+		return fmt.Errorf("could not rollback transaction: there is no open transaciton")
+	}
+
+	log.Info(2, "rolling back transaction")
+
+	if err := db.transaction.Rollback(); err != nil {
+		return fmt.Errorf("could not roll back transaction: %v", err)
+	}
+
+	db.transaction = nil
+
+	return nil
+}
+
 // Closes the database connection
 func (db *Database) Close() error {
 	log.Info(3, "closing database")

File src/tmsu/storage/database/schema.go

 
 import (
 	_ "github.com/mattn/go-sqlite3"
+	"tmsu/common/log"
 )
 
 func (db *Database) CreateSchema() error {
+	log.Info(2, "creating schema")
+
 	if err := db.CreateTagTable(); err != nil {
 		return err
 	}
 		return err
 	}
 
-	if err := db.Commit(); err != nil {
-		return err
-	}
-
 	return nil
 }
 

File src/tmsu/storage/file.go

 	return storage.Db.FilesByFingerprint(fingerprint)
 }
 
-// Retrieves the count of files with the specified tags and matching the specified path.
-func (storage *Storage) FileCountWithTags(tagNames []string, path string) (uint, error) {
-	expression := query.HasAll(tagNames)
-	return storage.Db.QueryFileCount(expression, path)
-}
-
 // Retrieves the set of untagged files.
 func (storage *Storage) UntaggedFiles() (entities.Files, error) {
 	return storage.Db.UntaggedFiles()
 }
 
+// Retrieves the count of files with the specified tags and matching the specified path.
+func (storage *Storage) FileCountWithTags(tagNames []string, path string, explicitOnly bool) (uint, error) {
+	expression := query.HasAll(tagNames)
+
+	if !explicitOnly {
+		var err error
+		expression, err = storage.addImpliedTags(expression)
+		if err != nil {
+			return 0, err
+		}
+	}
+
+	return storage.Db.QueryFileCount(expression, path)
+}
+
 // Retrieves the set of files with the specified tags and matching the specified path.
-func (storage *Storage) FilesWithTags(tagNames []string, path string) (entities.Files, error) {
+func (storage *Storage) FilesWithTags(tagNames []string, path string, explicitOnly bool) (entities.Files, error) {
 	expression := query.HasAll(tagNames)
+
+	if !explicitOnly {
+		var err error
+		expression, err = storage.addImpliedTags(expression)
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	return storage.Db.QueryFiles(expression, path)
 }
 
 // Retrieves the count of files that match the specified query and matching the specified path.
-func (storage *Storage) QueryFileCount(expression query.Expression, path string) (uint, error) {
+func (storage *Storage) QueryFileCount(expression query.Expression, path string, explicitOnly bool) (uint, error) {
+	if !explicitOnly {
+		var err error
+		expression, err = storage.addImpliedTags(expression)
+		if err != nil {
+			return 0, err
+		}
+	}
+
 	return storage.Db.QueryFileCount(expression, path)
 }
 
 // Retrieves the set of files that match the specified query.
-func (storage *Storage) QueryFiles(expression query.Expression, path string) (entities.Files, error) {
+func (storage *Storage) QueryFiles(expression query.Expression, path string, explicitOnly bool) (entities.Files, error) {
+	if !explicitOnly {
+		var err error
+		expression, err = storage.addImpliedTags(expression)
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	return storage.Db.QueryFiles(expression, path)
 }
 
 func (storage *Storage) DeleteUntaggedFiles() error {
 	return storage.Db.DeleteUntaggedFiles()
 }
+
+// unexported
+
+func (storage *Storage) addImpliedTags(expression query.Expression) (query.Expression, error) {
+	implications, err := storage.Implications()
+	if err != nil {
+		fmt.Errorf("could not retrieve tag implications: %v", err)
+	}
+
+	impliersByTag := make(map[string][]string, len(implications))
+	for _, implication := range implications {
+		impliers, ok := impliersByTag[implication.ImpliedTag.Name]
+		if !ok {
+			impliers = make([]string, 0, 1)
+		}
+
+		impliersByTag[implication.ImpliedTag.Name] = append(impliers, implication.ImplyingTag.Name)
+	}
+
+	return addImpliedTagsRecursive(expression, impliersByTag), nil
+}
+
+func addImpliedTagsRecursive(expression query.Expression, impliersByTag map[string][]string) query.Expression {
+	switch typedExpression := expression.(type) {
+	case query.OrExpression:
+		typedExpression.LeftOperand = addImpliedTagsRecursive(typedExpression.LeftOperand, impliersByTag)
+		typedExpression.RightOperand = addImpliedTagsRecursive(typedExpression.RightOperand, impliersByTag)
+		return typedExpression
+	case query.AndExpression:
+		typedExpression.LeftOperand = addImpliedTagsRecursive(typedExpression.LeftOperand, impliersByTag)
+		typedExpression.RightOperand = addImpliedTagsRecursive(typedExpression.RightOperand, impliersByTag)
+		return typedExpression
+	case query.NotExpression:
+		typedExpression.Operand = addImpliedTagsRecursive(typedExpression.Operand, impliersByTag)
+		return typedExpression
+	case query.TagExpression:
+		return applyImplicationsForTag(typedExpression, impliersByTag)
+	case query.ValueExpression, query.EmptyExpression:
+		return expression
+	default:
+		panic(fmt.Sprintf("unsupported expression type '%v'.", typedExpression))
+	}
+}
+
+func applyImplicationsForTag(tagExpression query.TagExpression, impliersByTag map[string][]string) query.Expression {
+	implyingTags, ok := impliersByTag[tagExpression.Name]
+	if !ok {
+		return tagExpression
+	}
+
+	var expression query.Expression = tagExpression
+
+	for index := 0; index < len(implyingTags); index++ {
+		implyingTag := implyingTags[index]
+
+		expression = query.OrExpression{expression, query.TagExpression{implyingTag}}
+
+		for _, furtherImplyingTag := range impliersByTag[implyingTag] {
+			if furtherImplyingTag != tagExpression.Name && !containsTagName(implyingTags, furtherImplyingTag) {
+				implyingTags = append(implyingTags, furtherImplyingTag)
+			}
+		}
+	}
+
+	return expression
+}
+
+func containsTagName(tagNames []string, tagName string) bool {
+	for _, tn := range tagNames {
+		if tn == tagName {
+			return true
+		}
+	}
+
+	return false
+}

File src/tmsu/storage/filetag.go

 }
 
 // Retrieves the file tags with the specified file ID.
-func (storage *Storage) FileTagsByFileId(fileId uint) (entities.FileTags, error) {
-	return storage.Db.FileTagsByFileId(fileId)
+func (storage *Storage) FileTagsByFileId(fileId uint, explicitOnly bool) (entities.FileTags, error) {
+	fileTags, err := storage.Db.FileTagsByFileId(fileId)
+	if err != nil {
+		return nil, err
+	}
+
+	if !explicitOnly {
+		tagIds := make([]uint, 0, len(fileTags))
+		for _, fileTag := range fileTags {
+			tagIds = append(tagIds, fileTag.TagId)
+		}
+
+		implications, err := storage.ImplicationsForTags(tagIds...)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, implication := range implications {
+			fileTag := entities.FileTag{fileId, implication.ImpliedTag.Id, 0}
+
+			if !containsFileTag(fileTags, fileTag) {
+				fileTags = append(fileTags, &fileTag)
+			}
+		}
+	}
+
+	return fileTags, nil
 }
 
 // Adds a file tag.
 
 // helpers
 
-func contains(files entities.Files, searchFile *entities.File) bool {
-	for _, file := range files {
-		if file.Path() == searchFile.Path() {
+func containsFileTag(fileTags entities.FileTags, fileTag entities.FileTag) bool {
+	for _, ft := range fileTags {
+		if ft.FileId == fileTag.FileId && ft.TagId == fileTag.TagId && ft.ValueId == fileTag.ValueId {
 			return true
 		}
 	}

File src/tmsu/storage/storage.go

 	return &Storage{db}, nil
 }
 
+func (storage *Storage) Begin() error {
+	return storage.Db.Begin()
+}
+
 func (storage *Storage) Commit() error {
 	return storage.Db.Commit()
 }
 
+func (storage *Storage) Rollback() error {
+	return storage.Db.Rollback()
+}
+
 func (storage *Storage) Close() error {
 	err := storage.Db.Close()
 	if err != nil {

File src/tmsu/vfs/fusevfs.go

 			log.Fatalf("could not create tag '%v': %v", name, err)
 		}
 
-		if err := vfs.store.Commit(); err != nil {
-			log.Fatalf("could not commit transaction: %v", err)
-		}
-
 		return fuse.OK
 	case queriesDir:
 		return fuse.EINVAL
 		log.Fatalf("could not rename tag '%v' to '%v': %v", oldTagName, newTagName, err)
 	}
 
-	if err := vfs.store.Commit(); err != nil {
-		log.Fatalf("could not commit transaction: %v", err)
-	}
-
 	return fuse.OK
 }
 
 			log.Fatalf("could not delete tag '%v': %v", tagName, err)
 		}
 
-		if err := vfs.store.Commit(); err != nil {
-			log.Fatalf("could not commit transaction: %v", err)
-		}
-
 		return fuse.OK
 	case queriesDir:
 		if len(path) != 2 {
 			log.Fatalf("could not remove tag '%v': %v", name, err)
 		}
 
-		if err := vfs.store.Commit(); err != nil {
-			log.Fatalf("could not commit transaction: %v", err)
-		}
-
 		return fuse.OK
 	}
 
 			log.Fatal(err)
 		}
 
-		if err := vfs.store.Commit(); err != nil {
-			log.Fatalf("could not commit transaction: %v", err)
-		}
-
 		return fuse.OK
 	case queriesDir:
 		return fuse.EPERM
 		}
 	}
 
-	_, _ = vfs.store.AddQuery(queryText)
-
-	if err := vfs.store.Commit(); err != nil {
-		log.Fatalf("could not commit transaction: %v", err)
+	q, err := vfs.store.Query(queryText)
+	if err != nil {
+		log.Fatalf("could not retrieve query '%v': %v", queryText, err)
+	}
+	if q == nil {
+		_, err = vfs.store.AddQuery(queryText)
+		if err != nil {
+			log.Fatalf("could not add query '%v': %v", queryText, err)
+		}
 	}
 
 	now := time.Now()
 	defer log.Infof(2, "END openTaggedEntryDir(%v)", path)
 
 	expression := query.HasAll(path)
-	files, err := vfs.store.QueryFiles(expression, "")
+	files, err := vfs.store.QueryFiles(expression, "", false)
 	if err != nil {
 		log.Fatalf("could not query files: %v", err)
 	}
 
 	furtherTagNames := make([]string, 0, 10)
 	for _, file := range files {
-		fileTags, err := vfs.store.FileTagsByFileId(file.Id)
+		fileTags, err := vfs.store.FileTagsByFileId(file.Id, false)
 		if err != nil {
 			log.Fatalf("could not retrieve file-tags for file '%v': %v", file.Id, err)
 		}
 		}
 	}
 
-	files, err := vfs.store.QueryFiles(expression, "")
+	files, err := vfs.store.QueryFiles(expression, "", false)
 	if err != nil {
 		log.Fatalf("could not query files: %v", err)
 	}