diff --git a/integration-tests/lts/primitives.test.ts b/integration-tests/lts/primitives.test.ts index 9f3edcc08..85b87b2b0 100644 --- a/integration-tests/lts/primitives.test.ts +++ b/integration-tests/lts/primitives.test.ts @@ -66,7 +66,7 @@ describe("primitives", () => { ); assert.equal( e.std.range(upperRange).toEdgeQL(), - `std::range({}, 8, inc_lower := true, inc_upper := false)` + `std::range({}, 8, inc_lower := false, inc_upper := false)` ); assert.equal( e.std.range(dateRange).toEdgeQL(), diff --git a/integration-tests/lts/select.test.ts b/integration-tests/lts/select.test.ts index 173428ffb..7c2c56da2 100644 --- a/integration-tests/lts/select.test.ts +++ b/integration-tests/lts/select.test.ts @@ -310,6 +310,7 @@ describe("select", () => { test("* in polymorphic", async () => { const q = e.select(e.Person, () => ({ ...e.is(e.Hero, e.Hero["*"]), + name: true, })); // 'id' is filtered out since it is not valid in a polymorphic expr @@ -317,7 +318,7 @@ describe("select", () => { tc.IsExact< $infer, { - name: string | null; + name: string; height: string | null; number_of_movies: number | null; secret_identity: string | null; diff --git a/integration-tests/nightly/dbschema/default.esdl b/integration-tests/nightly/dbschema/default.esdl index 4bda52523..f3267b234 100644 --- a/integration-tests/nightly/dbschema/default.esdl +++ b/integration-tests/nightly/dbschema/default.esdl @@ -1,3 +1,5 @@ module default { - + type WithMultiRange { + required ranges: multirange; + }; } diff --git a/integration-tests/nightly/dbschema/migrations/00001.edgeql b/integration-tests/nightly/dbschema/migrations/00001.edgeql new file mode 100644 index 000000000..6ea7e73bf --- /dev/null +++ b/integration-tests/nightly/dbschema/migrations/00001.edgeql @@ -0,0 +1,7 @@ +CREATE MIGRATION m1rlwpc5ikrkb7cvylhbcntglvnanm524yb6si5xlcjk6gd2lczugq + ONTO initial +{ + CREATE TYPE default::WithMultiRange { + CREATE REQUIRED PROPERTY ranges: multirange; + }; +}; diff --git a/integration-tests/nightly/multirange.test.ts b/integration-tests/nightly/multirange.test.ts new file mode 100644 index 000000000..9d6e5db32 --- /dev/null +++ b/integration-tests/nightly/multirange.test.ts @@ -0,0 +1,45 @@ +import type { Client, MultiRange } from "edgedb"; +import e from "./dbschema/edgeql-js"; +import { setupTests, tc, teardownTests } from "./setupTeardown"; + +import type { WithMultiRange } from "./dbschema/interfaces"; + +interface BaseObject { + id: string; +} +interface test_WithMultiRange extends BaseObject { + ranges: MultiRange; +} + +describe("multirange", () => { + let client: Client; + beforeAll(async () => { + const setup = await setupTests(); + ({ client } = setup); + }); + + afterAll(async () => { + await teardownTests(client); + }, 10_000); + + test("check generated interfaces", () => { + tc.assert>(true); + }); + + test("inferred return type + literal encoding", async () => { + const query = e.select(e.WithMultiRange, () => ({ + ranges: true, + })); + + const result = await query.run(client); + + tc.assert< + tc.IsExact< + typeof result, + { + ranges: MultiRange; + }[] + > + >(true); + }); +}); diff --git a/integration-tests/nightly/setupTeardown.ts b/integration-tests/nightly/setupTeardown.ts index f57bdb82a..d7adbe02a 100644 --- a/integration-tests/nightly/setupTeardown.ts +++ b/integration-tests/nightly/setupTeardown.ts @@ -17,6 +17,7 @@ export async function setupTests() { async function cleanupData(client: Client) { await client.execute(` # Delete any user-defined objects here +delete WithMultiRange; `); } diff --git a/packages/driver/genErrors.js b/packages/driver/genErrors.mjs similarity index 91% rename from packages/driver/genErrors.js rename to packages/driver/genErrors.mjs index 143f4e8dd..7a29acb29 100644 --- a/packages/driver/genErrors.js +++ b/packages/driver/genErrors.mjs @@ -16,11 +16,11 @@ * limitations under the License. */ -const fs = require("fs"); -const path = require("path"); - -const getStdin = require("get-stdin"); -const prettier = require("prettier"); +import { fileURLToPath, URL } from "url"; +import fs from "node:fs"; +import path from "node:path"; +import getStdin from "get-stdin"; +import prettier from "prettier"; class Buffer { constructor() { @@ -39,6 +39,8 @@ class Buffer { return this.buf.join("\n"); } } +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const __filename = new URL("", import.meta.url).pathname; (async () => { const prettierOptions = (await prettier.resolveConfig(__dirname)) ?? {}; diff --git a/packages/driver/package.json b/packages/driver/package.json index baf397aeb..dccd94cf6 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -43,7 +43,7 @@ "test": "npx --node-options='--experimental-fetch' jest --detectOpenHandles", "lint": "tslint 'packages/*/src/**/*.ts'", "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", - "gen-errors": "edb gen-errors-json --client | node genErrors.js", + "gen-errors": "edb gen-errors-json --client | node genErrors.mjs", "watch": "nodemon -e js,ts,tsx --ignore dist -x ", "dev": "yarn tsc --project tsconfig.json --incremental && yarn build:deno" } diff --git a/packages/driver/src/codecs/array.ts b/packages/driver/src/codecs/array.ts index c497b6d99..c2b8881b1 100644 --- a/packages/driver/src/codecs/array.ts +++ b/packages/driver/src/codecs/array.ts @@ -16,10 +16,11 @@ * limitations under the License. */ -import { ICodec, Codec, ScalarCodec, uuid, CodecKind } from "./ifaces"; +import type { ICodec, uuid, CodecKind } from "./ifaces"; +import { Codec, ScalarCodec } from "./ifaces"; import { WriteBuffer, ReadBuffer } from "../primitives/buffer"; import { TupleCodec } from "./tuple"; -import { RangeCodec } from "./range"; +import { MultiRangeCodec, RangeCodec } from "./range"; import { InvalidArgumentError, ProtocolError } from "../errors"; import { NamedTupleCodec } from "./namedtuple"; @@ -39,7 +40,8 @@ export class ArrayCodec extends Codec implements ICodec { this.subCodec instanceof ScalarCodec || this.subCodec instanceof TupleCodec || this.subCodec instanceof NamedTupleCodec || - this.subCodec instanceof RangeCodec + this.subCodec instanceof RangeCodec || + this.subCodec instanceof MultiRangeCodec ) ) { throw new InvalidArgumentError( @@ -48,7 +50,9 @@ export class ArrayCodec extends Codec implements ICodec { } if (!Array.isArray(obj) && !isTypedArray(obj)) { - throw new InvalidArgumentError("an array was expected"); + throw new InvalidArgumentError( + `an array was expected (got type ${obj.constructor.name})` + ); } const subCodec = this.subCodec; diff --git a/packages/driver/src/codecs/consts.ts b/packages/driver/src/codecs/consts.ts index a3ee7be14..45fa7d9cd 100644 --- a/packages/driver/src/codecs/consts.ts +++ b/packages/driver/src/codecs/consts.ts @@ -24,6 +24,7 @@ export const INVALID_CODEC_ID = "ffffffffffffffffffffffffffffffff"; export const KNOWN_TYPES = new Map([ ["00000000000000000000000000000001", "anytype"], ["00000000000000000000000000000002", "anytuple"], + ["00000000000000000000000000000003", "anyobject"], ["000000000000000000000000000000f0", "std"], ["000000000000000000000000000000ff", "empty-tuple"], ["00000000000000000000000000000100", "std::uuid"], diff --git a/packages/driver/src/codecs/ifaces.ts b/packages/driver/src/codecs/ifaces.ts index a9b9ff2fa..bde43efc1 100644 --- a/packages/driver/src/codecs/ifaces.ts +++ b/packages/driver/src/codecs/ifaces.ts @@ -29,7 +29,8 @@ export type CodecKind = | "set" | "scalar" | "sparse_object" - | "range"; + | "range" + | "multirange"; export interface ICodec { readonly tid: uuid; diff --git a/packages/driver/src/codecs/range.ts b/packages/driver/src/codecs/range.ts index e067996e2..4e0284d1b 100644 --- a/packages/driver/src/codecs/range.ts +++ b/packages/driver/src/codecs/range.ts @@ -16,10 +16,11 @@ * limitations under the License. */ -import { ICodec, Codec, uuid, CodecKind } from "./ifaces"; +import type { ICodec, uuid, CodecKind } from "./ifaces"; +import { Codec } from "./ifaces"; import { WriteBuffer, ReadBuffer } from "../primitives/buffer"; -import { Range } from "../datatypes/range"; -import { InvalidArgumentError } from "../errors"; +import { MultiRange, Range } from "../datatypes/range"; +import { InvalidArgumentError, ProtocolError } from "../errors"; enum RangeFlags { EMPTY = 1 << 0, @@ -29,6 +30,68 @@ enum RangeFlags { EMPTY_UPPER = 1 << 4, } +const MAXINT32 = 0x7fffffff; + +function encodeRange(buf: WriteBuffer, obj: any, subCodec: ICodec): void { + if (!(obj instanceof Range)) { + throw new InvalidArgumentError("a Range was expected"); + } + + const elemData = new WriteBuffer(); + + if (obj.lower !== null) { + subCodec.encode(elemData, obj.lower); + } + if (obj.upper !== null) { + subCodec.encode(elemData, obj.upper); + } + + const elemBuf = elemData.unwrap(); + + buf.writeInt32(1 + elemBuf.length); + buf.writeUInt8( + obj.isEmpty + ? RangeFlags.EMPTY + : (obj.incLower ? RangeFlags.INC_LOWER : 0) | + (obj.incUpper ? RangeFlags.INC_UPPER : 0) | + (obj.lower === null ? RangeFlags.EMPTY_LOWER : 0) | + (obj.upper === null ? RangeFlags.EMPTY_UPPER : 0) + ); + buf.writeBuffer(elemBuf); +} + +function decodeRange(buf: ReadBuffer, subCodec: ICodec): any { + const flags = buf.readUInt8(); + + if (flags & RangeFlags.EMPTY) { + return Range.empty(); + } + + const elemBuf = ReadBuffer.alloc(); + + let lower: any = null; + let upper: any = null; + + if (!(flags & RangeFlags.EMPTY_LOWER)) { + buf.sliceInto(elemBuf, buf.readInt32()); + lower = subCodec.decode(elemBuf); + elemBuf.finish(); + } + + if (!(flags & RangeFlags.EMPTY_UPPER)) { + buf.sliceInto(elemBuf, buf.readInt32()); + upper = subCodec.decode(elemBuf); + elemBuf.finish(); + } + + return new Range( + lower, + upper, + !!(flags & RangeFlags.INC_LOWER), + !!(flags & RangeFlags.INC_UPPER) + ); +} + export class RangeCodec extends Codec implements ICodec { private subCodec: ICodec; @@ -37,66 +100,101 @@ export class RangeCodec extends Codec implements ICodec { this.subCodec = subCodec; } + encode(buf: WriteBuffer, obj: any) { + return encodeRange(buf, obj, this.subCodec); + } + + decode(buf: ReadBuffer): any { + return decodeRange(buf, this.subCodec); + } + + getSubcodecs(): ICodec[] { + return [this.subCodec]; + } + + getKind(): CodecKind { + return "range"; + } +} + +export class MultiRangeCodec extends Codec implements ICodec { + private subCodec: ICodec; + + constructor(tid: uuid, subCodec: ICodec) { + super(tid); + this.subCodec = subCodec; + } + encode(buf: WriteBuffer, obj: any): void { - if (!(obj instanceof Range)) { - throw new InvalidArgumentError("a Range was expected"); + if (!(obj instanceof MultiRange)) { + throw new TypeError( + `a MultiRange expected (got type ${obj.constructor.name})` + ); } - const subCodec = this.subCodec; - const elemData = new WriteBuffer(); - - if (obj.lower !== null) { - subCodec.encode(elemData, obj.lower); + const objLen = obj.length; + if (objLen > MAXINT32) { + throw new InvalidArgumentError("too many elements in array"); } - if (obj.upper !== null) { - subCodec.encode(elemData, obj.upper); + + const elemData = new WriteBuffer(); + for (const item of obj) { + try { + encodeRange(elemData, item, this.subCodec); + } catch (e) { + if (e instanceof InvalidArgumentError) { + throw new InvalidArgumentError( + `invalid multirange element: ${e.message}` + ); + } else { + throw e; + } + } } const elemBuf = elemData.unwrap(); + const elemDataLen = elemBuf.length; + if (elemDataLen > MAXINT32 - 4) { + throw new InvalidArgumentError( + `size of encoded multirange datum exceeds the maximum allowed ${ + MAXINT32 - 4 + } bytes` + ); + } + + // Datum length + buf.writeInt32(4 + elemDataLen); - buf.writeInt32(1 + elemBuf.length); - buf.writeUInt8( - obj.isEmpty - ? RangeFlags.EMPTY - : (obj.incLower ? RangeFlags.INC_LOWER : 0) | - (obj.incUpper ? RangeFlags.INC_UPPER : 0) | - (obj.lower === null ? RangeFlags.EMPTY_LOWER : 0) | - (obj.upper === null ? RangeFlags.EMPTY_UPPER : 0) - ); + // Number of elements in multirange + buf.writeInt32(objLen); buf.writeBuffer(elemBuf); } decode(buf: ReadBuffer): any { - const flags = buf.readUInt8(); - - if (flags & RangeFlags.EMPTY) { - return Range.empty(); - } - + const elemCount = buf.readInt32(); + const result = new Array(elemCount); const elemBuf = ReadBuffer.alloc(); const subCodec = this.subCodec; - let lower: any = null; - let upper: any = null; - - if (!(flags & RangeFlags.EMPTY_LOWER)) { - buf.sliceInto(elemBuf, buf.readInt32()); - lower = subCodec.decode(elemBuf); - elemBuf.finish(); + for (let i = 0; i < elemCount; i++) { + const elemLen = buf.readInt32(); + if (elemLen === -1) { + throw new ProtocolError("unexpected NULL element in multirange value"); + } else { + buf.sliceInto(elemBuf, elemLen); + const elem = decodeRange(elemBuf, subCodec); + if (elemBuf.length) { + throw new ProtocolError( + `unexpected trailing data in buffer after multirange element decoding: ${elemBuf.length}` + ); + } + + result[i] = elem; + elemBuf.finish(); + } } - if (!(flags & RangeFlags.EMPTY_UPPER)) { - buf.sliceInto(elemBuf, buf.readInt32()); - upper = subCodec.decode(elemBuf); - elemBuf.finish(); - } - - return new Range( - lower, - upper, - !!(flags & RangeFlags.INC_LOWER), - !!(flags & RangeFlags.INC_UPPER) - ); + return new MultiRange(result); } getSubcodecs(): ICodec[] { @@ -104,6 +202,6 @@ export class RangeCodec extends Codec implements ICodec { } getKind(): CodecKind { - return "range"; + return "multirange"; } } diff --git a/packages/driver/src/codecs/registry.ts b/packages/driver/src/codecs/registry.ts index d5f861552..b951f31e0 100644 --- a/packages/driver/src/codecs/registry.ts +++ b/packages/driver/src/codecs/registry.ts @@ -30,7 +30,7 @@ import { NamedTupleCodec } from "./namedtuple"; import { EnumCodec } from "./enum"; import { ObjectCodec } from "./object"; import { SetCodec } from "./set"; -import { RangeCodec } from "./range"; +import { MultiRangeCodec, RangeCodec } from "./range"; import { ProtocolVersion } from "../ifaces"; import { versionGreaterThanOrEqual } from "../utils"; import { SparseObjectCodec } from "./sparseObject"; @@ -49,6 +49,7 @@ const CTYPE_ARRAY = 6; const CTYPE_ENUM = 7; const CTYPE_INPUT_SHAPE = 8; const CTYPE_RANGE = 9; +const CTYPE_MULTIRANGE = 12; export interface CustomCodecSpec { int64_bigint?: boolean; @@ -446,6 +447,18 @@ export class CodecsRegistry { res = new RangeCodec(tid, subCodec); break; } + + case CTYPE_MULTIRANGE: { + const pos = frb.readUInt16(); + const subCodec = cl[pos]; + if (subCodec == null) { + throw new ProtocolError( + "could not build range codec: missing subcodec" + ); + } + res = new MultiRangeCodec(tid, subCodec); + break; + } } if (res == null) { diff --git a/packages/driver/src/datatypes/range.ts b/packages/driver/src/datatypes/range.ts index 7593ca83e..df6be3f75 100644 --- a/packages/driver/src/datatypes/range.ts +++ b/packages/driver/src/datatypes/range.ts @@ -16,7 +16,7 @@ * limitations under the License. */ -import { Duration, LocalDate, LocalDateTime } from "./datetime"; +import type { Duration, LocalDate, LocalDateTime } from "./datetime"; export class Range< T extends number | Date | LocalDate | LocalDateTime | Duration @@ -26,7 +26,7 @@ export class Range< constructor( private readonly _lower: T | null, private readonly _upper: T | null, - private readonly _incLower: boolean = true, + private readonly _incLower: boolean = _lower != null, private readonly _incUpper: boolean = false ) {} @@ -63,3 +63,26 @@ export class Range< }; } } + +export class MultiRange< + T extends number | Date | LocalDate | LocalDateTime | Duration +> { + private readonly _ranges: Range[]; + constructor(ranges: Range[] = []) { + this._ranges = [...ranges]; + } + + get length() { + return this._ranges.length; + } + + *[Symbol.iterator]() { + for (const range of this._ranges) { + yield range; + } + } + + toJSON() { + return [...this._ranges]; + } +} diff --git a/packages/driver/src/errors/index.ts b/packages/driver/src/errors/index.ts index 46e88b92a..ff002002e 100644 --- a/packages/driver/src/errors/index.ts +++ b/packages/driver/src/errors/index.ts @@ -511,6 +511,32 @@ export class BackendUnavailableError extends AvailabilityError { } } +export class ServerOfflineError extends AvailabilityError { + protected static tags = { + [tags.SHOULD_RECONNECT]: true, + [tags.SHOULD_RETRY]: true, + }; + get code(): number { + return 0x08_00_00_02; + } +} + +export class UnknownTenantError extends AvailabilityError { + protected static tags = { + [tags.SHOULD_RECONNECT]: true, + [tags.SHOULD_RETRY]: true, + }; + get code(): number { + return 0x08_00_00_03; + } +} + +export class ServerBlockedError extends AvailabilityError { + get code(): number { + return 0x08_00_00_04; + } +} + export class BackendError extends EdgeDBError { get code(): number { return 0x09_00_00_00; diff --git a/packages/driver/src/errors/map.ts b/packages/driver/src/errors/map.ts index 725ec8ffd..ff60cde4c 100644 --- a/packages/driver/src/errors/map.ts +++ b/packages/driver/src/errors/map.ts @@ -103,6 +103,9 @@ errorMapping.set(0x07_00_00_00, errors.AccessError); errorMapping.set(0x07_01_00_00, errors.AuthenticationError); errorMapping.set(0x08_00_00_00, errors.AvailabilityError); errorMapping.set(0x08_00_00_01, errors.BackendUnavailableError); +errorMapping.set(0x08_00_00_02, errors.ServerOfflineError); +errorMapping.set(0x08_00_00_03, errors.UnknownTenantError); +errorMapping.set(0x08_00_00_04, errors.ServerBlockedError); errorMapping.set(0x09_00_00_00, errors.BackendError); errorMapping.set(0x09_00_01_00, errors.UnsupportedBackendFeatureError); errorMapping.set(0xf0_00_00_00, errors.LogMessage); diff --git a/packages/driver/src/index.shared.ts b/packages/driver/src/index.shared.ts index c411b92fd..a12976798 100644 --- a/packages/driver/src/index.shared.ts +++ b/packages/driver/src/index.shared.ts @@ -27,14 +27,14 @@ export { DateDuration, } from "./datatypes/datetime"; export { ConfigMemory } from "./datatypes/memory"; -export { Range } from "./datatypes/range"; +export { Range, MultiRange } from "./datatypes/range"; export type { Executor } from "./ifaces"; export * from "./errors"; /* Private APIs */ -import * as codecs from "./codecs/ifaces"; +import type * as codecs from "./codecs/ifaces"; import * as reg from "./codecs/registry"; import * as buf from "./primitives/buffer"; export const _CodecsRegistry = reg.CodecsRegistry; diff --git a/packages/driver/src/reflection/analyzeQuery.ts b/packages/driver/src/reflection/analyzeQuery.ts index 63cd7a8e1..5c169d7ab 100644 --- a/packages/driver/src/reflection/analyzeQuery.ts +++ b/packages/driver/src/reflection/analyzeQuery.ts @@ -5,7 +5,7 @@ import { EnumCodec } from "../codecs/enum"; import { ICodec, ScalarCodec } from "../codecs/ifaces"; import { NamedTupleCodec } from "../codecs/namedtuple"; import { ObjectCodec } from "../codecs/object"; -import { RangeCodec } from "../codecs/range"; +import { MultiRangeCodec, RangeCodec } from "../codecs/range"; import { NullCodec } from "../codecs/codecs"; import { SetCodec } from "../codecs/set"; import { TupleCodec } from "../codecs/tuple"; @@ -142,7 +142,15 @@ function walkCodec( throw Error("expected range subtype to be scalar type"); } ctx.imports.add("Range"); - return `Range<${subCodec.tsType}>`; + return `Range<${walkCodec(subCodec, ctx)}>`; + } + if (codec instanceof MultiRangeCodec) { + const subCodec = codec.getSubcodecs()[0]; + if (!(subCodec instanceof ScalarCodec)) { + throw Error("expected multirange subtype to be scalar type"); + } + ctx.imports.add("MultiRange"); + return `MultiRange<${walkCodec(subCodec, ctx)}>`; } throw Error(`Unexpected codec kind: ${codec.getKind()}`); } diff --git a/packages/driver/src/reflection/enums.ts b/packages/driver/src/reflection/enums.ts index 271ddaf1c..4697f60c0 100644 --- a/packages/driver/src/reflection/enums.ts +++ b/packages/driver/src/reflection/enums.ts @@ -15,6 +15,7 @@ export enum TypeKind { tuple = "tuple", array = "array", range = "range", + multirange = "multirange", } export enum ExpressionKind { diff --git a/packages/driver/src/reflection/queries/types.ts b/packages/driver/src/reflection/queries/types.ts index 2dc31dad1..f5647d6ef 100644 --- a/packages/driver/src/reflection/queries/types.ts +++ b/packages/driver/src/reflection/queries/types.ts @@ -1,5 +1,5 @@ -import { Executor } from "../../ifaces"; -import { Cardinality } from "../enums"; +import type { Executor } from "../../ifaces"; +import type { Cardinality } from "../enums"; import type { UUID } from "./queryTypes"; import { StrictMap } from "../strictMap"; @@ -27,6 +27,7 @@ export type TypeKind = | "array" | "tuple" | "range" + | "multirange" | "unknown"; export type TypeProperties = { @@ -73,7 +74,17 @@ export type RangeType = TypeProperties<"range"> & { is_abstract: boolean; }; -export type PrimitiveType = ScalarType | ArrayType | TupleType | RangeType; +export type MultiRangeType = TypeProperties<"multirange"> & { + multirange_element_id: UUID; + is_abstract: boolean; +}; + +export type PrimitiveType = + | ScalarType + | ArrayType + | TupleType + | RangeType + | MultiRangeType; export type Type = PrimitiveType | ObjectType; export type Types = StrictMap; @@ -120,6 +131,7 @@ export async function getTypes( `select sys::get_version().major;` ); const v2Plus = version >= 2; + const v4Plus = version >= 4; const QUERY = ` WITH MODULE schema, @@ -144,6 +156,7 @@ export async function getTypes( 'array' IF Type IS Array ELSE 'tuple' IF Type IS Tuple ELSE ${v2Plus ? `'range' IF Type IS Range ELSE` : ``} + ${v4Plus ? `'multirange' IF Type IS MultiRange ELSE` : ``} 'unknown', [IS ScalarType].enum_values, @@ -224,6 +237,11 @@ export async function getTypes( name } ORDER BY @index ASC), ${v2Plus ? `range_element_id := [IS Range].element_type.id,` : ``} + ${ + v4Plus + ? `multirange_element_id := [IS MultiRange].element_type.id,` + : `` + } } ORDER BY .name; `; @@ -261,6 +279,11 @@ export async function getTypes( typeMapping.get(type.material_id)?.id ?? type.material_id; } break; + case "multirange": + type.multirange_element_id = + typeMapping.get(type.multirange_element_id)?.id ?? + type.multirange_element_id; + break; case "range": type.range_element_id = typeMapping.get(type.range_element_id)?.id ?? type.range_element_id; diff --git a/packages/driver/src/reflection/reservedKeywords.ts b/packages/driver/src/reflection/reservedKeywords.ts index 26f0c68ec..9bf459235 100644 --- a/packages/driver/src/reflection/reservedKeywords.ts +++ b/packages/driver/src/reflection/reservedKeywords.ts @@ -12,6 +12,7 @@ export const reservedKeywords = new Set([ "anyarray", "anytuple", "anytype", + "anyobject", "begin", "case", "check", diff --git a/packages/driver/test/client.test.ts b/packages/driver/test/client.test.ts index 351f26ee8..c3a955f97 100644 --- a/packages/driver/test/client.test.ts +++ b/packages/driver/test/client.test.ts @@ -18,15 +18,15 @@ import fc from "fast-check"; import { parseConnectArguments } from "../src/conUtils.server"; +import type { Client, Executor, _ICodec } from "../src/index.node"; import { - Client, DivisionByZeroError, Duration, EdgeDBError, - Executor, LocalDate, LocalDateTime, Range, + MultiRange, MissingRequiredError, NoDataError, RelativeDuration, @@ -34,14 +34,13 @@ import { QueryArgumentError, _CodecsRegistry, _ReadBuffer, - _ICodec, Session, AuthenticationError, InvalidReferenceError, } from "../src/index.node"; import { AdminUIFetchConnection } from "../src/fetchConn"; -import { CustomCodecSpec } from "../src/codecs/registry"; +import type { CustomCodecSpec } from "../src/codecs/registry"; import { getAvailableFeatures, getClient, @@ -1224,32 +1223,32 @@ test("fetch: ConfigMemory", async () => { } }); -if (getEdgeDBVersion().major >= 2) { - function expandRangeEQL(lower: string, upper: string) { - return [ - [false, false], - [true, false], - [false, true], - [true, true], - ] - .map( - ([incl, incu]) => - `range(${lower}, ${upper}, inc_lower := ${incl}, inc_upper := ${incu})` - ) - .join(",\n"); - } +function expandRangeEQL(lower: string, upper: string) { + return [ + [false, false], + [true, false], + [false, true], + [true, true], + ] + .map( + ([incl, incu]) => + `range(${lower}, ${upper}, inc_lower := ${incl}, inc_upper := ${incu})` + ) + .join(",\n"); +} - function expandRangeJS(lower: any, upper: any) { - return [ - new Range(lower, upper, false, false), - new Range(lower, upper, true, false), - new Range(lower, upper, false, true), - new Range(lower, upper, true, true), - ]; - } +function expandRangeJS(lower: any, upper: any) { + return [ + new Range(lower, upper, false, false), + new Range(lower, upper, true, false), + new Range(lower, upper, false, true), + new Range(lower, upper, true, true), + ]; +} +if (getEdgeDBVersion().major >= 2) { test("fetch: ranges", async () => { - const client = await getClient(); + const client = getClient(); try { const res = await client.querySingle(` @@ -1360,6 +1359,25 @@ if (getEdgeDBVersion().major >= 2) { }); } +if (getEdgeDBVersion().major >= 4) { + test("fetch: multirange", async () => { + const client = getClient(); + try { + const expected = new MultiRange([new Range(1, 2)]); + const multiRangeRes = await client.query>( + "select >$mr;", + { + mr: expected, + } + ); + + expect(multiRangeRes).toEqual([expected]); + } finally { + await client.close(); + } + }); +} + test("fetch: tuple", async () => { const con = getClient(); let res: any; diff --git a/packages/generate/src/edgeql-js.ts b/packages/generate/src/edgeql-js.ts index 3678b429d..3414ae22b 100644 --- a/packages/generate/src/edgeql-js.ts +++ b/packages/generate/src/edgeql-js.ts @@ -121,21 +121,29 @@ export async function generateQueryBuilder(params: { operators, edgedbVersion: version, }; + console.log("Generating runtime spec..."); generateRuntimeSpec(generatorParams); + console.log("Generating cast maps..."); generateCastMaps(generatorParams); + console.log("Generating scalars..."); generateScalars(generatorParams); + console.log("Generating object types..."); generateObjectTypes(generatorParams); + console.log("Generating function types..."); generateFunctionTypes(generatorParams); + console.log("Generating operators..."); generateOperators(generatorParams); + console.log("Generating set impl..."); generateSetImpl(generatorParams); + console.log("Generating globals..."); generateGlobals(generatorParams); - // TODO: Fix 'fts' module generation properly, for now we just disable - // output of this module - dir._modules.delete("fts"); - dir._map.delete("modules/fts"); - // ----------------- + if (version.major < 4) { + dir._modules.delete("fts"); + dir._map.delete("modules/fts"); + } + console.log("Generating index..."); generateIndex(generatorParams); // generate module imports diff --git a/packages/generate/src/edgeql-js/generateCastMaps.ts b/packages/generate/src/edgeql-js/generateCastMaps.ts index 2b32cd94b..7f36880cc 100644 --- a/packages/generate/src/edgeql-js/generateCastMaps.ts +++ b/packages/generate/src/edgeql-js/generateCastMaps.ts @@ -267,7 +267,7 @@ export const generateCastMaps = (params: GeneratorParams) => { dts`declare `, t`type scalarLiterals =\n | ${Object.keys(literalToScalarMapping).join( "\n | " - )}\n | edgedb.Range;\n\n`, + )}\n | edgedb.Range | edgedb.MultiRange;\n\n`, ]); f.writeln([ @@ -296,6 +296,8 @@ export const generateCastMaps = (params: GeneratorParams) => { f.writeln([t` : T["__tstype__"]`]); f.writeln([t` : T extends $.RangeType`]); f.writeln([t` ? edgedb.Range`]); + f.writeln([t` : T extends $.MultiRangeType`]); + f.writeln([t` ? edgedb.MultiRange`]); f.writeln([t` : never;`]); f.writeln([ t`export `, @@ -344,6 +346,9 @@ export const generateCastMaps = (params: GeneratorParams) => { f.writeln([ t` T extends edgedb.Range ? $.RangeType> :`, ]); + f.writeln([ + t` T extends edgedb.MultiRange ? $.MultiRangeType> :`, + ]); f.writeln([t` $.BaseType;\n\n`]); f.writeln([ diff --git a/packages/generate/src/edgeql-js/generateObjectTypes.ts b/packages/generate/src/edgeql-js/generateObjectTypes.ts index 0a1b16a5c..a85c145f2 100644 --- a/packages/generate/src/edgeql-js/generateObjectTypes.ts +++ b/packages/generate/src/edgeql-js/generateObjectTypes.ts @@ -38,6 +38,12 @@ export const getStringRepresentation: ( runtimeType: [], }; } + if (type.name === "anyobject") { + return { + staticType: [`$.AnyObjectType`], + runtimeType: [], + }; + } if (type.name === "std::anypoint") { return { staticType: frag`${params.anytype ?? getRef("std::anypoint")}`, @@ -150,8 +156,19 @@ export const getStringRepresentation: ( .runtimeType })`, }; + } else if (type.kind === "multirange") { + return { + staticType: frag`$.MultiRangeType<${ + getStringRepresentation(types.get(type.multirange_element_id), params) + .staticType + }>`, + runtimeType: frag`$.MultiRangeType(${ + getStringRepresentation(types.get(type.multirange_element_id), params) + .runtimeType + })`, + }; } else { - throw new Error("Invalid type"); + throw new Error(`Invalid type: ${JSON.stringify(type, null, 2)}`); } }; diff --git a/packages/generate/src/funcoputil.ts b/packages/generate/src/funcoputil.ts index 6c33ba3e3..eef810a62 100644 --- a/packages/generate/src/funcoputil.ts +++ b/packages/generate/src/funcoputil.ts @@ -36,10 +36,13 @@ export function expandFuncopAnytypeOverloads( // create an anytype overload for 'range' so 'anypoint' // gets inferred to the same type in return type - const anypointParams = [ + const paramsList = [ ...overload.params.positional, ...overload.params.named, - ].filter((param) => param.type.name.includes("anypoint")); + ]; + const anypointParams = paramsList.filter((param) => + param.type.name.includes("anypoint") + ); if (anypointParams.length) { return [ { @@ -54,6 +57,23 @@ export function expandFuncopAnytypeOverloads( }, ]; } + const anyobjectParams = paramsList.filter((param) => + param.type.name.includes("anyobject") + ); + if (anyobjectParams.length) { + return [ + { + ...overload, + anytypes: { + kind: "noncastable" as const, + type: ["$.ObjectType"], + typeObj: anyobjectParams[0].type, + refName: anyobjectParams[0].typeName, + refPath: findPathOfAnytype(anyobjectParams[0].type.id, types), + }, + }, + ]; + } // Each overload with 'anytype' params is expanded into several overloads: // - overload for each implicitly castable root type union @@ -73,10 +93,9 @@ export function expandFuncopAnytypeOverloads( // other params reference first param type // - return anytype: references first param type - const anytypeParams = [ - ...overload.params.positional, - ...overload.params.named, - ].filter((param) => param.type.name.includes("anytype")); + const anytypeParams = paramsList.filter((param) => + param.type.name.includes("anytype") + ); if (anytypeParams.length) { const hasArrayType = @@ -204,7 +223,11 @@ function _findPathOfAnytype( ): string | null { const type = types.get(typeId); - if (type.name === "anytype" || type.name === "anypoint") { + if ( + type.name === "anytype" || + type.name === "anypoint" || + type.name === "anyobject" + ) { return '["__element__"]'; } if (type.kind === "array") { @@ -220,7 +243,7 @@ function _findPathOfAnytype( return `[${isNamed ? quote(name) : name}]${elPath}`; } } - } else if (type.kind === "range") { + } else if (type.kind === "range" || type.kind === "multirange") { return `["__element__"]["__element__"]`; } diff --git a/packages/generate/src/genutil.ts b/packages/generate/src/genutil.ts index bb49b19cc..b5e4ed715 100644 --- a/packages/generate/src/genutil.ts +++ b/packages/generate/src/genutil.ts @@ -213,6 +213,15 @@ export function toTSScalarType( return frag`${opts.edgedbDatatypePrefix}edgedb.Range<${tn}>`; } + case "multirange": { + const tn = toTSScalarType( + types.get(type.multirange_element_id) as introspect.PrimitiveType, + types, + opts + ); + return frag`${opts.edgedbDatatypePrefix}edgedb.MultiRange<${tn}>`; + } + default: util.assertNever(type); } diff --git a/packages/generate/src/syntax/casting.ts b/packages/generate/src/syntax/casting.ts index a6b9c6ad6..a29e167e3 100644 --- a/packages/generate/src/syntax/casting.ts +++ b/packages/generate/src/syntax/casting.ts @@ -15,6 +15,7 @@ import type { TupleType, TypeSet, RangeType, + MultiRangeType, } from "./typesystem"; import type { cardutil } from "./cardinality"; @@ -60,6 +61,12 @@ export type assignableBy = T extends ScalarType ? scalarAssignableBy : never > + : T extends MultiRangeType + ? MultiRangeType< + scalarAssignableBy extends ScalarType + ? scalarAssignableBy + : never + > : never; export type pointerToAssignmentExpression< diff --git a/packages/generate/src/syntax/funcops.ts b/packages/generate/src/syntax/funcops.ts index bcf5829cb..5d0c6a928 100644 --- a/packages/generate/src/syntax/funcops.ts +++ b/packages/generate/src/syntax/funcops.ts @@ -13,6 +13,7 @@ import type { TypeSet, RangeType, Expression, + MultiRangeType, } from "./typesystem"; import { cast } from "./cast"; import { isImplicitlyCastableTo, literalToTypeSet } from "./castMaps"; @@ -367,6 +368,10 @@ function compareType( return { match: true, anytype: arg }; } + if (type.name === "anyobject") { + return { match: arg.__kind__ === TypeKind.object, anytype: arg }; + } + if (type.name === "std::anypoint") { const descendants = getDescendantNames(typeSpec, typeId); if (descendants.includes(arg.__name__)) { @@ -405,6 +410,15 @@ function compareType( ); } } + if (type.kind === "multirange") { + if (arg.__kind__ === TypeKind.multirange) { + return compareType( + typeSpec, + type.multirange_element_id, + (arg as any as MultiRangeType).__element__ as BaseType + ); + } + } if (type.kind === "object") { if (arg.__kind__ !== TypeKind.object) return { match: false }; diff --git a/packages/generate/src/syntax/hydrate.ts b/packages/generate/src/syntax/hydrate.ts index 1caf236c7..1be07429d 100644 --- a/packages/generate/src/syntax/hydrate.ts +++ b/packages/generate/src/syntax/hydrate.ts @@ -217,8 +217,17 @@ export function makeType( return `range<${obj.__element__.__name__}>`; }); return obj; + } else if (type.kind === "multirange") { + obj.__kind__ = TypeKind.multirange; + util.defineGetter(obj, "__element__", () => { + return makeType(spec, type.multirange_element_id, literal, anytype); + }); + util.defineGetter(obj, "__name__", () => { + return `multirange<${obj.__element__.__name__}>`; + }); + return obj; } else { - throw new Error("Invalid type."); + throw new Error(`Invalid type: ${JSON.stringify(type, null, 2)}`); } } export type mergeObjectShapes< diff --git a/packages/generate/src/syntax/typesystem.ts b/packages/generate/src/syntax/typesystem.ts index ec482d916..05c31e407 100644 --- a/packages/generate/src/syntax/typesystem.ts +++ b/packages/generate/src/syntax/typesystem.ts @@ -9,7 +9,7 @@ import type { } from "edgedb/dist/reflection/index"; import { TypeKind } from "edgedb/dist/reflection/index"; import type { cardutil } from "./cardinality"; -import type { Range } from "edgedb"; +import type { Range, MultiRange } from "edgedb"; ////////////////// // BASETYPE @@ -244,7 +244,8 @@ export type SomeType = | TupleType | ObjectType | NamedTupleType - | RangeType; + | RangeType + | MultiRangeType; export interface PropertyDesc< Type extends BaseType = BaseType, @@ -405,7 +406,8 @@ export type PrimitiveType = | TupleType | NamedTupleType | ArrayType - | RangeType; + | RangeType + | MultiRangeType; export type PrimitiveTypeSet = TypeSet; @@ -672,6 +674,19 @@ export interface RangeType< __element__: Element; } +///////////////////////// +/// MULTIRANGE TYPE +///////////////////////// + +export interface MultiRangeType< + Element extends ScalarType = ScalarType, + Name extends string = `multirange<${Element["__name__"]}>` +> extends BaseType { + __name__: Name; + __kind__: TypeKind.multirange; + __element__: Element; +} + ///////////////////// /// TSTYPE COMPUTATION ///////////////////// @@ -694,6 +709,8 @@ export type BaseTypeToTsType< ? typeutil.flatten> : Type extends RangeType ? Range + : Type extends MultiRangeType + ? MultiRange : Type extends TupleType ? TupleItemsToTsType : Type extends NamedTupleType @@ -788,10 +805,13 @@ export type NonArrayType = | ObjectType | TupleType | NamedTupleType - | RangeType; + | RangeType + | MultiRangeType; export type AnyTupleType = TupleType | NamedTupleType; +export type AnyObjectType = ObjectType; + export type ParamType = | ScalarType | EnumType @@ -800,7 +820,9 @@ export type ParamType = | TupleType> | NamedTupleType<{ [k: string]: ParamType }> | RangeType + | MultiRangeType > | TupleType> | NamedTupleType<{ [k: string]: ParamType }> - | RangeType; + | RangeType + | MultiRangeType;