Add native literal substring query predicates
Implement checked containsInsensitive string predicates in GraphQL, protobuf, native SQL and bounded residual evaluation. ASCII folding is deterministic; SQL pattern characters remain literal. Retain deferred full-text index design. Studio migration awaits a decision on checked native getter forwarding across conformances; do not duplicate titles or bypass private state ownership. Compiler and local full-stack substring-query acceptance pass.
This commit is contained in:
@@ -165,6 +165,7 @@ enum QueryComparisonOperator {
|
|||||||
QUERY_COMPARISON_OPERATOR_LTE = 5;
|
QUERY_COMPARISON_OPERATOR_LTE = 5;
|
||||||
QUERY_COMPARISON_OPERATOR_GT = 6;
|
QUERY_COMPARISON_OPERATOR_GT = 6;
|
||||||
QUERY_COMPARISON_OPERATOR_GTE = 7;
|
QUERY_COMPARISON_OPERATOR_GTE = 7;
|
||||||
|
QUERY_COMPARISON_OPERATOR_CONTAINS_INSENSITIVE = 8;
|
||||||
}
|
}
|
||||||
message QueryComparison {
|
message QueryComparison {
|
||||||
QueryOperand operand = 1;
|
QueryOperand operand = 1;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -53,7 +53,7 @@ const pairs = (node: ValueNode): readonly import("graphql").ObjectFieldNode[] =>
|
|||||||
node.kind === "ObjectValue"
|
node.kind === "ObjectValue"
|
||||||
? node.fields
|
? node.fields
|
||||||
: fail("QUERY_UNSUPPORTED_FEATURE", "Query structure must be a literal object", node);
|
: fail("QUERY_UNSUPPORTED_FEATURE", "Query structure must be a literal object", node);
|
||||||
const ops = new Set(["eq", "in", "isNull", "lt", "lte", "gt", "gte"]);
|
const ops = new Set(["eq", "containsInsensitive", "in", "isNull", "lt", "lte", "gt", "gte"]);
|
||||||
const prefix = (a: readonly string[], b: readonly string[]) => a.every((part, i) => part === b[i]);
|
const prefix = (a: readonly string[], b: readonly string[]) => a.every((part, i) => part === b[i]);
|
||||||
|
|
||||||
export function relationalCompiler(
|
export function relationalCompiler(
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function relationalWire(
|
|||||||
value: argument(p.value),
|
value: argument(p.value),
|
||||||
operator: {
|
operator: {
|
||||||
eq: wire.QueryComparisonOperator.EQ,
|
eq: wire.QueryComparisonOperator.EQ,
|
||||||
|
containsInsensitive: wire.QueryComparisonOperator.CONTAINS_INSENSITIVE,
|
||||||
in: wire.QueryComparisonOperator.IN,
|
in: wire.QueryComparisonOperator.IN,
|
||||||
isNull: wire.QueryComparisonOperator.IS_NULL,
|
isNull: wire.QueryComparisonOperator.IS_NULL,
|
||||||
lt: wire.QueryComparisonOperator.LT,
|
lt: wire.QueryComparisonOperator.LT,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export type QueryPredicate =
|
|||||||
| {
|
| {
|
||||||
kind: "compare";
|
kind: "compare";
|
||||||
expression: QueryExpression;
|
expression: QueryExpression;
|
||||||
operator: "eq" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte";
|
operator: "eq" | "containsInsensitive" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte";
|
||||||
value: QueryArgument;
|
value: QueryArgument;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -93,7 +93,7 @@ export type QueryAggregatePredicate =
|
|||||||
| {
|
| {
|
||||||
kind: "compare";
|
kind: "compare";
|
||||||
expression: QueryExpression | QueryReduction;
|
expression: QueryExpression | QueryReduction;
|
||||||
operator: "eq" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte";
|
operator: "eq" | "containsInsensitive" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte";
|
||||||
value: QueryArgument;
|
value: QueryArgument;
|
||||||
};
|
};
|
||||||
export interface QueryKey {
|
export interface QueryKey {
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export function querySchema(
|
|||||||
let result = comparisons.get(key);
|
let result = comparisons.get(key);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
const fields: GraphQLInputFieldConfigMap = { eq: { type: value }, isNull: { type: GraphQLBoolean } };
|
const fields: GraphQLInputFieldConfigMap = { eq: { type: value }, isNull: { type: GraphQLBoolean } };
|
||||||
|
if (base.kind === "scalar" && base.name === "string") fields.containsInsensitive = { type: value };
|
||||||
if (base.kind === "scalar" && base.name !== "bytes")
|
if (base.kind === "scalar" && base.name !== "bytes")
|
||||||
fields.in = { type: new GraphQLList(new GraphQLNonNull(value)) };
|
fields.in = { type: new GraphQLList(new GraphQLNonNull(value)) };
|
||||||
if (base.kind === "scalar" && base.name !== "bool" && base.name !== "bytes")
|
if (base.kind === "scalar" && base.name !== "bool" && base.name !== "bytes")
|
||||||
|
|||||||
Vendored
+1
-1
@@ -76,7 +76,7 @@ export async function queryWorkspaceFixture(options: { live?: boolean } = {}) {
|
|||||||
const pkg: PackageRevision = pkgResult.resource.revision;
|
const pkg: PackageRevision = pkgResult.resource.revision;
|
||||||
const allInterfaces = [...interfaces.values(), ...(pkgResult.resource.specializations ?? [])];
|
const allInterfaces = [...interfaces.values(), ...(pkgResult.resource.specializations ?? [])];
|
||||||
const documents: Record<string, string> = {
|
const documents: Record<string, string> = {
|
||||||
"extent.graphql": `query Extent {root {objects(first: 30, orderBy: [{rank: ASC}]) {entries {node {_qx {ref} title rank}}}}}`,
|
"extent.graphql": `query Extent($text: String! = "") {root {objects(first: 30, where: {title: {containsInsensitive: $text}}, orderBy: [{rank: ASC}]) {entries {node {_qx {ref} title rank}}}}}`,
|
||||||
"upcoming.graphql": `query Upcoming($first: Int!, $after: Cursor) {root {items(first: $first, after: $after, where: {done: {eq: false}}, orderBy: [{rank: ASC}]) {entries {key cursor node {_qx {ref} ...TaskRow}} pageInfo {hasNextPage endCursor}}}}`,
|
"upcoming.graphql": `query Upcoming($first: Int!, $after: Cursor) {root {items(first: $first, after: $after, where: {done: {eq: false}}, orderBy: [{rank: ASC}]) {entries {key cursor node {_qx {ref} ...TaskRow}} pageInfo {hasNextPage endCursor}}}}`,
|
||||||
"row.graphql": `fragment TaskRow on TaskFacts {title rank done assignee {name}}`,
|
"row.graphql": `fragment TaskRow on TaskFacts {title rank done assignee {name}}`,
|
||||||
"enriched.graphql": `query Enriched {root {items(first: 3) {entries {key node {_qx {ref} title score}}}}}`,
|
"enriched.graphql": `query Enriched {root {items(first: 3) {entries {key node {_qx {ref} title score}}}}}`,
|
||||||
|
|||||||
@@ -276,6 +276,10 @@ test("fixed GraphQL yields exact effects, typed references and distinct query ar
|
|||||||
test("query allowlist and two-sided RPC effects fail during compilation", async () => {
|
test("query allowlist and two-sided RPC effects fail during compilation", async () => {
|
||||||
const rejects = (promise: Promise<unknown>, code: string) =>
|
const rejects = (promise: Promise<unknown>, code: string) =>
|
||||||
assert.rejects(promise, (error: unknown) => error instanceof QueryCompileError && error.code === code);
|
assert.rejects(promise, (error: unknown) => error instanceof QueryCompileError && error.code === code);
|
||||||
|
const substring = (field: string) =>
|
||||||
|
`query Upcoming {root {items(first: 3, where: {${field}: {containsInsensitive: "TASK"}}) {entries {node {...Row}}}}}`;
|
||||||
|
await compile(substring("title"));
|
||||||
|
await rejects(compile(substring("due")), "QUERY_VALIDATION");
|
||||||
await rejects(compile(document, "fragment Row on TaskFacts { hidden }"), "QUERY_VALIDATION");
|
await rejects(compile(document, "fragment Row on TaskFacts { hidden }"), "QUERY_VALIDATION");
|
||||||
await rejects(compile(document, "fragment Row on TaskFacts { score }"), "QUERY_RPC_CONSUMER_REASON");
|
await rejects(compile(document, "fragment Row on TaskFacts { score }"), "QUERY_RPC_CONSUMER_REASON");
|
||||||
await compile(
|
await compile(
|
||||||
|
|||||||
Reference in New Issue
Block a user