order by support

This commit is contained in:
booleanmaybe 2026-04-03 12:06:07 -04:00
parent 2efaeb25f5
commit 47c385513e
17 changed files with 1953 additions and 9 deletions

View file

@ -28,6 +28,14 @@ Select with a basic filter:
select where status = "done" and priority <= 2
```
Select with ordering:
```sql
select order by priority
select where status = "done" order by updatedAt desc
select where "bug" in tags order by priority asc, createdAt desc
```
Create a tiki:
```sql
@ -220,3 +228,16 @@ Non-string `run(...)` command:
```sql
after update run(1 + 2)
```
Ordering by a non-orderable field:
```sql
select order by tags
select order by dependsOn
```
Order by inside a subquery:
```sql
select where count(select where status = "done" order by priority) >= 1
```

View file

@ -0,0 +1,202 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 780 580">
<!--
Grid system:
- Column width: 84px, row height: 28px
- Row header: 90px wide
- Header bar: 26px tall
- All fills use opacity for dark/light background compatibility
- Valid cells: green pill with result type
- Invalid cells: dim dash
-->
<defs>
<style>
.title { font: 700 16px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #A5B4FC; }
.op-title { font: 700 14px -apple-system, 'Segoe UI', Roboto, sans-serif; }
.hdr { font: 600 11px 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; fill: #E2E8F0; }
.rhdr { font: 600 11px 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; fill: #94A3B8; }
.cell-ok { font: 600 10px 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; fill: #34D399; }
.cell-na { font: 400 11px -apple-system, sans-serif; fill: #475569; }
.footnote { font: italic 11px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #64748B; }
.legend-text { font: 400 11px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #94A3B8; }
</style>
<filter id="shadow" x="-4%" y="-4%" width="108%" height="116%">
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
</filter>
</defs>
<text class="title" x="390" y="24" text-anchor="middle">Binary operator type resolution</text>
<!-- ==================== + OPERATOR ==================== -->
<g transform="translate(20, 44)">
<!-- Operator label -->
<rect x="0" y="0" width="26" height="26" rx="7" fill="#818CF8" fill-opacity="0.25" stroke="#818CF8" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<text x="13" y="17" text-anchor="middle" font-size="16" font-weight="700" fill="#A5B4FC">+</text>
<text class="op-title" x="36" y="17" fill="#A5B4FC">addition / concatenation</text>
<!-- Column header bar -->
<rect x="90" y="34" width="646" height="26" rx="6" fill="#334155" fill-opacity="0.6"/>
<text class="hdr" x="178" y="51" text-anchor="middle">string</text>
<text class="hdr" x="264" y="51" text-anchor="middle">int</text>
<text class="hdr" x="350" y="51" text-anchor="middle">date</text>
<text class="hdr" x="436" y="51" text-anchor="middle">tstamp</text>
<text class="hdr" x="522" y="51" text-anchor="middle">duration</text>
<text class="hdr" x="608" y="51" text-anchor="middle">list&lt;str&gt;</text>
<text class="hdr" x="694" y="51" text-anchor="middle">list&lt;ref&gt;</text>
<!-- Row 1: string -->
<text class="rhdr" x="50" y="79" text-anchor="middle">string</text>
<rect x="140" y="66" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="178" y="81" text-anchor="middle">string</text>
<text class="cell-na" x="264" y="81" text-anchor="middle"></text>
<text class="cell-na" x="350" y="81" text-anchor="middle"></text>
<text class="cell-na" x="436" y="81" text-anchor="middle"></text>
<text class="cell-na" x="522" y="81" text-anchor="middle"></text>
<text class="cell-na" x="608" y="81" text-anchor="middle"></text>
<text class="cell-na" x="694" y="81" text-anchor="middle"></text>
<!-- Row 2: int (alternating row background) -->
<rect x="90" y="92" width="646" height="28" rx="4" fill="#94A3B8" fill-opacity="0.04"/>
<text class="rhdr" x="50" y="107" text-anchor="middle">int</text>
<text class="cell-na" x="178" y="107" text-anchor="middle"></text>
<rect x="226" y="94" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="264" y="109" text-anchor="middle">int</text>
<text class="cell-na" x="350" y="107" text-anchor="middle"></text>
<text class="cell-na" x="436" y="107" text-anchor="middle"></text>
<text class="cell-na" x="522" y="107" text-anchor="middle"></text>
<text class="cell-na" x="608" y="107" text-anchor="middle"></text>
<text class="cell-na" x="694" y="107" text-anchor="middle"></text>
<!-- Row 3: date -->
<text class="rhdr" x="50" y="135" text-anchor="middle">date</text>
<text class="cell-na" x="178" y="135" text-anchor="middle"></text>
<text class="cell-na" x="264" y="135" text-anchor="middle"></text>
<text class="cell-na" x="350" y="135" text-anchor="middle"></text>
<text class="cell-na" x="436" y="135" text-anchor="middle"></text>
<rect x="484" y="122" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="522" y="137" text-anchor="middle">date</text>
<text class="cell-na" x="608" y="135" text-anchor="middle"></text>
<text class="cell-na" x="694" y="135" text-anchor="middle"></text>
<!-- Row 4: timestamp (alternating row background) -->
<rect x="90" y="148" width="646" height="28" rx="4" fill="#94A3B8" fill-opacity="0.04"/>
<text class="rhdr" x="50" y="163" text-anchor="middle">tstamp</text>
<text class="cell-na" x="178" y="163" text-anchor="middle"></text>
<text class="cell-na" x="264" y="163" text-anchor="middle"></text>
<text class="cell-na" x="350" y="163" text-anchor="middle"></text>
<text class="cell-na" x="436" y="163" text-anchor="middle"></text>
<rect x="484" y="150" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="522" y="165" text-anchor="middle">tstamp</text>
<text class="cell-na" x="608" y="163" text-anchor="middle"></text>
<text class="cell-na" x="694" y="163" text-anchor="middle"></text>
<!-- Row 5: list<string> -->
<text class="rhdr" x="50" y="191" text-anchor="middle">list&lt;str&gt;</text>
<rect x="140" y="178" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="178" y="193" text-anchor="middle">list&lt;str&gt;</text>
<text class="cell-na" x="264" y="191" text-anchor="middle"></text>
<text class="cell-na" x="350" y="191" text-anchor="middle"></text>
<text class="cell-na" x="436" y="191" text-anchor="middle"></text>
<text class="cell-na" x="522" y="191" text-anchor="middle"></text>
<rect x="570" y="178" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="608" y="193" text-anchor="middle">list&lt;str&gt;</text>
<text class="cell-na" x="694" y="191" text-anchor="middle"></text>
<!-- Row 6: list<ref> (alternating row background) -->
<rect x="90" y="204" width="646" height="28" rx="4" fill="#94A3B8" fill-opacity="0.04"/>
<text class="rhdr" x="50" y="219" text-anchor="middle">list&lt;ref&gt;</text>
<text class="cell-na" x="178" y="219" text-anchor="middle"></text>
<text class="cell-na" x="264" y="219" text-anchor="middle"></text>
<text class="cell-na" x="350" y="219" text-anchor="middle"></text>
<text class="cell-na" x="436" y="219" text-anchor="middle"></text>
<text class="cell-na" x="522" y="219" text-anchor="middle"></text>
<text class="cell-na" x="608" y="219" text-anchor="middle"></text>
<rect x="656" y="206" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="694" y="221" text-anchor="middle">list&lt;ref&gt;</text>
</g>
<!-- ==================== - OPERATOR ==================== -->
<g transform="translate(20, 300)">
<!-- Operator label -->
<rect x="0" y="0" width="26" height="26" rx="7" fill="#F87171" fill-opacity="0.25" stroke="#F87171" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<text x="13" y="17" text-anchor="middle" font-size="16" font-weight="700" fill="#F87171"></text>
<text class="op-title" x="36" y="17" fill="#F87171">subtraction / removal</text>
<!-- Column header bar -->
<rect x="90" y="34" width="646" height="26" rx="6" fill="#334155" fill-opacity="0.6"/>
<text class="hdr" x="178" y="51" text-anchor="middle">string</text>
<text class="hdr" x="264" y="51" text-anchor="middle">int</text>
<text class="hdr" x="350" y="51" text-anchor="middle">date</text>
<text class="hdr" x="436" y="51" text-anchor="middle">tstamp</text>
<text class="hdr" x="522" y="51" text-anchor="middle">duration</text>
<text class="hdr" x="608" y="51" text-anchor="middle">list&lt;str&gt;</text>
<text class="hdr" x="694" y="51" text-anchor="middle">list&lt;ref&gt;</text>
<!-- Row 1: int -->
<text class="rhdr" x="50" y="79" text-anchor="middle">int</text>
<text class="cell-na" x="178" y="79" text-anchor="middle"></text>
<rect x="226" y="66" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="264" y="81" text-anchor="middle">int</text>
<text class="cell-na" x="350" y="79" text-anchor="middle"></text>
<text class="cell-na" x="436" y="79" text-anchor="middle"></text>
<text class="cell-na" x="522" y="79" text-anchor="middle"></text>
<text class="cell-na" x="608" y="79" text-anchor="middle"></text>
<text class="cell-na" x="694" y="79" text-anchor="middle"></text>
<!-- Row 2: date (alternating row background) -->
<rect x="90" y="92" width="646" height="28" rx="4" fill="#94A3B8" fill-opacity="0.04"/>
<text class="rhdr" x="50" y="107" text-anchor="middle">date</text>
<text class="cell-na" x="178" y="107" text-anchor="middle"></text>
<text class="cell-na" x="264" y="107" text-anchor="middle"></text>
<rect x="312" y="94" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="350" y="109" text-anchor="middle">duration</text>
<text class="cell-na" x="436" y="107" text-anchor="middle"></text>
<rect x="484" y="94" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="522" y="109" text-anchor="middle">date</text>
<text class="cell-na" x="608" y="107" text-anchor="middle"></text>
<text class="cell-na" x="694" y="107" text-anchor="middle"></text>
<!-- Row 3: timestamp -->
<text class="rhdr" x="50" y="135" text-anchor="middle">tstamp</text>
<text class="cell-na" x="178" y="135" text-anchor="middle"></text>
<text class="cell-na" x="264" y="135" text-anchor="middle"></text>
<text class="cell-na" x="350" y="135" text-anchor="middle"></text>
<rect x="398" y="122" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="436" y="137" text-anchor="middle">duration</text>
<rect x="484" y="122" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="522" y="137" text-anchor="middle">tstamp</text>
<text class="cell-na" x="608" y="135" text-anchor="middle"></text>
<text class="cell-na" x="694" y="135" text-anchor="middle"></text>
<!-- Row 4: list<string> (alternating row background) -->
<rect x="90" y="148" width="646" height="28" rx="4" fill="#94A3B8" fill-opacity="0.04"/>
<text class="rhdr" x="50" y="163" text-anchor="middle">list&lt;str&gt;</text>
<rect x="140" y="150" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="178" y="165" text-anchor="middle">list&lt;str&gt;</text>
<text class="cell-na" x="264" y="163" text-anchor="middle"></text>
<text class="cell-na" x="350" y="163" text-anchor="middle"></text>
<text class="cell-na" x="436" y="163" text-anchor="middle"></text>
<text class="cell-na" x="522" y="163" text-anchor="middle"></text>
<rect x="570" y="150" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="608" y="165" text-anchor="middle">list&lt;str&gt;</text>
<text class="cell-na" x="694" y="163" text-anchor="middle"></text>
<!-- Row 5: list<ref> -->
<text class="rhdr" x="50" y="191" text-anchor="middle">list&lt;ref&gt;</text>
<text class="cell-na" x="178" y="191" text-anchor="middle"></text>
<text class="cell-na" x="264" y="191" text-anchor="middle"></text>
<text class="cell-na" x="350" y="191" text-anchor="middle"></text>
<text class="cell-na" x="436" y="191" text-anchor="middle"></text>
<text class="cell-na" x="522" y="191" text-anchor="middle"></text>
<text class="cell-na" x="608" y="191" text-anchor="middle"></text>
<rect x="656" y="178" width="76" height="22" rx="11" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="cell-ok" x="694" y="193" text-anchor="middle">list&lt;ref&gt;</text>
</g>
<!-- Legend + footnotes -->
<rect x="270" y="520" width="14" height="14" rx="7" fill="#10B981" fill-opacity="0.15" stroke="#10B981" stroke-width="1" stroke-opacity="0.3"/>
<text class="legend-text" x="290" y="531">valid (shows result type)</text>
<text class="legend-text" x="460" y="531">— invalid</text>
<text class="footnote" x="390" y="554" text-anchor="middle">string includes string-like types: string, status, type, id, ref</text>
<text class="footnote" x="390" y="570" text-anchor="middle">list&lt;ref&gt; + also accepts bare id/ref values on the right side</text>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,339 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 888">
<!--
Condition grammar railroad diagram — transparent/dark-bg compatible
Tracks: condition, orCond, andCond, notCond, primaryCond, exprCond
-->
<defs>
<marker id="arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="10" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 3.5 L 0 7 z" fill="#94A3B8"/>
</marker>
<filter id="shadow" x="-4%" y="-4%" width="108%" height="116%">
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
</filter>
<style>
.title { font: 700 16px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #A5B4FC; }
.label { font: bold 14px 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; fill: #94A3B8; }
.terminal rect { fill: #3B82F6; fill-opacity: 0.15; stroke: #3B82F6; stroke-width: 1.5; stroke-opacity: 0.5; filter: url(#shadow); }
.terminal text { font: 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #93C5FD; font-weight: 600; }
.nonterminal rect { fill: #F59E0B; fill-opacity: 0.15; stroke: #F59E0B; stroke-width: 1.5; stroke-opacity: 0.5; filter: url(#shadow); }
.nonterminal text { font: 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #FCD34D; font-weight: 500; }
.track { stroke: #94A3B8; stroke-width: 1.5; fill: none; }
</style>
</defs>
<!-- title -->
<text class="title" x="450" y="22" text-anchor="middle">Condition grammar</text>
<!-- all track content shifted down 8px for title clearance -->
<g transform="translate(0, 8)">
<!-- ==================== condition ==================== -->
<g transform="translate(0, 20)">
<g>
<rect x="2" y="22" width="92" height="28" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="36" text-anchor="start">condition</text>
</g>
<circle cx="130" cy="32" r="4" fill="#94A3B8"/>
<circle cx="130" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="134" y1="32" x2="160" y2="32"/>
<g class="nonterminal" transform="translate(160, 16)">
<rect width="82" height="32" rx="4"/>
<text x="41" y="16" text-anchor="middle" dominant-baseline="central">orCond</text>
</g>
<line class="track" x1="242" y1="32" x2="280" y2="32"/>
<circle cx="280" cy="32" r="4" fill="#94A3B8"/>
<circle cx="280" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
<!-- separator -->
<line x1="10" y1="80" x2="890" y2="80" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== orCond ==================== -->
<!-- andCond with loopback via "or" below -->
<g transform="translate(0, 100)">
<g>
<rect x="2" y="22" width="66" height="28" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="36" text-anchor="start">orCond</text>
</g>
<circle cx="130" cy="32" r="4" fill="#94A3B8"/>
<circle cx="130" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="134" y1="32" x2="170" y2="32"/>
<g class="nonterminal" transform="translate(170, 16)">
<rect width="96" height="32" rx="4"/>
<text x="48" y="16" text-anchor="middle" dominant-baseline="central">andCond</text>
</g>
<line class="track" x1="266" y1="32" x2="320" y2="32"/>
<!-- loopback below with "or" keyword -->
<path class="track" d="M 310,32 C 310,62 300,76 280,76 L 240,76"/>
<g class="terminal" transform="translate(200, 60)">
<rect width="40" height="32" rx="16"/>
<text x="20" y="16" text-anchor="middle" dominant-baseline="central">or</text>
</g>
<path class="track" d="M 200,76 C 180,76 170,62 170,42"/>
<path d="M 165,46 L 170,36 L 175,46 z" fill="#94A3B8"/>
<circle cx="320" cy="32" r="4" fill="#94A3B8"/>
<circle cx="320" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
<!-- separator -->
<line x1="10" y1="195" x2="890" y2="195" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== andCond ==================== -->
<!-- notCond with loopback via "and" below -->
<g transform="translate(0, 210)">
<g>
<rect x="2" y="22" width="75" height="28" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="36" text-anchor="start">andCond</text>
</g>
<circle cx="130" cy="32" r="4" fill="#94A3B8"/>
<circle cx="130" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="134" y1="32" x2="170" y2="32"/>
<g class="nonterminal" transform="translate(170, 16)">
<rect width="96" height="32" rx="4"/>
<text x="48" y="16" text-anchor="middle" dominant-baseline="central">notCond</text>
</g>
<line class="track" x1="266" y1="32" x2="320" y2="32"/>
<!-- loopback below with "and" keyword -->
<path class="track" d="M 310,32 C 310,62 300,76 280,76 L 240,76"/>
<g class="terminal" transform="translate(195, 60)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">and</text>
</g>
<path class="track" d="M 195,76 C 175,76 170,62 170,42"/>
<path d="M 165,46 L 170,36 L 175,46 z" fill="#94A3B8"/>
<circle cx="320" cy="32" r="4" fill="#94A3B8"/>
<circle cx="320" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
<!-- separator -->
<line x1="10" y1="305" x2="890" y2="305" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== notCond ==================== -->
<!-- Two alternatives: "not" notCond (recursive) | primaryCond -->
<g transform="translate(0, 320)">
<g>
<rect x="2" y="40" width="75" height="28" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="54" text-anchor="start">notCond</text>
</g>
<circle cx="130" cy="50" r="4" fill="#94A3B8"/>
<circle cx="130" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="134" y1="50" x2="170" y2="50"/>
<!-- upper: not + notCond -->
<path class="track" d="M 170,50 C 170,20 180,10 200,10"/>
<g class="terminal" transform="translate(200, -6)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">not</text>
</g>
<line class="track" x1="250" y1="10" x2="274" y2="10"/>
<g class="nonterminal" transform="translate(274, -6)">
<rect width="96" height="32" rx="4"/>
<text x="48" y="16" text-anchor="middle" dominant-baseline="central">notCond</text>
</g>
<path class="track" d="M 370,10 C 390,10 400,20 400,50"/>
<!-- lower: primaryCond (straight through) -->
<line class="track" x1="170" y1="50" x2="200" y2="50"/>
<g class="nonterminal" transform="translate(200, 34)">
<rect width="130" height="32" rx="4"/>
<text x="65" y="16" text-anchor="middle" dominant-baseline="central">primaryCond</text>
</g>
<line class="track" x1="330" y1="50" x2="400" y2="50"/>
<line class="track" x1="400" y1="50" x2="440" y2="50"/>
<circle cx="440" cy="50" r="4" fill="#94A3B8"/>
<circle cx="440" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
<!-- separator -->
<line x1="10" y1="420" x2="890" y2="420" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== primaryCond ==================== -->
<!-- Two alternatives: ( condition ) | exprCond -->
<g transform="translate(0, 440)">
<g>
<rect x="2" y="40" width="108" height="28" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="54" text-anchor="start">primaryCond</text>
</g>
<circle cx="140" cy="50" r="4" fill="#94A3B8"/>
<circle cx="140" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="144" y1="50" x2="180" y2="50"/>
<!-- upper: ( condition ) -->
<path class="track" d="M 180,50 C 180,20 190,10 210,10"/>
<g class="terminal" transform="translate(210, -6)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">(</text>
</g>
<line class="track" x1="240" y1="10" x2="258" y2="10"/>
<g class="nonterminal" transform="translate(258, -6)">
<rect width="100" height="32" rx="4"/>
<text x="50" y="16" text-anchor="middle" dominant-baseline="central">condition</text>
</g>
<line class="track" x1="358" y1="10" x2="376" y2="10"/>
<g class="terminal" transform="translate(376, -6)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">)</text>
</g>
<path class="track" d="M 406,10 C 426,10 436,20 436,50"/>
<!-- lower: exprCond (straight through) -->
<line class="track" x1="180" y1="50" x2="210" y2="50"/>
<g class="nonterminal" transform="translate(210, 34)">
<rect width="100" height="32" rx="4"/>
<text x="50" y="16" text-anchor="middle" dominant-baseline="central">exprCond</text>
</g>
<line class="track" x1="310" y1="50" x2="436" y2="50"/>
<line class="track" x1="436" y1="50" x2="476" y2="50"/>
<circle cx="476" cy="50" r="4" fill="#94A3B8"/>
<circle cx="476" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
<!-- separator -->
<line x1="10" y1="537" x2="890" y2="537" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== exprCond ==================== -->
<!-- expr followed by optional tail: 7 alternatives or bypass -->
<g transform="translate(0, 555)">
<g>
<rect x="2" y="40" width="83" height="28" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="54" text-anchor="start">exprCond</text>
</g>
<circle cx="130" cy="50" r="4" fill="#94A3B8"/>
<circle cx="130" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="134" y1="50" x2="160" y2="50"/>
<g class="nonterminal" transform="translate(160, 34)">
<rect width="60" height="32" rx="4"/>
<text x="30" y="16" text-anchor="middle" dominant-baseline="central">expr</text>
</g>
<line class="track" x1="220" y1="50" x2="260" y2="50"/>
<!-- branch: bypass (no tail) above, or one of 7 tails -->
<!-- bypass: straight across above -->
<path class="track" d="M 250,50 C 250,15 260,5 280,5 L 730,5 C 750,5 760,15 760,50"/>
<!-- compareTail: compareOp + expr -->
<path class="track" d="M 260,50 C 260,68 270,78 290,78"/>
<g class="nonterminal" transform="translate(290, 62)">
<rect width="110" height="32" rx="4"/>
<text x="55" y="16" text-anchor="middle" dominant-baseline="central">compareOp</text>
</g>
<line class="track" x1="400" y1="78" x2="420" y2="78"/>
<g class="nonterminal" transform="translate(420, 62)">
<rect width="60" height="32" rx="4"/>
<text x="30" y="16" text-anchor="middle" dominant-baseline="central">expr</text>
</g>
<path class="track" d="M 480,78 C 740,78 740,60 760,50"/>
<!-- isEmptyTail: is empty -->
<path class="track" d="M 260,50 C 260,108 270,118 290,118"/>
<g class="terminal" transform="translate(290, 102)">
<rect width="36" height="32" rx="16"/>
<text x="18" y="16" text-anchor="middle" dominant-baseline="central">is</text>
</g>
<line class="track" x1="326" y1="118" x2="346" y2="118"/>
<g class="terminal" transform="translate(346, 102)">
<rect width="70" height="32" rx="16"/>
<text x="35" y="16" text-anchor="middle" dominant-baseline="central">empty</text>
</g>
<path class="track" d="M 416,118 C 740,118 740,60 760,50"/>
<!-- isNotEmptyTail: is not empty -->
<path class="track" d="M 260,50 C 260,148 270,158 290,158"/>
<g class="terminal" transform="translate(290, 142)">
<rect width="36" height="32" rx="16"/>
<text x="18" y="16" text-anchor="middle" dominant-baseline="central">is</text>
</g>
<line class="track" x1="326" y1="158" x2="346" y2="158"/>
<g class="terminal" transform="translate(346, 142)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">not</text>
</g>
<line class="track" x1="396" y1="158" x2="416" y2="158"/>
<g class="terminal" transform="translate(416, 142)">
<rect width="70" height="32" rx="16"/>
<text x="35" y="16" text-anchor="middle" dominant-baseline="central">empty</text>
</g>
<path class="track" d="M 486,158 C 740,158 740,60 760,50"/>
<!-- inTail: in expr -->
<path class="track" d="M 260,50 C 260,188 270,198 290,198"/>
<g class="terminal" transform="translate(290, 182)">
<rect width="36" height="32" rx="16"/>
<text x="18" y="16" text-anchor="middle" dominant-baseline="central">in</text>
</g>
<line class="track" x1="326" y1="198" x2="346" y2="198"/>
<g class="nonterminal" transform="translate(346, 182)">
<rect width="60" height="32" rx="4"/>
<text x="30" y="16" text-anchor="middle" dominant-baseline="central">expr</text>
</g>
<path class="track" d="M 406,198 C 740,198 740,60 760,50"/>
<!-- notInTail: not in expr -->
<path class="track" d="M 260,50 C 260,228 270,238 290,238"/>
<g class="terminal" transform="translate(290, 222)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">not</text>
</g>
<line class="track" x1="340" y1="238" x2="360" y2="238"/>
<g class="terminal" transform="translate(360, 222)">
<rect width="36" height="32" rx="16"/>
<text x="18" y="16" text-anchor="middle" dominant-baseline="central">in</text>
</g>
<line class="track" x1="396" y1="238" x2="416" y2="238"/>
<g class="nonterminal" transform="translate(416, 222)">
<rect width="60" height="32" rx="4"/>
<text x="30" y="16" text-anchor="middle" dominant-baseline="central">expr</text>
</g>
<path class="track" d="M 476,238 C 740,238 740,60 760,50"/>
<!-- anyTail: any primaryCond -->
<path class="track" d="M 260,50 C 260,268 270,278 290,278"/>
<g class="terminal" transform="translate(290, 262)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">any</text>
</g>
<line class="track" x1="340" y1="278" x2="360" y2="278"/>
<g class="nonterminal" transform="translate(360, 262)">
<rect width="130" height="32" rx="4"/>
<text x="65" y="16" text-anchor="middle" dominant-baseline="central">primaryCond</text>
</g>
<path class="track" d="M 490,278 C 740,278 740,60 760,50"/>
<!-- allTail: all primaryCond -->
<path class="track" d="M 260,50 C 260,308 270,318 290,318"/>
<g class="terminal" transform="translate(290, 302)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">all</text>
</g>
<line class="track" x1="340" y1="318" x2="360" y2="318"/>
<g class="nonterminal" transform="translate(360, 302)">
<rect width="130" height="32" rx="4"/>
<text x="65" y="16" text-anchor="middle" dominant-baseline="central">primaryCond</text>
</g>
<path class="track" d="M 490,318 C 740,318 740,60 760,50"/>
<!-- exit -->
<line class="track" x1="760" y1="50" x2="800" y2="50"/>
<circle cx="800" cy="50" r="4" fill="#94A3B8"/>
<circle cx="800" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,303 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 712">
<!--
Expression grammar railroad diagram — transparent/dark-bg compatible
Tracks: expr, unaryExpr, funcCall, qualifiedRef
-->
<defs>
<marker id="arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="10" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 3.5 L 0 7 z" fill="#94A3B8"/>
</marker>
<filter id="shadow" x="-4%" y="-4%" width="108%" height="116%">
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
</filter>
<style>
.title { font: 700 16px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #A5B4FC; }
.label { font: bold 14px 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; fill: #94A3B8; }
.terminal rect { fill: #3B82F6; fill-opacity: 0.15; stroke: #3B82F6; stroke-width: 1.5; stroke-opacity: 0.5; filter: url(#shadow); }
.terminal text { font: 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #93C5FD; font-weight: 600; }
.nonterminal rect { fill: #F59E0B; fill-opacity: 0.15; stroke: #F59E0B; stroke-width: 1.5; stroke-opacity: 0.5; filter: url(#shadow); }
.nonterminal text { font: 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #FCD34D; font-weight: 500; }
.lit-terminal rect { fill: #6366F1; fill-opacity: 0.15; stroke: #6366F1; stroke-width: 1.5; stroke-opacity: 0.5; filter: url(#shadow); }
.lit-terminal text { font: 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #A5B4FC; font-weight: 500; }
.track { stroke: #94A3B8; stroke-width: 1.5; fill: none; }
.track-dot { fill: #94A3B8; }
.label-pill { fill: #94A3B8; fill-opacity: 0.08; stroke: #94A3B8; stroke-width: 0.5; stroke-opacity: 0.2; }
</style>
</defs>
<!-- Title -->
<text class="title" x="450" y="22" text-anchor="middle">Expression grammar</text>
<!-- All content shifted down 8px for title -->
<g transform="translate(0, 8)">
<!-- ==================== expr ==================== -->
<!-- unaryExpr with loopback via +/- below -->
<g transform="translate(0, 20)">
<!-- label background pill -->
<rect class="label-pill" x="2" y="20" width="50" height="24" rx="6"/>
<text class="label" x="10" y="36" text-anchor="start">expr</text>
<!-- entry dot with outer ring -->
<circle fill="#94A3B8" cx="100" cy="32" r="4"/>
<circle fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3" cx="100" cy="32" r="7"/>
<line class="track" x1="104" y1="32" x2="140" y2="32"/>
<g class="nonterminal" transform="translate(140, 16)">
<rect width="110" height="32" rx="4"/>
<text x="55" y="16" text-anchor="middle" dominant-baseline="central">unaryExpr</text>
</g>
<line class="track" x1="250" y1="32" x2="310" y2="32"/>
<!-- loopback below with +/- alternatives -->
<path class="track" d="M 300,32 C 300,58 292,72 276,72"/>
<!-- two alternatives for the operator: + and - -->
<!-- + path (upper of the two) -->
<g class="terminal" transform="translate(220, 56)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">+</text>
</g>
<path class="track" d="M 276,72 L 250,72"/>
<path class="track" d="M 220,72 C 200,72 190,62 180,52"/>
<!-- - path (lower) -->
<path class="track" d="M 276,72 C 276,98 268,108 250,108"/>
<g class="terminal" transform="translate(220, 92)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">-</text>
</g>
<path class="track" d="M 220,108 C 200,108 186,90 180,52"/>
<!-- arrow pointing up at rejoin -->
<path class="track" d="M 180,52 C 175,42 170,38 140,42"/>
<path d="M 135,46 L 140,36 L 145,46 z" fill="#94A3B8"/>
<!-- exit dot with outer ring -->
<circle fill="#94A3B8" cx="310" cy="32" r="4"/>
<circle fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5" cx="310" cy="32" r="7"/>
</g>
<!-- separator line between expr and unaryExpr -->
<line x1="10" y1="155" x2="890" y2="155" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== unaryExpr ==================== -->
<!-- 11-way alternative fan. Use literal type boxes for value types -->
<g transform="translate(0, 160)">
<!-- label background pill -->
<rect class="label-pill" x="2" y="84" width="98" height="24" rx="6"/>
<text class="label" x="10" y="100" text-anchor="start">unaryExpr</text>
<!-- entry dot with outer ring -->
<circle fill="#94A3B8" cx="120" cy="96" r="4"/>
<circle fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3" cx="120" cy="96" r="7"/>
<line class="track" x1="124" y1="96" x2="160" y2="96"/>
<!-- 1. funcCall (y=-30) -->
<path class="track" d="M 160,96 C 160,0 170,-30 190,-30"/>
<g class="nonterminal" transform="translate(190, -46)">
<rect width="92" height="32" rx="4"/>
<text x="46" y="16" text-anchor="middle" dominant-baseline="central">funcCall</text>
</g>
<path class="track" d="M 282,-30 C 380,-30 390,0 390,96"/>
<!-- 2. subQuery (y=2) -->
<path class="track" d="M 160,96 C 160,30 170,2 190,2"/>
<g class="nonterminal" transform="translate(190, -14)">
<rect width="92" height="32" rx="4"/>
<text x="46" y="16" text-anchor="middle" dominant-baseline="central">subQuery</text>
</g>
<path class="track" d="M 282,2 C 370,2 380,30 390,96"/>
<!-- 3. qualifiedRef (y=34) -->
<path class="track" d="M 160,96 C 160,54 170,34 190,34"/>
<g class="nonterminal" transform="translate(190, 18)">
<rect width="120" height="32" rx="4"/>
<text x="60" y="16" text-anchor="middle" dominant-baseline="central">qualifiedRef</text>
</g>
<path class="track" d="M 310,34 C 370,34 380,54 390,96"/>
<!-- 4. listLiteral (y=66) -->
<path class="track" d="M 160,96 C 160,76 170,66 190,66"/>
<g class="nonterminal" transform="translate(190, 50)">
<rect width="110" height="32" rx="4"/>
<text x="55" y="16" text-anchor="middle" dominant-baseline="central">listLiteral</text>
</g>
<path class="track" d="M 300,66 C 370,66 380,76 390,96"/>
<!-- 5. string (y=96 — straight through) -->
<line class="track" x1="160" y1="96" x2="190" y2="96"/>
<g class="lit-terminal" transform="translate(190, 80)">
<rect width="78" height="32" rx="16"/>
<text x="39" y="16" text-anchor="middle" dominant-baseline="central">string</text>
</g>
<line class="track" x1="268" y1="96" x2="390" y2="96"/>
<!-- 6. date (y=128) -->
<path class="track" d="M 160,96 C 160,116 170,128 190,128"/>
<g class="lit-terminal" transform="translate(190, 112)">
<rect width="64" height="32" rx="16"/>
<text x="32" y="16" text-anchor="middle" dominant-baseline="central">date</text>
</g>
<path class="track" d="M 254,128 C 370,128 380,116 390,96"/>
<!-- 7. duration (y=160) -->
<path class="track" d="M 160,96 C 160,140 170,160 190,160"/>
<g class="lit-terminal" transform="translate(190, 144)">
<rect width="86" height="32" rx="16"/>
<text x="43" y="16" text-anchor="middle" dominant-baseline="central">duration</text>
</g>
<path class="track" d="M 276,160 C 370,160 380,140 390,96"/>
<!-- 8. int (y=192) -->
<path class="track" d="M 160,96 C 160,166 170,192 190,192"/>
<g class="lit-terminal" transform="translate(190, 176)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">int</text>
</g>
<path class="track" d="M 240,192 C 370,192 380,166 390,96"/>
<!-- 9. empty (y=224) -->
<path class="track" d="M 160,96 C 160,196 170,224 190,224"/>
<g class="lit-terminal" transform="translate(190, 208)">
<rect width="70" height="32" rx="16"/>
<text x="35" y="16" text-anchor="middle" dominant-baseline="central">empty</text>
</g>
<path class="track" d="M 260,224 C 370,224 380,196 390,96"/>
<!-- 10. fieldRef (y=256) -->
<path class="track" d="M 160,96 C 160,224 170,256 190,256"/>
<g class="nonterminal" transform="translate(190, 240)">
<rect width="88" height="32" rx="4"/>
<text x="44" y="16" text-anchor="middle" dominant-baseline="central">fieldRef</text>
</g>
<path class="track" d="M 278,256 C 370,256 380,224 390,96"/>
<!-- 11. ( expr ) (y=288) -->
<path class="track" d="M 160,96 C 160,256 170,288 190,288"/>
<g class="terminal" transform="translate(190, 272)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">(</text>
</g>
<line class="track" x1="220" y1="288" x2="236" y2="288"/>
<g class="nonterminal" transform="translate(236, 272)">
<rect width="60" height="32" rx="4"/>
<text x="30" y="16" text-anchor="middle" dominant-baseline="central">expr</text>
</g>
<line class="track" x1="296" y1="288" x2="312" y2="288"/>
<g class="terminal" transform="translate(312, 272)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">)</text>
</g>
<path class="track" d="M 342,288 C 370,288 380,256 390,96"/>
<!-- exit -->
<line class="track" x1="390" y1="96" x2="430" y2="96"/>
<!-- exit dot with outer ring -->
<circle fill="#94A3B8" cx="430" cy="96" r="4"/>
<circle fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5" cx="430" cy="96" r="7"/>
</g>
<!-- separator line between unaryExpr and funcCall -->
<line x1="10" y1="505" x2="890" y2="505" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== funcCall ==================== -->
<g transform="translate(0, 510)">
<!-- label background pill -->
<rect class="label-pill" x="2" y="20" width="82" height="24" rx="6"/>
<text class="label" x="10" y="36" text-anchor="start">funcCall</text>
<!-- entry dot with outer ring -->
<circle fill="#94A3B8" cx="120" cy="32" r="4"/>
<circle fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3" cx="120" cy="32" r="7"/>
<line class="track" x1="124" y1="32" x2="150" y2="32"/>
<g class="nonterminal" transform="translate(150, 16)">
<rect width="100" height="32" rx="4"/>
<text x="50" y="16" text-anchor="middle" dominant-baseline="central">identifier</text>
</g>
<line class="track" x1="250" y1="32" x2="270" y2="32"/>
<g class="terminal" transform="translate(270, 16)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">(</text>
</g>
<line class="track" x1="300" y1="32" x2="330" y2="32"/>
<!-- optional args: bypass above, expr with comma loopback below -->
<!-- main path: expr -->
<g class="nonterminal" transform="translate(330, 16)">
<rect width="60" height="32" rx="4"/>
<text x="30" y="16" text-anchor="middle" dominant-baseline="central">expr</text>
</g>
<line class="track" x1="390" y1="32" x2="440" y2="32"/>
<!-- loopback below with comma -->
<path class="track" d="M 430,32 C 430,62 420,72 400,72"/>
<g class="terminal" transform="translate(355, 56)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">,</text>
</g>
<path class="track" d="M 355,72 C 340,72 330,62 330,42"/>
<path d="M 325,46 L 330,36 L 335,46 z" fill="#94A3B8"/>
<!-- bypass above: no args -->
<path class="track" d="M 320,32 C 320,5 330,-5 350,-5 L 410,-5 C 430,-5 440,5 440,32"/>
<g class="terminal" transform="translate(440, 16)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">)</text>
</g>
<line class="track" x1="470" y1="32" x2="510" y2="32"/>
<!-- exit dot with outer ring -->
<circle fill="#94A3B8" cx="510" cy="32" r="4"/>
<circle fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5" cx="510" cy="32" r="7"/>
</g>
<!-- separator line between funcCall and qualifiedRef -->
<line x1="10" y1="605" x2="890" y2="605" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== qualifiedRef ==================== -->
<g transform="translate(0, 610)">
<!-- label background pill -->
<rect class="label-pill" x="2" y="20" width="118" height="24" rx="6"/>
<text class="label" x="10" y="36" text-anchor="start">qualifiedRef</text>
<!-- entry dot with outer ring -->
<circle fill="#94A3B8" cx="140" cy="32" r="4"/>
<circle fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3" cx="140" cy="32" r="7"/>
<line class="track" x1="144" y1="32" x2="180" y2="32"/>
<!-- alternative: old | new -->
<path class="track" d="M 180,32 C 180,10 190,0 210,0"/>
<g class="terminal" transform="translate(210, -16)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">old</text>
</g>
<path class="track" d="M 260,0 C 280,0 290,10 290,32"/>
<path class="track" d="M 180,32 C 180,54 190,64 210,64"/>
<g class="terminal" transform="translate(210, 48)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">new</text>
</g>
<path class="track" d="M 260,64 C 280,64 290,54 290,32"/>
<line class="track" x1="290" y1="32" x2="310" y2="32"/>
<g class="terminal" transform="translate(310, 16)">
<rect width="24" height="32" rx="16"/>
<text x="12" y="16" text-anchor="middle" dominant-baseline="central">.</text>
</g>
<line class="track" x1="334" y1="32" x2="354" y2="32"/>
<g class="nonterminal" transform="translate(354, 16)">
<rect width="100" height="32" rx="4"/>
<text x="50" y="16" text-anchor="middle" dominant-baseline="central">identifier</text>
</g>
<line class="track" x1="454" y1="32" x2="490" y2="32"/>
<!-- exit dot with outer ring -->
<circle fill="#94A3B8" cx="490" cy="32" r="4"/>
<circle fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5" cx="490" cy="32" r="7"/>
</g>
</g><!-- end content wrapper -->
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,95 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 350">
<!--
Grid system:
- Row height: 40px, gap: 6px
- Column widths: context=200, old=100, new=100
- Start x=40, y=60
- Indicator pills: 60x28, rx=14
- All fills use opacity for transparency on any background
-->
<defs>
<style>
.title { font: 700 16px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #A5B4FC; }
.col-hdr { font: 700 15px 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; }
.row-label { font: 500 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #CBD5E1; }
.note-text { font: 500 11px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #FBBF24; }
.legend-text { font: 400 11px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #94A3B8; }
.indicator { font: 700 14px -apple-system, sans-serif; }
</style>
<!-- Checkmark icon -->
<symbol id="check" viewBox="0 0 16 16">
<path d="M3 8.5 L6.5 12 L13 4" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</symbol>
<!-- X icon -->
<symbol id="cross" viewBox="0 0 16 16">
<path d="M4 4 L12 12 M12 4 L4 12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" fill="none"/>
</symbol>
<!-- Drop shadow filter -->
<filter id="shadow" x="-4%" y="-4%" width="108%" height="116%">
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Title -->
<text class="title" x="250" y="30" text-anchor="middle">Qualifier scope by context</text>
<!-- Subtle top border -->
<line x1="40" y1="38" x2="470" y2="38" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- Column header backgrounds -->
<rect x="270" y="42" width="80" height="28" rx="6" fill="#818CF8" fill-opacity="0.08"/>
<rect x="380" y="42" width="80" height="28" rx="6" fill="#34D399" fill-opacity="0.08"/>
<!-- Column headers -->
<text class="col-hdr" x="310" y="62" text-anchor="middle" fill="#818CF8">old.</text>
<text class="col-hdr" x="420" y="62" text-anchor="middle" fill="#34D399">new.</text>
<!-- Separator line -->
<line x1="40" y1="74" x2="470" y2="74" stroke="#475569" stroke-width="1" stroke-opacity="0.5"/>
<!-- Row 1: standalone statement -->
<text class="row-label" x="50" y="102">standalone statement</text>
<rect x="280" y="86" width="60" height="28" rx="14" fill="#EF4444" fill-opacity="0.2" stroke="#EF4444" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<use href="#cross" x="296" y="92" width="16" height="16" color="#F87171"/>
<rect x="390" y="86" width="60" height="28" rx="14" fill="#EF4444" fill-opacity="0.2" stroke="#EF4444" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<use href="#cross" x="406" y="92" width="16" height="16" color="#F87171"/>
<!-- Row 2: create trigger (alternate row highlight) -->
<rect x="40" y="120" width="430" height="40" rx="4" fill="#94A3B8" fill-opacity="0.04"/>
<text class="row-label" x="50" y="148">create trigger</text>
<rect x="280" y="132" width="60" height="28" rx="14" fill="#EF4444" fill-opacity="0.2" stroke="#EF4444" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<use href="#cross" x="296" y="138" width="16" height="16" color="#F87171"/>
<rect x="390" y="132" width="60" height="28" rx="14" fill="#10B981" fill-opacity="0.2" stroke="#10B981" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<use href="#check" x="406" y="138" width="16" height="16" color="#34D399"/>
<!-- Row 3: update trigger -->
<text class="row-label" x="50" y="194">update trigger</text>
<rect x="280" y="178" width="60" height="28" rx="14" fill="#10B981" fill-opacity="0.2" stroke="#10B981" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<use href="#check" x="296" y="184" width="16" height="16" color="#34D399"/>
<rect x="390" y="178" width="60" height="28" rx="14" fill="#10B981" fill-opacity="0.2" stroke="#10B981" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<use href="#check" x="406" y="184" width="16" height="16" color="#34D399"/>
<!-- Row 4: delete trigger (alternate row highlight) -->
<rect x="40" y="212" width="430" height="40" rx="4" fill="#94A3B8" fill-opacity="0.04"/>
<text class="row-label" x="50" y="240">delete trigger</text>
<rect x="280" y="224" width="60" height="28" rx="14" fill="#10B981" fill-opacity="0.2" stroke="#10B981" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<use href="#check" x="296" y="230" width="16" height="16" color="#34D399"/>
<rect x="390" y="224" width="60" height="28" rx="14" fill="#EF4444" fill-opacity="0.2" stroke="#EF4444" stroke-width="1" stroke-opacity="0.4" filter="url(#shadow)"/>
<use href="#cross" x="406" y="230" width="16" height="16" color="#F87171"/>
<!-- Separator -->
<line x1="40" y1="264" x2="470" y2="264" stroke="#475569" stroke-width="1" stroke-opacity="0.3"/>
<!-- Note about quantifier body -->
<rect x="40" y="274" width="430" height="30" rx="8" fill="#FBBF24" fill-opacity="0.1" stroke="#FBBF24" stroke-width="1" stroke-opacity="0.3" filter="url(#shadow)"/>
<text class="note-text" x="255" y="293" text-anchor="middle">quantifier body (any / all): both old. and new. are disabled</text>
<!-- Legend -->
<rect x="100" y="316" width="16" height="16" rx="8" fill="#10B981" fill-opacity="0.2" stroke="#10B981" stroke-width="1" stroke-opacity="0.4"/>
<use href="#check" x="102" y="318" width="12" height="12" color="#34D399"/>
<text class="legend-text" x="124" y="328">allowed</text>
<rect x="230" y="316" width="16" height="16" rx="8" fill="#EF4444" fill-opacity="0.2" stroke="#EF4444" stroke-width="1" stroke-opacity="0.4"/>
<use href="#cross" x="232" y="318" width="12" height="12" color="#F87171"/>
<text class="legend-text" x="254" y="328">not allowed</text>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -0,0 +1,235 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 820 560">
<!--
Grid system for Ruki railroad diagrams (transparent/dark-bg compatible)
========================================
Terminal (keyword) box: height=32, rx=16 (fully rounded ends)
Non-terminal box: height=32, rx=4 (subtle rounding)
Track vertical spacing: ~130px between track baselines
Label column: x=10, track starts at x=140
Line/dot color: #94A3B8
Terminal: fill=#3B82F6 fill-opacity=0.15, stroke=#3B82F6 stroke-opacity=0.5
Non-terminal: fill=#F59E0B fill-opacity=0.15, stroke=#F59E0B stroke-opacity=0.4
Loopback: below main line (standard railroad repetition)
Bypass: above main line for optional elements
Visual polish:
- Drop shadow filter on all boxes (subtle, dark-bg friendly)
- Label background pills with rounded rect behind each rule name
- Entry dots with outer ring; exit dots with double circle
- Separator lines between tracks
- Title text at top in accent color
- viewBox height +20 to accommodate title; content shifted down 8px
-->
<defs>
<filter id="shadow" x="-4%" y="-4%" width="108%" height="116%">
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
</filter>
<marker id="arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="10" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 3.5 L 0 7 z" fill="#94A3B8"/>
</marker>
<style>
.title { font: 700 16px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #A5B4FC; }
.label { font: bold 14px 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; fill: #94A3B8; }
.terminal rect { fill: #3B82F6; fill-opacity: 0.15; stroke: #3B82F6; stroke-width: 1.5; stroke-opacity: 0.5; filter: url(#shadow); }
.terminal text { font: 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #93C5FD; font-weight: 600; }
.nonterminal rect { fill: #F59E0B; fill-opacity: 0.15; stroke: #F59E0B; stroke-width: 1.5; stroke-opacity: 0.4; filter: url(#shadow); }
.nonterminal text { font: 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #FCD34D; font-weight: 500; }
.track { stroke: #94A3B8; stroke-width: 1.5; fill: none; }
.track-dot { fill: #94A3B8; }
.separator { stroke: #475569; stroke-width: 0.5; stroke-opacity: 0.3; }
</style>
</defs>
<!-- diagram title -->
<text class="title" x="410" y="22" text-anchor="middle">Statement grammar</text>
<!-- content shifted down 8px to accommodate title -->
<g transform="translate(0, 8)">
<!-- ==================== selectStmt ==================== -->
<!-- Main line at y=50, bypass above at y=10 -->
<g transform="translate(0, 30)">
<!-- label background pill -->
<g class="label-group">
<rect x="5" y="38" width="120" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="65" y="54" text-anchor="middle">selectStmt</text>
</g>
<!-- entry dot with ring -->
<circle class="track-dot" cx="140" cy="50" r="4"/>
<circle cx="140" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="144" y1="50" x2="170" y2="50"/>
<!-- select keyword -->
<g class="terminal" transform="translate(170, 34)">
<rect width="72" height="32" rx="16"/>
<text x="36" y="16" text-anchor="middle" dominant-baseline="central">select</text>
</g>
<line class="track" x1="242" y1="50" x2="280" y2="50"/>
<!-- main path: where + condition -->
<g class="terminal" transform="translate(280, 34)">
<rect width="70" height="32" rx="16"/>
<text x="35" y="16" text-anchor="middle" dominant-baseline="central">where</text>
</g>
<line class="track" x1="350" y1="50" x2="380" y2="50"/>
<g class="nonterminal" transform="translate(380, 34)">
<rect width="100" height="32" rx="4"/>
<text x="50" y="16" text-anchor="middle" dominant-baseline="central">condition</text>
</g>
<line class="track" x1="480" y1="50" x2="540" y2="50"/>
<!-- bypass path above: skip where+condition -->
<path class="track" d="M 270,50 C 270,20 280,10 300,10 L 510,10 C 530,10 540,20 540,50"/>
<!-- exit dot with double circle -->
<circle class="track-dot" cx="540" cy="50" r="4"/>
<circle cx="540" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
<!-- separator between selectStmt and createStmt -->
<line class="separator" x1="10" y1="140" x2="810" y2="140"/>
<!-- ==================== createStmt ==================== -->
<!-- Main line at y=32, loopback below at y=72 -->
<g transform="translate(0, 150)">
<!-- label background pill -->
<g class="label-group">
<rect x="5" y="18" width="122" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="66" y="34" text-anchor="middle">createStmt</text>
</g>
<!-- entry dot with ring -->
<circle class="track-dot" cx="140" cy="32" r="4"/>
<circle cx="140" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="144" y1="32" x2="170" y2="32"/>
<!-- create keyword -->
<g class="terminal" transform="translate(170, 16)">
<rect width="74" height="32" rx="16"/>
<text x="37" y="16" text-anchor="middle" dominant-baseline="central">create</text>
</g>
<line class="track" x1="244" y1="32" x2="280" y2="32"/>
<!-- assignment non-terminal -->
<g class="nonterminal" transform="translate(280, 16)">
<rect width="110" height="32" rx="4"/>
<text x="55" y="16" text-anchor="middle" dominant-baseline="central">assignment</text>
</g>
<line class="track" x1="390" y1="32" x2="440" y2="32"/>
<!-- loopback below: from after assignment back to before assignment -->
<path class="track" d="M 430,32 C 430,62 420,76 400,76 L 300,76 C 280,76 270,62 270,42"/>
<!-- arrowhead pointing up at the rejoin -->
<path d="M 265,46 L 270,36 L 275,46 z" fill="#94A3B8"/>
<!-- exit dot with double circle -->
<circle class="track-dot" cx="440" cy="32" r="4"/>
<circle cx="440" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
<!-- separator between createStmt and updateStmt -->
<line class="separator" x1="10" y1="270" x2="810" y2="270"/>
<!-- ==================== updateStmt ==================== -->
<!-- Main line at y=32, loopback below at y=76 -->
<g transform="translate(0, 280)">
<!-- label background pill -->
<g class="label-group">
<rect x="5" y="18" width="124" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="67" y="34" text-anchor="middle">updateStmt</text>
</g>
<!-- entry dot with ring -->
<circle class="track-dot" cx="140" cy="32" r="4"/>
<circle cx="140" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="144" y1="32" x2="170" y2="32"/>
<!-- update keyword -->
<g class="terminal" transform="translate(170, 16)">
<rect width="78" height="32" rx="16"/>
<text x="39" y="16" text-anchor="middle" dominant-baseline="central">update</text>
</g>
<line class="track" x1="248" y1="32" x2="268" y2="32"/>
<!-- where keyword -->
<g class="terminal" transform="translate(268, 16)">
<rect width="70" height="32" rx="16"/>
<text x="35" y="16" text-anchor="middle" dominant-baseline="central">where</text>
</g>
<line class="track" x1="338" y1="32" x2="358" y2="32"/>
<!-- condition -->
<g class="nonterminal" transform="translate(358, 16)">
<rect width="100" height="32" rx="4"/>
<text x="50" y="16" text-anchor="middle" dominant-baseline="central">condition</text>
</g>
<line class="track" x1="458" y1="32" x2="478" y2="32"/>
<!-- set keyword -->
<g class="terminal" transform="translate(478, 16)">
<rect width="50" height="32" rx="16"/>
<text x="25" y="16" text-anchor="middle" dominant-baseline="central">set</text>
</g>
<line class="track" x1="528" y1="32" x2="548" y2="32"/>
<!-- assignment non-terminal -->
<g class="nonterminal" transform="translate(548, 16)">
<rect width="110" height="32" rx="4"/>
<text x="55" y="16" text-anchor="middle" dominant-baseline="central">assignment</text>
</g>
<line class="track" x1="658" y1="32" x2="710" y2="32"/>
<!-- loopback below -->
<path class="track" d="M 700,32 C 700,62 690,76 670,76 L 568,76 C 548,76 538,62 538,42"/>
<path d="M 533,46 L 538,36 L 543,46 z" fill="#94A3B8"/>
<!-- exit dot with double circle -->
<circle class="track-dot" cx="710" cy="32" r="4"/>
<circle cx="710" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
<!-- separator between updateStmt and deleteStmt -->
<line class="separator" x1="10" y1="400" x2="810" y2="400"/>
<!-- ==================== deleteStmt ==================== -->
<g transform="translate(0, 420)">
<!-- label background pill -->
<g class="label-group">
<rect x="5" y="18" width="120" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="65" y="34" text-anchor="middle">deleteStmt</text>
</g>
<!-- entry dot with ring -->
<circle class="track-dot" cx="140" cy="32" r="4"/>
<circle cx="140" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<line class="track" x1="144" y1="32" x2="170" y2="32"/>
<!-- delete keyword -->
<g class="terminal" transform="translate(170, 16)">
<rect width="72" height="32" rx="16"/>
<text x="36" y="16" text-anchor="middle" dominant-baseline="central">delete</text>
</g>
<line class="track" x1="242" y1="32" x2="270" y2="32"/>
<!-- where keyword -->
<g class="terminal" transform="translate(270, 16)">
<rect width="70" height="32" rx="16"/>
<text x="35" y="16" text-anchor="middle" dominant-baseline="central">where</text>
</g>
<line class="track" x1="340" y1="32" x2="370" y2="32"/>
<!-- condition -->
<g class="nonterminal" transform="translate(370, 16)">
<rect width="100" height="32" rx="4"/>
<text x="50" y="16" text-anchor="middle" dominant-baseline="central">condition</text>
</g>
<line class="track" x1="470" y1="32" x2="510" y2="32"/>
<!-- exit dot with double circle -->
<circle class="track-dot" cx="510" cy="32" r="4"/>
<circle cx="510" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,307 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 708">
<!--
Trigger railroad diagram — transparent/dark-bg compatible
6 tracks: trigger, timing, event, action, deny, runAction
-->
<defs>
<marker id="arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="10" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 3.5 L 0 7 z" fill="#94A3B8"/>
</marker>
<filter id="shadow" x="-4%" y="-4%" width="108%" height="116%">
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
</filter>
<style>
.label { font: bold 14px 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; fill: #94A3B8; }
.title { font: 700 16px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #A5B4FC; }
.terminal rect { fill: #3B82F6; fill-opacity: 0.15; stroke: #3B82F6; stroke-width: 1.5; stroke-opacity: 0.5; filter: url(#shadow); }
.terminal text { font: 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #93C5FD; font-weight: 600; }
.nonterminal rect { fill: #F59E0B; fill-opacity: 0.15; stroke: #F59E0B; stroke-width: 1.5; stroke-opacity: 0.5; filter: url(#shadow); }
.nonterminal text { font: 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #FCD34D; font-weight: 500; }
.track { stroke: #94A3B8; stroke-width: 1.5; fill: none; }
</style>
</defs>
<!-- title -->
<text class="title" x="450" y="22" text-anchor="middle">Trigger grammar</text>
<!-- all tracks shifted down 8px to accommodate title -->
<g transform="translate(0, 8)">
<!-- ==================== trigger ==================== -->
<!-- Main line y=50, bypass above at y=10 for optional where -->
<g transform="translate(0, 30)">
<g>
<rect x="2" y="40" width="71" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="54" text-anchor="start">trigger</text>
</g>
<!-- entry dot with outer ring -->
<circle cx="120" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<circle cx="120" cy="50" r="4" fill="#94A3B8"/>
<line class="track" x1="124" y1="50" x2="150" y2="50"/>
<!-- timing non-terminal -->
<g class="nonterminal" transform="translate(150, 34)">
<rect width="80" height="32" rx="4"/>
<text x="40" y="16" text-anchor="middle" dominant-baseline="central">timing</text>
</g>
<line class="track" x1="230" y1="50" x2="256" y2="50"/>
<!-- event non-terminal -->
<g class="nonterminal" transform="translate(256, 34)">
<rect width="72" height="32" rx="4"/>
<text x="36" y="16" text-anchor="middle" dominant-baseline="central">event</text>
</g>
<line class="track" x1="328" y1="50" x2="358" y2="50"/>
<!-- optional where + condition: main path -->
<g class="terminal" transform="translate(358, 34)">
<rect width="70" height="32" rx="16"/>
<text x="35" y="16" text-anchor="middle" dominant-baseline="central">where</text>
</g>
<line class="track" x1="428" y1="50" x2="454" y2="50"/>
<g class="nonterminal" transform="translate(454, 34)">
<rect width="100" height="32" rx="4"/>
<text x="50" y="16" text-anchor="middle" dominant-baseline="central">condition</text>
</g>
<line class="track" x1="554" y1="50" x2="600" y2="50"/>
<!-- bypass above for optional where+condition -->
<path class="track" d="M 348,50 C 348,20 358,10 378,10 L 570,10 C 590,10 600,20 600,50"/>
<!-- alternative: action or deny -->
<!-- upper path: action -->
<path class="track" d="M 600,50 C 600,20 610,10 630,10"/>
<g class="nonterminal" transform="translate(630, -6)">
<rect width="80" height="32" rx="4"/>
<text x="40" y="16" text-anchor="middle" dominant-baseline="central">action</text>
</g>
<path class="track" d="M 710,10 C 730,10 740,20 740,50"/>
<!-- lower path: deny -->
<path class="track" d="M 600,50 C 600,80 610,90 630,90"/>
<g class="nonterminal" transform="translate(630, 74)">
<rect width="80" height="32" rx="4"/>
<text x="40" y="16" text-anchor="middle" dominant-baseline="central">deny</text>
</g>
<path class="track" d="M 710,90 C 730,90 740,80 740,50"/>
<line class="track" x1="740" y1="50" x2="780" y2="50"/>
<!-- exit dot with thicker outer ring -->
<circle cx="780" cy="50" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
<circle cx="780" cy="50" r="4" fill="#94A3B8"/>
</g>
<!-- separator between trigger and timing -->
<line x1="10" y1="160" x2="890" y2="160" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== timing ==================== -->
<g transform="translate(0, 170)">
<g>
<rect x="2" y="22" width="64" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="36" text-anchor="start">timing</text>
</g>
<!-- entry dot with outer ring -->
<circle cx="120" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<circle cx="120" cy="32" r="4" fill="#94A3B8"/>
<line class="track" x1="124" y1="32" x2="160" y2="32"/>
<!-- upper: before -->
<path class="track" d="M 160,32 C 160,10 170,0 190,0"/>
<g class="terminal" transform="translate(190, -16)">
<rect width="78" height="32" rx="16"/>
<text x="39" y="16" text-anchor="middle" dominant-baseline="central">before</text>
</g>
<path class="track" d="M 268,0 C 288,0 298,10 298,32"/>
<!-- lower: after -->
<path class="track" d="M 160,32 C 160,54 170,64 190,64"/>
<g class="terminal" transform="translate(190, 48)">
<rect width="78" height="32" rx="16"/>
<text x="39" y="16" text-anchor="middle" dominant-baseline="central">after</text>
</g>
<path class="track" d="M 268,64 C 288,64 298,54 298,32"/>
<line class="track" x1="298" y1="32" x2="340" y2="32"/>
<!-- exit dot with thicker outer ring -->
<circle cx="340" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
<circle cx="340" cy="32" r="4" fill="#94A3B8"/>
</g>
<!-- separator between timing and event -->
<line x1="10" y1="278" x2="890" y2="278" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== event ==================== -->
<g transform="translate(0, 290)">
<g>
<rect x="2" y="22" width="56" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="36" text-anchor="start">event</text>
</g>
<!-- entry dot with outer ring -->
<circle cx="120" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<circle cx="120" cy="32" r="4" fill="#94A3B8"/>
<line class="track" x1="124" y1="32" x2="160" y2="32"/>
<!-- top: create -->
<path class="track" d="M 160,32 C 160,2 170,-8 190,-8"/>
<g class="terminal" transform="translate(190, -24)">
<rect width="78" height="32" rx="16"/>
<text x="39" y="16" text-anchor="middle" dominant-baseline="central">create</text>
</g>
<path class="track" d="M 268,-8 C 288,-8 298,2 298,32"/>
<!-- middle: update -->
<g class="terminal" transform="translate(190, 16)">
<rect width="78" height="32" rx="16"/>
<text x="39" y="16" text-anchor="middle" dominant-baseline="central">update</text>
</g>
<!-- straight through for middle option -->
<line class="track" x1="160" y1="32" x2="190" y2="32"/>
<line class="track" x1="268" y1="32" x2="298" y2="32"/>
<!-- bottom: delete -->
<path class="track" d="M 160,32 C 160,62 170,72 190,72"/>
<g class="terminal" transform="translate(190, 56)">
<rect width="78" height="32" rx="16"/>
<text x="39" y="16" text-anchor="middle" dominant-baseline="central">delete</text>
</g>
<path class="track" d="M 268,72 C 288,72 298,62 298,32"/>
<line class="track" x1="298" y1="32" x2="340" y2="32"/>
<!-- exit dot with thicker outer ring -->
<circle cx="340" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
<circle cx="340" cy="32" r="4" fill="#94A3B8"/>
</g>
<!-- separator between event and action -->
<line x1="10" y1="407" x2="890" y2="407" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== action ==================== -->
<!-- Center at y=55 to give room for 4 alternatives above and below -->
<g transform="translate(0, 420)">
<g>
<rect x="2" y="45" width="64" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="59" text-anchor="start">action</text>
</g>
<!-- entry dot with outer ring -->
<circle cx="120" cy="55" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<circle cx="120" cy="55" r="4" fill="#94A3B8"/>
<line class="track" x1="124" y1="55" x2="160" y2="55"/>
<!-- top: runAction -->
<path class="track" d="M 160,55 C 160,10 170,-5 190,-5"/>
<g class="nonterminal" transform="translate(190, -21)">
<rect width="104" height="32" rx="4"/>
<text x="52" y="16" text-anchor="middle" dominant-baseline="central">runAction</text>
</g>
<path class="track" d="M 294,-5 C 314,-5 324,10 324,55"/>
<!-- upper-middle: createStmt -->
<path class="track" d="M 160,55 C 160,35 170,25 190,25"/>
<g class="nonterminal" transform="translate(190, 9)">
<rect width="104" height="32" rx="4"/>
<text x="52" y="16" text-anchor="middle" dominant-baseline="central">createStmt</text>
</g>
<path class="track" d="M 294,25 C 314,25 324,35 324,55"/>
<!-- lower-middle: updateStmt (straight through) -->
<line class="track" x1="160" y1="55" x2="190" y2="55"/>
<g class="nonterminal" transform="translate(190, 39)">
<rect width="104" height="32" rx="4"/>
<text x="52" y="16" text-anchor="middle" dominant-baseline="central">updateStmt</text>
</g>
<line class="track" x1="294" y1="55" x2="324" y2="55"/>
<!-- bottom: deleteStmt -->
<path class="track" d="M 160,55 C 160,75 170,85 190,85"/>
<g class="nonterminal" transform="translate(190, 69)">
<rect width="104" height="32" rx="4"/>
<text x="52" y="16" text-anchor="middle" dominant-baseline="central">deleteStmt</text>
</g>
<path class="track" d="M 294,85 C 314,85 324,75 324,55"/>
<line class="track" x1="324" y1="55" x2="370" y2="55"/>
<!-- exit dot with thicker outer ring -->
<circle cx="370" cy="55" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
<circle cx="370" cy="55" r="4" fill="#94A3B8"/>
</g>
<!-- separator between action and deny -->
<line x1="10" y1="548" x2="890" y2="548" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== deny ==================== -->
<g transform="translate(0, 560)">
<g>
<rect x="2" y="22" width="52" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="36" text-anchor="start">deny</text>
</g>
<!-- entry dot with outer ring -->
<circle cx="120" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<circle cx="120" cy="32" r="4" fill="#94A3B8"/>
<line class="track" x1="124" y1="32" x2="150" y2="32"/>
<g class="terminal" transform="translate(150, 16)">
<rect width="62" height="32" rx="16"/>
<text x="31" y="16" text-anchor="middle" dominant-baseline="central">deny</text>
</g>
<line class="track" x1="212" y1="32" x2="240" y2="32"/>
<g class="nonterminal" transform="translate(240, 16)">
<rect width="72" height="32" rx="4"/>
<text x="36" y="16" text-anchor="middle" dominant-baseline="central">string</text>
</g>
<line class="track" x1="312" y1="32" x2="350" y2="32"/>
<!-- exit dot with thicker outer ring -->
<circle cx="350" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
<circle cx="350" cy="32" r="4" fill="#94A3B8"/>
</g>
<!-- separator between deny and runAction -->
<line x1="10" y1="620" x2="890" y2="620" stroke="#475569" stroke-width="0.5" stroke-opacity="0.3"/>
<!-- ==================== runAction ==================== -->
<g transform="translate(0, 630)">
<g>
<rect x="2" y="22" width="90" height="24" rx="6" fill="#94A3B8" fill-opacity="0.08" stroke="#94A3B8" stroke-width="0.5" stroke-opacity="0.2"/>
<text class="label" x="10" y="36" text-anchor="start">runAction</text>
</g>
<!-- entry dot with outer ring -->
<circle cx="120" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1" stroke-opacity="0.3"/>
<circle cx="120" cy="32" r="4" fill="#94A3B8"/>
<line class="track" x1="124" y1="32" x2="150" y2="32"/>
<g class="terminal" transform="translate(150, 16)">
<rect width="52" height="32" rx="16"/>
<text x="26" y="16" text-anchor="middle" dominant-baseline="central">run</text>
</g>
<line class="track" x1="202" y1="32" x2="222" y2="32"/>
<g class="terminal" transform="translate(222, 16)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">(</text>
</g>
<line class="track" x1="252" y1="32" x2="272" y2="32"/>
<g class="nonterminal" transform="translate(272, 16)">
<rect width="60" height="32" rx="4"/>
<text x="30" y="16" text-anchor="middle" dominant-baseline="central">expr</text>
</g>
<line class="track" x1="332" y1="32" x2="352" y2="32"/>
<g class="terminal" transform="translate(352, 16)">
<rect width="30" height="32" rx="16"/>
<text x="15" y="16" text-anchor="middle" dominant-baseline="central">)</text>
</g>
<line class="track" x1="382" y1="32" x2="420" y2="32"/>
<!-- exit dot with thicker outer ring -->
<circle cx="420" cy="32" r="7" fill="none" stroke="#94A3B8" stroke-width="1.5" stroke-opacity="0.5"/>
<circle cx="420" cy="32" r="4" fill="#94A3B8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,106 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 820 310">
<defs>
<filter id="shadow" x="-4%" y="-4%" width="108%" height="116%">
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
</filter>
<filter id="success-glow" x="-10%" y="-10%" width="120%" height="120%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<marker id="flow-arrow" viewBox="0 0 12 8" refX="12" refY="4" markerWidth="12" markerHeight="8" orient="auto">
<path d="M 0 0 L 12 4 L 0 8 z" fill="#94A3B8"/>
</marker>
<marker id="err-arrow" viewBox="0 0 12 8" refX="12" refY="4" markerWidth="12" markerHeight="8" orient="auto">
<path d="M 0 0 L 12 4 L 0 8 z" fill="#F87171"/>
</marker>
<style>
.title { font: 700 16px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #A5B4FC; }
.stage-text { font: 700 13px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #93C5FD; }
.data-text { font: 600 12px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #6EE7B7; }
.err-title { font: 700 11px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #FCA5A5; }
.err-detail { font: 400 10px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #FDA4AF; }
.flow-line { stroke: #94A3B8; stroke-width: 1.5; fill: none; marker-end: url(#flow-arrow); }
.err-line { stroke: #F87171; stroke-width: 1.5; fill: none; stroke-dasharray: 5,3; marker-end: url(#err-arrow); }
.err-line-solid { stroke: #F87171; stroke-width: 1.5; fill: none; marker-end: url(#err-arrow); }
.stage-label { font: 500 10px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #64748B; }
.legend-text { font: 400 11px -apple-system, 'Segoe UI', Roboto, sans-serif; fill: #94A3B8; }
</style>
</defs>
<text class="title" x="410" y="24" text-anchor="middle">Validation pipeline</text>
<!-- ===== Main pipeline (y=70) ===== -->
<!-- Input text -->
<rect x="20" y="50" width="90" height="40" rx="10" fill="#10B981" fill-opacity="0.12" stroke="#10B981" stroke-width="1.5" stroke-opacity="0.4" filter="url(#shadow)"/>
<text class="data-text" x="65" y="74" text-anchor="middle">Input text</text>
<line class="flow-line" x1="110" y1="70" x2="148" y2="70"/>
<!-- Lexer / Parser -->
<rect x="148" y="46" width="134" height="48" rx="10" fill="#3B82F6" fill-opacity="0.12" stroke="#3B82F6" stroke-width="1.5" stroke-opacity="0.4" filter="url(#shadow)"/>
<text class="stage-text" x="215" y="74" text-anchor="middle">Lexer / Parser</text>
<line class="flow-line" x1="282" y1="70" x2="318" y2="70"/>
<!-- AST -->
<rect x="318" y="52" width="60" height="36" rx="10" fill="#10B981" fill-opacity="0.12" stroke="#10B981" stroke-width="1.5" stroke-opacity="0.4" filter="url(#shadow)"/>
<text class="data-text" x="348" y="74" text-anchor="middle">AST</text>
<line class="flow-line" x1="378" y1="70" x2="414" y2="70"/>
<!-- Semantic Validator -->
<rect x="414" y="46" width="176" height="48" rx="10" fill="#3B82F6" fill-opacity="0.12" stroke="#3B82F6" stroke-width="1.5" stroke-opacity="0.4" filter="url(#shadow)"/>
<text class="stage-text" x="502" y="74" text-anchor="middle">Semantic Validator</text>
<line class="flow-line" x1="590" y1="70" x2="626" y2="70"/>
<!-- Valid AST -->
<rect x="626" y="50" width="100" height="40" rx="10" fill="#10B981" fill-opacity="0.12" stroke="#10B981" stroke-width="1.5" stroke-opacity="0.4" filter="url(#success-glow)"/>
<text class="data-text" x="676" y="74" text-anchor="middle">Valid AST</text>
<!-- ===== Parse errors (branch down from Lexer/Parser) ===== -->
<line class="err-line" x1="215" y1="94" x2="215" y2="140"/>
<!-- stage 1 label with pill background -->
<rect x="191" y="122" width="50" height="16" rx="4" fill="#64748B" fill-opacity="0.15"/>
<text class="stage-label" x="215" y="134" text-anchor="middle">stage 1</text>
<rect x="130" y="144" width="170" height="52" rx="8" fill="#EF4444" fill-opacity="0.1" stroke="#EF4444" stroke-width="1.5" stroke-opacity="0.35" filter="url(#shadow)"/>
<text class="err-title" x="215" y="164" text-anchor="middle">Parse errors</text>
<text class="err-detail" x="215" y="180" text-anchor="middle">unknown keyword, missing clause, bad syntax</text>
<!-- ===== Semantic errors (branch down from Semantic Validator) ===== -->
<line class="err-line" x1="502" y1="94" x2="502" y2="140"/>
<!-- stage 2 label with pill background -->
<rect x="478" y="122" width="50" height="16" rx="4" fill="#64748B" fill-opacity="0.15"/>
<text class="stage-label" x="502" y="134" text-anchor="middle">stage 2</text>
<!-- Semantic error umbrella -->
<rect x="350" y="144" width="304" height="36" rx="8" fill="#EF4444" fill-opacity="0.1" stroke="#EF4444" stroke-width="1.5" stroke-opacity="0.35" filter="url(#shadow)"/>
<text class="err-title" x="502" y="166" text-anchor="middle">Semantic validation errors</text>
<!-- Sub-category connectors -->
<line class="err-line-solid" x1="400" y1="180" x2="400" y2="200"/>
<line class="err-line-solid" x1="502" y1="180" x2="502" y2="200"/>
<line class="err-line-solid" x1="604" y1="180" x2="604" y2="200"/>
<!-- Sub-category boxes -->
<rect x="345" y="200" width="110" height="52" rx="8" fill="#EF4444" fill-opacity="0.08" stroke="#EF4444" stroke-width="1" stroke-opacity="0.25" filter="url(#shadow)"/>
<text class="err-title" x="400" y="219" text-anchor="middle">Structural</text>
<text class="err-detail" x="400" y="237" text-anchor="middle">dup fields, trigger rules</text>
<rect x="467" y="200" width="110" height="52" rx="8" fill="#EF4444" fill-opacity="0.08" stroke="#EF4444" stroke-width="1" stroke-opacity="0.25" filter="url(#shadow)"/>
<text class="err-title" x="522" y="219" text-anchor="middle">Field / Qualifier</text>
<text class="err-detail" x="522" y="237" text-anchor="middle">unknown, old./new. scope</text>
<rect x="589" y="200" width="110" height="52" rx="8" fill="#EF4444" fill-opacity="0.08" stroke="#EF4444" stroke-width="1" stroke-opacity="0.25" filter="url(#shadow)"/>
<text class="err-title" x="644" y="219" text-anchor="middle">Type / Operator</text>
<text class="err-detail" x="644" y="237" text-anchor="middle">mismatch, bad usage</text>
<!-- ===== Legend ===== -->
<rect x="200" y="275" width="12" height="12" rx="3" fill="#3B82F6" fill-opacity="0.12" stroke="#3B82F6" stroke-width="1" stroke-opacity="0.4"/>
<text class="legend-text" x="218" y="285">processing stage</text>
<rect x="350" y="275" width="12" height="12" rx="3" fill="#10B981" fill-opacity="0.12" stroke="#10B981" stroke-width="1" stroke-opacity="0.4"/>
<text class="legend-text" x="368" y="285">data</text>
<rect x="430" y="275" width="12" height="12" rx="3" fill="#EF4444" fill-opacity="0.1" stroke="#EF4444" stroke-width="1" stroke-opacity="0.35"/>
<text class="legend-text" x="448" y="285">error</text>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -18,7 +18,17 @@ This page explains how Ruki statements, triggers, conditions, and expressions be
- `select` without `where` means a statement with no condition node.
- `select where ...` validates the condition and its contained expressions.
- A subquery form `select` or `select where ...` can appear only inside `count(...)`.
- `select ... order by <field> [asc|desc], ...` specifies result ordering.
- A subquery form `select` or `select where ...` can appear only inside `count(...)`. Subqueries do not support `order by`.
`order by`
- Each field must exist in the schema and be an orderable type.
- Orderable types: `int`, `date`, `timestamp`, `duration`, `string`, `status`, `type`, `id`, `ref`.
- Non-orderable types: `list<string>`, `list<ref>`, `recurrence`, `bool`.
- Default direction is ascending. Use `desc` for descending.
- Duplicate fields are rejected.
- Only bare field names are allowed — `old.` and `new.` qualifiers are not valid in `order by`.
`create`
@ -80,6 +90,8 @@ before delete where old.priority <= 2 deny "cannot delete high priority tikis"
before update where old.status = "in progress" and new.status = "done" deny "tikis must go through review before completion"
```
![Qualifier scope by context](images/qualifier-scope.svg)
Important special case:
- inside a quantifier body such as `dependsOn any ...`, qualifiers are disabled again
@ -121,5 +133,7 @@ Binary `+` and `-` are semantic rather than purely numeric:
- `timestamp - duration` yields `timestamp`
- `timestamp - timestamp` yields `duration`
![Binary operator type resolution](images/binary-op-types.svg)
For the detailed type rules and built-ins, see [Types And Values](types-and-values.md) and [Operators And Built-ins](operators-and-builtins.md).

View file

@ -47,8 +47,11 @@ The following EBNF-style summary shows the grammar:
```text
statement = selectStmt | createStmt | updateStmt | deleteStmt ;
selectStmt = "select" [ "where" condition ] ;
selectStmt = "select" [ "where" condition ] [ orderBy ] ;
createStmt = "create" assignment { assignment } ;
orderBy = "order" "by" sortField { "," sortField } ;
sortField = identifier [ "asc" | "desc" ] ;
updateStmt = "update" "where" condition "set" assignment { assignment } ;
deleteStmt = "delete" "where" condition ;
@ -63,12 +66,18 @@ runAction = "run" "(" expr ")" ;
deny = "deny" string ;
```
![Statement grammar railroad diagram](images/stmt-railroad.svg)
![Trigger grammar railroad diagram](images/trigger-railroad.svg)
Notes:
- `select` is a valid top-level statement, but it is not valid as a trigger action.
- `create` requires at least one assignment.
- `update` requires both `where` and `set`.
- `delete` requires `where`.
- `order by` is only valid on `select`, not on subqueries inside `count(...)`.
- `asc`, `desc`, `order`, and `by` are contextual keywords — they are only special in the ORDER BY clause.
## Condition grammar
@ -99,6 +108,8 @@ anyTail = "any" primaryCond ;
allTail = "all" primaryCond ;
```
![Condition grammar railroad diagram](images/cond-railroad.svg)
Examples:
```sql
@ -109,6 +120,14 @@ select where dependsOn any status != "done"
select where not (status = "done" or priority = 1)
```
Order by:
```sql
select order by priority
select where status = "done" order by updatedAt desc
select where "bug" in tags order by priority asc, createdAt desc
```
## Expression grammar
Expressions support literals, field references, qualifiers, function calls, list literals, parenthesized expressions, subqueries, and left-associative `+` or `-` chains:
@ -136,6 +155,8 @@ emptyLiteral = "empty" ;
fieldRef = identifier ;
```
![Expression grammar railroad diagram](images/expr-railroad.svg)
Examples:
```sql

View file

@ -8,6 +8,7 @@
- [Field and qualifier errors](#field-and-qualifier-errors)
- [Type and operator errors](#type-and-operator-errors)
- [Enum and list errors](#enum-and-list-errors)
- [Order by errors](#order-by-errors)
- [Built-in and subquery errors](#built-in-and-subquery-errors)
## Overview
@ -16,6 +17,8 @@ This page explains the errors you can get in Ruki. It covers syntax errors, unkn
## Validation layers
![Validation pipeline](images/validation-pipeline.svg)
Ruki has two distinct failure stages:
1. Parse-time failures
@ -166,6 +169,34 @@ select where status in dependsOn
select where tags any status = "done"
```
## Order by errors
Unknown field:
```sql
select order by nonexistent
```
Non-orderable types:
```sql
select order by tags
select order by dependsOn
select order by recurrence
```
Duplicate field:
```sql
select order by priority, priority desc
```
Order by inside a subquery:
```sql
select where count(select where status = "done" order by priority) >= 1
```
## Built-in and subquery errors
Unknown function:

View file

@ -13,9 +13,10 @@ type Statement struct {
Delete *DeleteStmt
}
// SelectStmt represents "select [where <condition>]".
// SelectStmt represents "select [where <condition>] [order by <field> [asc|desc], ...]".
type SelectStmt struct {
Where Condition // nil = select all
Where Condition // nil = select all
OrderBy []OrderByClause // nil = unordered
}
// CreateStmt represents "create <field>=<value>...".
@ -181,6 +182,14 @@ func (*FunctionCall) exprNode() {}
func (*BinaryExpr) exprNode() {}
func (*SubQuery) exprNode() {}
// --- order by ---
// OrderByClause represents a single sort criterion in "order by <field> [asc|desc]".
type OrderByClause struct {
Field string // field name
Desc bool // true = descending, false = ascending (default)
}
// --- assignments ---
// Assignment represents "field=value" in create/update statements.

View file

@ -14,7 +14,20 @@ type statementGrammar struct {
}
type selectGrammar struct {
Where *orCond `parser:"'select' ( 'where' @@ )?"`
Where *orCond `parser:"'select' ( 'where' @@ )?"`
OrderBy *orderByGrammar `parser:"@@?"`
}
// --- order by grammar ---
type orderByGrammar struct {
First orderByField `parser:"'order' 'by' @@"`
Rest []orderByField `parser:"( ',' @@ )*"`
}
type orderByField struct {
Field string `parser:"@Ident"`
Direction *string `parser:"@( 'asc' | 'desc' )?"`
}
type createGrammar struct {
@ -156,7 +169,8 @@ type funcCallExpr struct {
}
type subQueryExpr struct {
Where *orCond `parser:"'select' ( 'where' @@ )?"`
Where *orCond `parser:"'select' ( 'where' @@ )?"`
OrderBy *orderByGrammar `parser:"@@?"`
}
type qualRefExpr struct {

View file

@ -49,7 +49,8 @@ func lowerSelect(g *selectGrammar) (*SelectStmt, error) {
return nil, err
}
}
return &SelectStmt{Where: where}, nil
orderBy := lowerOrderBy(g.OrderBy)
return &SelectStmt{Where: where, OrderBy: orderBy}, nil
}
func lowerCreate(g *createGrammar) (*CreateStmt, error) {
@ -317,6 +318,9 @@ func lowerFuncCall(g *funcCallExpr) (Expr, error) {
}
func lowerSubQuery(g *subQueryExpr) (Expr, error) {
if g.OrderBy != nil {
return nil, fmt.Errorf("order by is not valid inside a subquery")
}
var where Condition
if g.Where != nil {
var err error
@ -340,6 +344,25 @@ func lowerListLit(g *listLitExpr) (Expr, error) {
return &ListLiteral{Elements: elems}, nil
}
// --- order by lowering ---
func lowerOrderBy(g *orderByGrammar) []OrderByClause {
if g == nil {
return nil
}
clauses := make([]OrderByClause, 0, 1+len(g.Rest))
clauses = append(clauses, lowerOrderByField(&g.First))
for i := range g.Rest {
clauses = append(clauses, lowerOrderByField(&g.Rest[i]))
}
return clauses
}
func lowerOrderByField(g *orderByField) OrderByClause {
desc := g.Direction != nil && *g.Direction == "desc"
return OrderByClause{Field: g.Field, Desc: desc}
}
// --- literal helpers ---
func unquoteString(s string) string {

View file

@ -618,6 +618,109 @@ func TestParseStatementErrors(t *testing.T) {
}
}
func TestParseSelectOrderBy(t *testing.T) {
p := newTestParser()
tests := []struct {
name string
input string
wantWhere bool
wantOrderBy []OrderByClause
}{
{
"order by single field",
"select order by priority",
false,
[]OrderByClause{{Field: "priority", Desc: false}},
},
{
"order by desc",
"select order by priority desc",
false,
[]OrderByClause{{Field: "priority", Desc: true}},
},
{
"order by asc",
"select order by priority asc",
false,
[]OrderByClause{{Field: "priority", Desc: false}},
},
{
"order by multiple fields",
"select order by priority desc, createdAt asc",
false,
[]OrderByClause{
{Field: "priority", Desc: true},
{Field: "createdAt", Desc: false},
},
},
{
"order by mixed directions",
"select order by status, priority desc, title",
false,
[]OrderByClause{
{Field: "status", Desc: false},
{Field: "priority", Desc: true},
{Field: "title", Desc: false},
},
},
{
"where and order by",
`select where status = "done" order by updatedAt desc`,
true,
[]OrderByClause{{Field: "updatedAt", Desc: true}},
},
{
"where and order by multiple",
`select where "bug" in tags order by priority asc, createdAt desc`,
true,
[]OrderByClause{
{Field: "priority", Desc: false},
{Field: "createdAt", Desc: true},
},
},
{
"select without order by still works",
`select where status = "done"`,
true,
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, err := p.ParseStatement(tt.input)
if err != nil {
t.Fatalf("parse error: %v", err)
}
if stmt.Select == nil {
t.Fatal("expected Select")
}
if tt.wantWhere && stmt.Select.Where == nil {
t.Fatal("expected Where condition")
}
if !tt.wantWhere && stmt.Select.Where != nil {
t.Fatal("unexpected Where condition")
}
if len(tt.wantOrderBy) == 0 && len(stmt.Select.OrderBy) != 0 {
t.Fatalf("expected no OrderBy, got %v", stmt.Select.OrderBy)
}
if len(tt.wantOrderBy) != len(stmt.Select.OrderBy) {
t.Fatalf("expected %d OrderBy clauses, got %d", len(tt.wantOrderBy), len(stmt.Select.OrderBy))
}
for i, want := range tt.wantOrderBy {
got := stmt.Select.OrderBy[i]
if got.Field != want.Field {
t.Errorf("OrderBy[%d].Field = %q, want %q", i, got.Field, want.Field)
}
if got.Desc != want.Desc {
t.Errorf("OrderBy[%d].Desc = %v, want %v", i, got.Desc, want.Desc)
}
}
})
}
}
func TestParseComment(t *testing.T) {
p := newTestParser()

View file

@ -60,9 +60,11 @@ func (p *Parser) validateStatement(s *Statement) error {
return p.validateCondition(s.Delete.Where)
case s.Select != nil:
if s.Select.Where != nil {
return p.validateCondition(s.Select.Where)
if err := p.validateCondition(s.Select.Where); err != nil {
return err
}
}
return nil
return p.validateOrderBy(s.Select.OrderBy)
default:
return fmt.Errorf("empty statement")
}
@ -136,6 +138,39 @@ func (p *Parser) validateAssignments(assignments []Assignment) error {
return nil
}
// --- order by validation ---
func (p *Parser) validateOrderBy(clauses []OrderByClause) error {
if len(clauses) == 0 {
return nil
}
seen := make(map[string]struct{}, len(clauses))
for _, c := range clauses {
if _, dup := seen[c.Field]; dup {
return fmt.Errorf("duplicate field %q in order by", c.Field)
}
seen[c.Field] = struct{}{}
fs, ok := p.schema.Field(c.Field)
if !ok {
return fmt.Errorf("unknown field %q in order by", c.Field)
}
if !isOrderableType(fs.Type) {
return fmt.Errorf("cannot order by %s field %q", typeName(fs.Type), c.Field)
}
}
return nil
}
func isOrderableType(t ValueType) bool {
switch t {
case ValueInt, ValueDate, ValueTimestamp, ValueDuration,
ValueString, ValueStatus, ValueTaskType, ValueID, ValueRef:
return true
default:
return false
}
}
// --- condition validation with type-checking ---
func (p *Parser) validateCondition(c Condition) error {

View file

@ -1527,6 +1527,92 @@ func TestValidation_EnumInRejectsFieldRefs(t *testing.T) {
}
}
func TestValidation_OrderBy(t *testing.T) {
p := newTestParser()
t.Run("valid cases", func(t *testing.T) {
valid := []struct {
name string
input string
}{
{"int field", "select order by priority"},
{"date field", "select order by due"},
{"timestamp field", "select order by createdAt desc"},
{"string field", "select order by title asc"},
{"status field", "select order by status"},
{"type field", "select order by type desc"},
{"id field", "select order by id"},
{"multiple fields", "select order by priority desc, createdAt"},
{"with where", `select where status = "done" order by priority`},
}
for _, tt := range valid {
t.Run(tt.name, func(t *testing.T) {
_, err := p.ParseStatement(tt.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
})
t.Run("invalid cases", func(t *testing.T) {
invalid := []struct {
name string
input string
wantErr string
}{
{
"unknown field",
"select order by nonexistent",
"unknown field",
},
{
"list<string> not orderable",
"select order by tags",
"cannot order by",
},
{
"list<ref> not orderable",
"select order by dependsOn",
"cannot order by",
},
{
"recurrence not orderable",
"select order by recurrence",
"cannot order by",
},
{
"duplicate field",
"select order by priority, priority desc",
"duplicate field",
},
}
for _, tt := range invalid {
t.Run(tt.name, func(t *testing.T) {
_, err := p.ParseStatement(tt.input)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got: %v", tt.wantErr, err)
}
})
}
})
}
func TestValidation_OrderByInSubquery(t *testing.T) {
p := newTestParser()
_, err := p.ParseStatement(`select where count(select where status = "done" order by priority) > 0`)
if err == nil {
t.Fatal("expected error for order by inside subquery")
}
if !strings.Contains(err.Error(), "order by is not valid inside a subquery") {
t.Fatalf("expected subquery error, got: %v", err)
}
}
func TestValidation_ListAssignmentRejectsFieldRefs(t *testing.T) {
p := newTestParser()