orbit-gql-migration
Converts and migrates legacy Orbit JSON DSL queries to Orbit GQL, then checks each converted query read-only against the graph. Legacy queries are objects with query_type, nodes, relationships, or aggregations. Use when a repo, runbook, script, doc, or agent skill contains them. Also use when a `glab orbit query --file` envelope or API body has an object `query`. Also use when a legacy JSON query fails with HTTP 400 compile_error. Do not use for writing new GQL queries (use the orbit skill).
- License 许可
- MIT
- 最近更新
- 2026-11-12
- Stars
- ★ 61
- Forks
- 16
01 前言 Front Matter
- name
- orbit-gql-migration
- license
- MIT
- version
- 0.1.0
- metadata
- { "audience": "developers", "keywords": "orbit, gql, migration, json, dsl, deprecated, glab", "workflow": "ai", "source-path": "skills/orbit-gql-migration", "source-project": "gitlab-org/orbit/knowledge-graph" }
- description
- Converts and migrates legacy Orbit JSON DSL queries to Orbit GQL, then checks each converted query read-only against the graph. Legacy queries are objects with query_type, nodes, relationships, or aggregations. Use when a repo, runbook, script, doc, or agent skill contains them. Also use when a `glab orbit query --file` envelope or API body has an object `query`. Also use when a legacy JSON query fails with HTTP 400 compile_error. Do not use for writing new GQL queries (use the orbit skill).
- compatibility
- Requires glab with the orbit commands and a GitLab instance with Orbit enabled.
03 SKILL.md 正文
Migrate legacy Orbit JSON queries to GQL
The Orbit JSON query language is deprecated and removed. Convert each legacy
query to GQL (glab orbit skills get orbit has the syntax), run it, and keep
the GQL. This skill is a temporary migration aid and will be removed. Do not
copy it into long-lived tooling. Public API background:
https://docs.gitlab.com/api/orbit/.
Step 0: confirm the instance accepts GQL
glab orbit query "CALL db.schema('Project')"
- A GQL syntax or compile error means the instance is too old for GQL. Stop and ask the administrator to upgrade GitLab and Orbit. Do not rewrite queries you cannot run.
- An auth error means the login is missing or expired. Fix it with
glab auth login, then rerun. An HTTP 403 can instead mean the user has no Orbit access, and logging in again does not fix that. Ask the administrator. - A network error or HTTP 429 is not a version problem. Wait and retry.
Traps
neighborsdefaults todirection: outgoing. Map the direction you find, and write->when it is omitted.-[...]-adds the incoming neighbors and returns plausible extra rows.- A node alias that is a GQL reserved word fails with "expected a variable" at
the alias. The check is case-insensitive, so
Endfails too. Rename it. Likely ones:end,desc,order,in,on,is,as,by,all,any,case,of,set,with,limit,match.startis fine. - The query lives in an envelope. Replace the object under
querywith a string of GQL and keep the other envelope fields. Never replace the whole file with bare GQL, because--filerejects it. A short query also runs without a file:glab orbit query '<gql>'takes the query text directly. - Some constructs have no GQL form. See "Rejected constructs".
Workflow
- Find legacy queries:
rg -l '"query_type"\s*:',--filepayloads, heredocs, API or MCP call bodies withnodesorrelationships, and skills or prompts that embed them. Skip output files you wrote yourself. - Convert one query at a time with the mapping.
- Validate it (below).
- Report queries with no equivalent. Leave them for manual rework instead of guessing.
Validate a conversion
Every step is read-only.
-
Run it. Use raw output so the shape is easy to inspect:
glab orbit query --response-format raw "<gql>" > /tmp/out.json jq -c '{query_type, row_count, pagination: .result.pagination}' /tmp/out.jsonExit 0 means it parsed and ran. HTTP 400
compile_errorusually names the line and column. Fix it and rerun. -
Compare with what the old query returned. If you saved old output, compare row count, property names, and the IDs of the first rows. If you did not, use an independent source for the same question, such as the REST API. Five newest merged MRs of a project:
glab orbit query --response-format raw "MATCH (mr:MergeRequest {project_id: <id>, state: 'merged'}) RETURN mr.iid ORDER BY mr.created_at DESC LIMIT 5" \ | jq -c '[.result.nodes[] | select(.type=="MergeRequest") | .iid]' glab api 'projects/<id>/merge_requests?state=merged&order_by=created_at&sort=desc&per_page=5' | jq -c '[.[].iid]'For counts, compare each group with the REST
X-Totalheader (glab api -i 'projects/<id>/merge_requests?state=opened&per_page=1'). Small differences can come from indexing lag; large ones mean the conversion is wrong. The REST API omitsX-Totalabove 10,000 rows. On very large projects the counts can also drift by a few percent. Use this check on small and medium projects. -
Check completeness.
pagination.has_moretrue means more rows exist thanLIMITreturned. That is expected for a capped list. Compare only the rows you asked for. -
Run a control. Change one filter value to a nonsense value and confirm the row count drops to 0. A query that returns the same rows either way is not filtering.
-
Do not trust an empty result. Probe with a row you know exists (
MATCH (n:Label {id: <known id>}) RETURN n LIMIT 1).
Mapping
| Legacy JSON construct | GQL |
|---|---|
{"query": {...}, ...} envelope |
{"query": "<GQL text>", ...}; keep other fields such as response_format |
node without columns |
RETURN n (default columns) |
options (dynamic_columns, include_debug_sql) |
drop it; it never changes semantics. Report dynamic_columns use |
id_property |
no known equivalent; report for manual rework |
query_type |
inferred from the pattern; drop it |
nodes[].id / entity |
(id:Entity); rename ids that are GQL reserved words, e.g. end |
node_ids: [1] |
{id: 1} |
node_ids: [1,2,3] |
WHERE n.id IN [1, 2, 3] |
id_range {start,end} |
WHERE n.id >= a AND n.id <= b |
filters: {p: v} / eq |
inline {p: v} or WHERE n.p = v |
ne: v |
WHERE n.p <> v or NOT n.p = v |
node-level not: {p: {in: [...]}} |
WHERE NOT n.p IN [...] (not postfix NOT IN) |
gt/gte/lt/lte |
> >= < <= |
in |
IN [...] |
contains/starts_with/ends_with |
CONTAINS / STARTS WITH / ENDS WITH |
is_null: true / false |
IS NULL / IS NOT NULL |
token_match/all_tokens/any_tokens |
same-named functions in WHERE |
relationships[{type,from,to}] |
(from)-[:TYPE]->(to) |
type: [A,B] |
-[:A|B]-> |
hops: [1,2] |
-[:TYPE*1..2]->; max 3 in both |
direction: incoming |
<-[:T]- |
direction: both |
-[:T]- in a neighbors query only; rejected in a traversal, see below |
relationship filters (single hop) |
[r:T {p: v}] or WHERE r.p = v |
type: "*" |
--> |
columns: [a,b] |
RETURN n.a, n.b |
columns: "*" |
RETURN properties(n) |
order_by: "-n.p" |
ORDER BY n.p DESC (asc: no suffix) |
limit |
LIMIT n; default is 30 in both, write it anyway |
aggregations [{count: n, as: x}] |
RETURN ..., count(n) AS x; sum/min/max/avg likewise |
group_by: ["n.p"] |
the non-aggregate RETURN items |
group_by: ["u"] (node) |
RETURN u{.id, .username} |
group_by [{key, truncate, as}] |
date_trunc('month', n.p) AS as |
aggregation_sort: "-x" |
ORDER BY x DESC |
path_finding {from, to, max_depth, rel_types} |
MATCH path = ANY SHORTEST (a:L {id: 1})-[:T|U*1..max_depth]->(b:L {id: 2}) RETURN path; GQL requires named types, so rel_types is mandatory |
neighbors {direction, rel_types} |
outgoing (default) (c:L {id: 1})-[:A|B]->(n), incoming <-[:A|B]-, both -[:A|B]-, then RETURN c, n; omitted rel_types is (c)-->(n) |
Worked examples
Aggregation. Legacy:
{"query": {"query_type": "aggregation",
"nodes": [{"id": "mr", "entity": "MergeRequest"},
{"id": "p", "entity": "Project", "node_ids": [278964]}],
"relationships": [{"type": "IN_PROJECT", "from": "mr", "to": "p"}],
"group_by": ["mr.state"],
"aggregations": [{"count": "mr", "as": "mrs"}],
"aggregation_sort": "-mrs", "limit": 10}}
GQL:
MATCH (mr:MergeRequest)-[:IN_PROJECT]->(p:Project {id: 278964})
RETURN mr.state, count(mr) AS mrs
ORDER BY mrs DESC
LIMIT 10
Neighbors with no direction, so outgoing. Legacy:
{"query": {"query_type": "neighbors",
"nodes": [{"id": "mr", "entity": "MergeRequest", "node_ids": [482908721]}],
"neighbors": {"rel_types": ["AUTHORED", "IN_PROJECT"]}, "limit": 25}}
GQL:
MATCH (mr:MergeRequest {id: 482908721})-[:AUTHORED|IN_PROJECT]->(n)
RETURN mr, n
LIMIT 25
Rejected constructs and workarounds
| Construct | What to do |
|---|---|
OR between conditions |
IN [...] for one property; otherwise run two queries |
OPTIONAL MATCH, WITH, UNION, UNWIND |
split into separate queries |
DISTINCT, count(*) |
group by the key; count(node) |
a second ORDER BY key |
sort the rows client side |
| no node anchored by an ID or literal filter | anchor one node; LIMIT does not count |
direction: both on a traversal relationship |
no equivalent. Run one query per direction, or a neighbors-style query |
relationship filters on a variable-length hop |
no equivalent (GQL rejects it). Any rewrite, such as a chain of filtered single-hop edges, changes the plan and result shape, so check its results by hand |
postfix n.p NOT IN [...] |
NOT n.p IN [...] |
Pitfalls
- Negation exists in both languages:
neand the node-levelnotblock map to<>andNOT. Keep semantics identical while migrating; improve after. - Compare columns loosely: the legacy order followed node declaration order.
- Relationship directions follow the schema; the error names the valid one.
CALL db.schema('Node')lists them. - Fan-out on high-cardinality joins: prefer an aggregate.
- A node alias cannot be reused for a different label.
- Both languages cap at 16 predicates and 3 hops.
04 同仓库更多 Skills
orbit
Use Orbit (`glab orbit` or `orbit`) to search code and read it with callers and tests. It reads MRs, issues, and failed jobs with the code they cite. Graph queries answer relationship and aggregation questions across GitLab. Skip it for writes that `glab` handles.
orbit-wrapper
Use Orbit (`glab orbit` or `orbit`) to search code and read it with callers and tests. It reads MRs, issues, and failed jobs with the code they cite. Graph queries answer relationship and aggregation questions across GitLab. Skip it for writes that `glab` handles.
orbit-start
Walk a user through their first GitLab Orbit session. Check the setup, then answer a simple first question and a workflow from the prompt examples with Orbit. Offer to run a prompt again without Orbit, and to show how Orbit works. Use when the user asks to get started with Orbit, try Orbit, or see what Orbit can do.
orbit-package-registry
Use Orbit graph queries to answer questions about GitLab Package Registry packages and Container Registry repositories across projects and groups. Use it to find which projects host a package. It covers package versions and their files, declared dependencies, known vulnerabilities, and the pipelines that built them. Load it with the `orbit` skill. Skip it for one project's package list or for publishing, which `glab` and the package manager handle.
orbit-cli
Index and query a local checkout offline with the GitLab Orbit CLI (`orbit`, or `glab orbit`). Reach for it when a question names a symbol or spans code structure. Examples: who calls or extends this, where it is defined, what a file defines, how definitions are distributed. One call replaces many file reads and text greps, including matches in config, templates, and docs. Works on the working tree and unpushed branches, and also searches and reads the project's GitLab. Not a fit: reading one known file, or counts and relationships across GitLab (use the `orbit` skill).