diff --git a/integration-tests/lts/select.test.ts b/integration-tests/lts/select.test.ts index 7c2c56da2..35f7e360f 100644 --- a/integration-tests/lts/select.test.ts +++ b/integration-tests/lts/select.test.ts @@ -500,6 +500,31 @@ describe("select", () => { ); }); + test("named tuples", async () => { + const namedTuple = e.tuple({ foo: e.str("bar") }); + const result = await e.select(namedTuple).run(client); + assert.deepEqual(result, { foo: "bar" }); + + const pathResult = await e.select(namedTuple.foo).run(client); + assert.deepEqual(pathResult, "bar"); + + const nestedObjectTuple = e.for( + e.enumerate(e.select(e.Hero)), + (enumeration) => + e.tuple({ + hero: enumeration[1], + index: enumeration[0], + }) + ); + const nestedObjectQuery = e.select(nestedObjectTuple.hero, (hero) => ({ + name: hero.name, + order_by: nestedObjectTuple.index, + })); + const nestedObjectResult = await nestedObjectQuery.run(client); + assert.equal(nestedObjectResult.length, 3); + assert.ok(nestedObjectResult.every((r) => Boolean(r.name))); + }); + test("filter by id", async () => { const result = await e .select(e.Hero, () => ({ diff --git a/integration-tests/stable/fts.test.ts b/integration-tests/stable/fts.test.ts new file mode 100644 index 000000000..ad45bbb52 --- /dev/null +++ b/integration-tests/stable/fts.test.ts @@ -0,0 +1,84 @@ +import type { Client } from "edgedb"; +import e, { type $infer } from "./dbschema/edgeql-js"; +import { setupTests, tc, teardownTests } from "./setupTeardown"; +import { $Post } from "./dbschema/edgeql-js/modules/default"; + +describe("full-text search", () => { + let client: Client; + beforeAll(async () => { + const setup = await setupTests(); + ({ client } = setup); + }); + + afterAll(async () => { + await teardownTests(client); + }, 10_000); + + test("basic fts", async () => { + const posts = [ + "Full-text search is a technique for searching text content. It works by storing every unique word that appears in a document.", + "To perform a full-text search, the search engine examines all the words in the specified document or set of documents.", + "The process of full-text search begins with the user entering a string of characters (the search string).", + "The search engine then retrieves all instances of the search string in the document or collection of documents.", + "Full-text search can be used in many applications that require search capability, such as web search engines, document management systems, and digital libraries.", + ]; + const inserted = await e + .params({ posts: e.array(e.str) }, (params) => { + return e.for(e.array_unpack(params.posts), (post) => + e.insert(e.Post, { text: post }) + ); + }) + .run(client, { posts }); + + const searchExpr = e.select(e.fts.search(e.Post, "search")); + + const allQuery = e.select(searchExpr, () => ({ + object: true, + score: true, + })); + const all = await allQuery.run(client); + + expect(all.length).toBe(inserted.length); + + tc.assert< + tc.IsExact< + $infer, + { + object: { id: string }; + score: number; + }[] + > + >(true); + + const filteredQuery = e.select(searchExpr, (post) => ({ + object: true, + score: true, + filter: e.op(post.score, ">", e.float64(0.81)), + })); + const filtered = await filteredQuery.run(client); + + expect(filtered.length).toBe(1); + + const noShapeQuery = e.select(searchExpr); + const noShape = await noShapeQuery.run(client); + + expect(noShape).toEqual(all); + + const objectSelectQuery = e.select(searchExpr.object, (post) => ({ + text: post.text, + order_by: searchExpr.score, + })); + const objectSelect = await objectSelectQuery.run(client); + expect(objectSelect).toEqual([ + { text: posts[0] }, + { text: posts[1] }, + { text: posts[2] }, + { text: posts[3] }, + { text: posts[4] }, + ]); + + tc.assert, { text: string }[]>>( + true + ); + }); +}); diff --git a/integration-tests/stable/package.json b/integration-tests/stable/package.json index 00c37b84f..c1bbb726a 100644 --- a/integration-tests/stable/package.json +++ b/integration-tests/stable/package.json @@ -15,7 +15,8 @@ "jest": "^29.5.0", "superjson": "^1.12.4", "ts-jest": "^29.1.0", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "edgedb": "^1.4.1" }, "dependencies": {} } diff --git a/packages/generate/src/edgeql-js/generateObjectTypes.ts b/packages/generate/src/edgeql-js/generateObjectTypes.ts index a85c145f2..f0ead4c0b 100644 --- a/packages/generate/src/edgeql-js/generateObjectTypes.ts +++ b/packages/generate/src/edgeql-js/generateObjectTypes.ts @@ -40,7 +40,7 @@ export const getStringRepresentation: ( } if (type.name === "anyobject") { return { - staticType: [`$.AnyObjectType`], + staticType: frag`${params.anytype ?? "$.AnyObjectType"}`, runtimeType: [], }; } diff --git a/packages/generate/src/syntax/hydrate.ts b/packages/generate/src/syntax/hydrate.ts index 1be07429d..ad6f5991f 100644 --- a/packages/generate/src/syntax/hydrate.ts +++ b/packages/generate/src/syntax/hydrate.ts @@ -98,7 +98,11 @@ export function makeType( ): T { const type = spec.get(id); - if (type.name === "anytype" || type.name === "std::anypoint") { + if ( + type.name === "anytype" || + type.name === "std::anypoint" || + type.name === "anyobject" + ) { if (anytype) return anytype as unknown as T; throw new Error("anytype not provided"); } diff --git a/packages/generate/src/syntax/path.ts b/packages/generate/src/syntax/path.ts index 0e03b98c0..9ba52ec8f 100644 --- a/packages/generate/src/syntax/path.ts +++ b/packages/generate/src/syntax/path.ts @@ -20,6 +20,7 @@ import { $toEdgeQL } from "./toEdgeQL"; import { $queryFunc, $queryFuncJSON } from "./query"; import type { + $expr_TuplePath, BaseType, Expression, LinkDesc, @@ -308,20 +309,20 @@ const pathifyProxyHandlers: ProxyHandler = { }, }; -export function $pathify( - _root: Root -): $pathify { +export function $pathify(_root: Root): $pathify { if (_root.__element__.__kind__ !== TypeKind.object) { - return _root as any; + return _root as $pathify; } - const root: $expr_PathNode = _root as any; + const root = _root as unknown as + | $expr_PathNode + | $expr_TuplePath; let pointers = { ...root.__element__.__pointers__, }; - if (root.__parent__) { + if (root.__parent__ && root.__kind__ !== ExpressionKind.TuplePath) { const { type, linkName } = root.__parent__; const parentPointer = type.__element__.__pointers__[linkName]; if (parentPointer?.__kind__ === "link") { diff --git a/packages/generate/src/syntax/select.ts b/packages/generate/src/syntax/select.ts index 60371e7fc..b05789a5f 100644 --- a/packages/generate/src/syntax/select.ts +++ b/packages/generate/src/syntax/select.ts @@ -35,6 +35,8 @@ import type { BaseType, ExclusiveTuple, orLiteralValue, + NamedTupleType, + $expr_TuplePath, } from "./typesystem"; import { @@ -111,7 +113,9 @@ export type exclusivesToFilterSingle = : orLiteralValue; }; }[number]; -export type SelectModifiers = { +export type SelectModifiers< + T extends ObjectType | NamedTupleType = ObjectType +> = { // export type SelectModifiers = { filter?: SelectFilterExpression; filter_single?: // | Partial< @@ -143,7 +147,9 @@ export type SelectModifiers = { // : never // : never; // }>) - exclusivesToFilterSingle | SelectFilterExpression; + T extends ObjectType + ? exclusivesToFilterSingle | SelectFilterExpression + : never; // | (ObjectType extends T // ? unknown @@ -368,7 +374,7 @@ export type InferOffsetLimitCardinality< // Modifiers // >; export type ComputeSelectCardinality< - Expr extends ObjectTypeExpression, + Expr extends ObjectTypeExpression | TypeSet, Modifiers extends UnknownSelectModifiers > = InferOffsetLimitCardinality< undefined extends Modifiers["filter_single"] @@ -780,6 +786,21 @@ export type objectTypeToSelectShape = : any; }> & { [k: string]: unknown }; +export type namedTupleTypeToSelectShape< + T extends NamedTupleType = NamedTupleType +> = Partial< + { + [k in keyof T["__shape__"]]: T["__shape__"][k] extends PropertyDesc + ? + | boolean + | TypeSet< + T["__shape__"][k]["target"], + cardutil.assignable + > + : any; + } & { [k: string]: unknown } +>; + // incorporate __shape__ (computeds) on selection shapes // this works but a major rewrite of setToTsType is required // to incorporate __shape__-based selection shapes into @@ -902,6 +923,21 @@ export function select< >; __cardinality__: ComputeSelectCardinality; }>; + +export function select< + Expr extends TypeSet, + Shape extends namedTupleTypeToSelectShape & + SelectModifiers, + Modifiers extends UnknownSelectModifiers = Pick +>( + expr: Expr, + shape: ( + scope: $expr_TuplePath + ) => Readonly +): $expr_Select<{ + __element__: NamedTupleType; + __cardinality__: ComputeSelectCardinality; +}>; /* For the moment is isn't possible to implement both closure-based and plain @@ -1117,7 +1153,10 @@ function resolveShape( } else { // for scalar expressions, scope === expr // shape keys are not allowed - if (expr.__element__.__kind__ !== TypeKind.object) { + if ( + expr.__element__.__kind__ !== TypeKind.object && + expr.__element__.__kind__ !== TypeKind.namedtuple + ) { throw new Error( `Invalid select shape key '${key}' on scalar expression, ` + `only modifiers are allowed (filter, order_by, offset and limit)` diff --git a/packages/generate/src/syntax/toEdgeQL.ts b/packages/generate/src/syntax/toEdgeQL.ts index 98d0afe90..c4c0dd3b0 100644 --- a/packages/generate/src/syntax/toEdgeQL.ts +++ b/packages/generate/src/syntax/toEdgeQL.ts @@ -149,13 +149,16 @@ export function $toEdgeQL(this: any) { if (withVars.has(expr)) { continue; } - // ignore unbound leaves, nodes, and intersections // these should be rendered as is if ( !refData.boundScope && (expr.__kind__ === ExpressionKind.PathLeaf || expr.__kind__ === ExpressionKind.PathNode || + expr.__kind__ === ExpressionKind.TuplePath || + (expr.__kind__ === ExpressionKind.Select && + (expr.__expr__ as SomeExpression).__kind__ === + ExpressionKind.TuplePath) || expr.__kind__ === ExpressionKind.TypeIntersection) ) { continue;