diff --git a/docs/guide.md b/docs/guide.md index ab45dfc..0b199e7 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -132,6 +132,16 @@ query { } ``` +Postgres also supports full text search using a TSV index. Super Graph makes it easy to use this full text search capability using the `search` argument. + +```graphql +query { + products(seasrch "amazing") { + name + } +} +``` + Super Graph support complex queries where you can add filters, ordering,offsets and limits on the query. #### Logical Operators diff --git a/example/db/migrate/20190405042247_add_search_column.rb b/example/db/migrate/20190405042247_add_search_column.rb new file mode 100644 index 0000000..71ddf96 --- /dev/null +++ b/example/db/migrate/20190405042247_add_search_column.rb @@ -0,0 +1,33 @@ +class AddSearchColumn < ActiveRecord::Migration[5.1] + def self.up + add_column :products, :tsv, :tsvector + add_index :products, :tsv, using: "gin" + + say_with_time("Adding trigger to update the ts_vector column") do + execute <<-SQL + CREATE FUNCTION products_tsv_trigger() RETURNS trigger AS $$ + begin + new.tsv := + setweight(to_tsvector('pg_catalog.english', coalesce(new.name,'')), 'A') || + setweight(to_tsvector('pg_catalog.english', coalesce(new.description,'')), 'B'); + return new; + end + $$ LANGUAGE plpgsql; + + CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON products FOR EACH ROW EXECUTE PROCEDURE products_tsv_trigger(); + SQL + end + end + + def self.down + say_with_time("Removing trigger to update the tsv column") do + execute <<-SQL + DROP TRIGGER tsvectorupdate + ON products + SQL + end + + remove_index :products, :tsv + remove_column :products, :tsv + end +end \ No newline at end of file diff --git a/example/db/schema.rb b/example/db/schema.rb index 8ffb865..d00fb8f 100644 --- a/example/db/schema.rb +++ b/example/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_03_22_200743) do +ActiveRecord::Schema.define(version: 2019_04_05_042247) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -36,6 +36,8 @@ ActiveRecord::Schema.define(version: 2019_03_22_200743) do t.bigint "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.tsvector "tsv" + t.index ["tsv"], name: "index_products_on_tsv", using: :gin t.index ["user_id"], name: "index_products_on_user_id" end diff --git a/go.mod b/go.mod index 1fa38ab..2618383 100644 --- a/go.mod +++ b/go.mod @@ -13,5 +13,6 @@ require ( github.com/sirupsen/logrus v1.4.0 github.com/spf13/viper v1.3.1 github.com/valyala/fasttemplate v1.0.1 + golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a mellium.im/sasl v0.2.1 // indirect ) diff --git a/psql/psql.go b/psql/psql.go index 3e0c29f..6095c04 100644 --- a/psql/psql.go +++ b/psql/psql.go @@ -397,12 +397,28 @@ func (v *selectBlock) renderRelationship(w io.Writer, schema *DBSchema) { func (v *selectBlock) renderWhere(w io.Writer) error { if v.sel.Where.Op == qcode.OpEqID { - col, ok := v.schema.PCols[v.sel.Table] - if !ok { - return fmt.Errorf("no primary key defined for %s", v.sel.Table) + t, err := v.schema.GetTable(v.sel.Table) + if err != nil { + return err + } + if len(t.PrimaryCol) == 0 { + return fmt.Errorf("no primary key column defined for %s", v.sel.Table) } - fmt.Fprintf(w, `(("%s") = ('%s'))`, col.Name, v.sel.Where.Val) + fmt.Fprintf(w, `(("%s") = ('%s'))`, t.PrimaryCol, v.sel.Where.Val) + return nil + } + + if v.sel.Where.Op == qcode.OpTsQuery { + t, err := v.schema.GetTable(v.sel.Table) + if err != nil { + return err + } + if len(t.TSVCol) == 0 { + return fmt.Errorf("no tsv column defined for %s", v.sel.Table) + } + + fmt.Fprintf(w, `(("%s") @@ to_tsquery('%s'))`, t.TSVCol, v.sel.Where.Val) return nil } diff --git a/psql/psql_test.go b/psql/psql_test.go index dc9e27c..054243d 100644 --- a/psql/psql_test.go +++ b/psql/psql_test.go @@ -337,6 +337,26 @@ func fetchByID(t *testing.T) { } } +func searchQuery(t *testing.T) { + gql := `query { + products(search: "Amazing") { + id + name + } + }` + + sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "products" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("tsv") @@ to_tsquery('Amazing'))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "products_0") AS "done_1337";` + + resSQL, err := compileGQLToPSQL(gql) + if err != nil { + t.Fatal(err) + } + + if resSQL != sql { + t.Fatal(errNotExpected) + } +} + func aggFunction(t *testing.T) { gql := `query { products { @@ -383,6 +403,7 @@ func TestCompileGQL(t *testing.T) { t.Run("withWhereIsNull", withWhereIsNull) t.Run("withWhereMultiOr", withWhereMultiOr) t.Run("fetchByID", fetchByID) + t.Run("searchQuery", searchQuery) t.Run("belongsTo", belongsTo) t.Run("oneToMany", oneToMany) t.Run("manyToMany", manyToMany) diff --git a/psql/tables.go b/psql/tables.go index 7f2a225..ffe4537 100644 --- a/psql/tables.go +++ b/psql/tables.go @@ -18,11 +18,16 @@ type TTKey struct { type DBSchema struct { ColMap map[TCKey]*DBColumn ColIDMap map[int]*DBColumn - PCols map[string]*DBColumn + Tables map[string]*DBTableInfo RelMap map[TTKey]*DBRel } +type DBTableInfo struct { + PrimaryCol string + TSVCol string +} + type RelType int const ( @@ -63,7 +68,7 @@ func initSchema() *DBSchema { return &DBSchema{ ColMap: make(map[TCKey]*DBColumn), ColIDMap: make(map[int]*DBColumn), - PCols: make(map[string]*DBColumn), + Tables: make(map[string]*DBTableInfo), RelMap: make(map[TTKey]*DBRel), } } @@ -71,6 +76,7 @@ func initSchema() *DBSchema { func updateSchema(schema *DBSchema, t *DBTable, cols []*DBColumn) { // Current table ct := strings.ToLower(t.Name) + schema.Tables[ct] = &DBTableInfo{} // Foreign key columns in current table var jcols []*DBColumn @@ -82,8 +88,11 @@ func updateSchema(schema *DBSchema, t *DBTable, cols []*DBColumn) { for _, c := range cols { switch { + case c.Type == "tsvector": + schema.Tables[ct].TSVCol = c.Name + case c.PrimaryKey: - schema.PCols[ct] = c + schema.Tables[ct].PrimaryCol = c.Name case len(c.FKeyTable) != 0: if len(c.FKeyColID) == 0 { @@ -244,3 +253,11 @@ WHERE c.relkind = 'r'::char return t, nil } + +func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) { + t, ok := s.Tables[table] + if !ok { + return nil, fmt.Errorf("table info not found '%s'", table) + } + return t, nil +} diff --git a/qcode/qcode.go b/qcode/qcode.go index 2691af2..fefd6ba 100644 --- a/qcode/qcode.go +++ b/qcode/qcode.go @@ -85,6 +85,7 @@ const ( OpHasKeyAll OpIsNull OpEqID + OpTsQuery ) func (t ExpOp) String() string { @@ -139,6 +140,8 @@ func (t ExpOp) String() string { v = "op-is-null" case OpEqID: v = "op-eq-id" + case OpTsQuery: + v = "op-ts-query" } return fmt.Sprintf("<%s>", v) } @@ -341,6 +344,10 @@ func (com *Compiler) compileArgs(sel *Select, args []*Arg) error { if sel.ID == int16(0) { err = com.compileArgID(sel, args[i]) } + case "search": + if sel.ID == int16(0) { + err = com.compileArgSearch(sel, args[i]) + } case "where": err = com.compileArgWhere(sel, args[i]) case "orderby", "order_by", "order": @@ -438,6 +445,25 @@ func (com *Compiler) compileArgID(sel *Select, arg *Arg) error { return nil } +func (com *Compiler) compileArgSearch(sel *Select, arg *Arg) error { + if sel.Where != nil && sel.Where.Op == OpTsQuery { + return nil + } + + ex := &Exp{ + Op: OpTsQuery, + Type: ValStr, + Val: arg.Val.Val, + } + + if sel.Where != nil { + sel.Where = &Exp{Op: OpAnd, Children: []*Exp{ex, sel.Where}} + } else { + sel.Where = ex + } + return nil +} + func (com *Compiler) compileArgWhere(sel *Select, arg *Arg) error { var err error