@graphenedata/cli 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE.md +3 -3
  2. package/README.md +138 -0
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/bin.js +2 -2
  5. package/dist/cli/bigQuery-I3F46SC6.js +75 -0
  6. package/dist/cli/bigQuery-I3F46SC6.js.map +7 -0
  7. package/dist/cli/chunk-OVWODUTJ.js +12849 -0
  8. package/dist/cli/chunk-OVWODUTJ.js.map +7 -0
  9. package/dist/cli/chunk-QAXEOZ43.js +53 -0
  10. package/dist/cli/chunk-QAXEOZ43.js.map +7 -0
  11. package/dist/cli/cli.js +245 -10290
  12. package/dist/cli/clickhouse-ZN5AN2UL.js +64 -0
  13. package/dist/cli/clickhouse-ZN5AN2UL.js.map +7 -0
  14. package/dist/cli/duckdb-IYBIO5KJ.js +87 -0
  15. package/dist/cli/duckdb-IYBIO5KJ.js.map +7 -0
  16. package/dist/cli/serve2-TNN5EROW.js +447 -0
  17. package/dist/cli/serve2-TNN5EROW.js.map +7 -0
  18. package/dist/cli/snowflake-MOQB5GA4.js +128 -0
  19. package/dist/cli/snowflake-MOQB5GA4.js.map +7 -0
  20. package/dist/index.d.ts +63 -0
  21. package/dist/lang/index.d.ts +63 -0
  22. package/dist/skills/graphene/SKILL.md +235 -0
  23. package/dist/skills/graphene/references/big-value.md +20 -0
  24. package/dist/skills/graphene/references/date-range.md +64 -0
  25. package/dist/skills/graphene/references/dropdown.md +62 -0
  26. package/dist/skills/graphene/references/echarts.md +162 -0
  27. package/dist/skills/graphene/references/gsql.md +393 -0
  28. package/dist/skills/graphene/references/model-gsql.md +72 -0
  29. package/dist/skills/graphene/references/table.md +143 -0
  30. package/dist/skills/graphene/references/text-input.md +29 -0
  31. package/dist/ui/app.css +263 -299
  32. package/dist/ui/component-utilities/dataShaping.ts +484 -0
  33. package/dist/ui/component-utilities/dataSummary.ts +57 -0
  34. package/dist/ui/component-utilities/enrich.ts +763 -0
  35. package/dist/ui/component-utilities/format.ts +177 -0
  36. package/dist/ui/component-utilities/inputUtils.ts +48 -9
  37. package/dist/ui/component-utilities/theme.ts +200 -0
  38. package/dist/ui/component-utilities/themeStores.ts +26 -21
  39. package/dist/ui/component-utilities/types.ts +70 -0
  40. package/dist/ui/components/AreaChart.svelte +57 -105
  41. package/dist/ui/components/BarChart.svelte +71 -129
  42. package/dist/ui/components/BigValue.svelte +24 -40
  43. package/dist/ui/components/Column.svelte +11 -19
  44. package/dist/ui/components/DateRange.svelte +71 -34
  45. package/dist/ui/components/Dropdown.svelte +82 -49
  46. package/dist/ui/components/DropdownOption.svelte +1 -2
  47. package/dist/ui/components/ECharts.svelte +179 -60
  48. package/dist/ui/components/InlineDelta.svelte +51 -32
  49. package/dist/ui/components/LineChart.svelte +54 -125
  50. package/dist/ui/components/PieChart.svelte +27 -37
  51. package/dist/ui/components/QueryLoad.svelte +78 -44
  52. package/dist/ui/components/Row.svelte +2 -1
  53. package/dist/ui/components/ScatterPlot.svelte +52 -0
  54. package/dist/ui/components/Skeleton.svelte +32 -0
  55. package/dist/ui/components/Table.svelte +3 -2
  56. package/dist/ui/components/TableGroupRow.svelte +28 -36
  57. package/dist/ui/components/TableHarness.svelte +32 -0
  58. package/dist/ui/components/TableHeader.svelte +34 -59
  59. package/dist/ui/components/TableRow.svelte +15 -39
  60. package/dist/ui/components/TableSubtotalRow.svelte +26 -21
  61. package/dist/ui/components/TableTotalRow.svelte +27 -37
  62. package/dist/ui/components/TextInput.svelte +17 -14
  63. package/dist/ui/components/Value.svelte +25 -0
  64. package/dist/ui/components/_Table.svelte +80 -76
  65. package/dist/ui/internal/ChartGallery.svelte +527 -0
  66. package/dist/ui/internal/ErrorDisplay.svelte +60 -0
  67. package/dist/ui/internal/LocalApp.svelte +87 -19
  68. package/dist/ui/internal/PageNavGroup.svelte +269 -0
  69. package/dist/ui/internal/Sidebar.svelte +178 -0
  70. package/dist/ui/internal/SidebarToggle.svelte +47 -0
  71. package/dist/ui/internal/StyleGallery.svelte +244 -0
  72. package/dist/ui/internal/clientCache.ts +15 -13
  73. package/dist/ui/internal/pageInputs.svelte.js +292 -0
  74. package/dist/ui/internal/queryEngine.ts +124 -132
  75. package/dist/ui/internal/runSocket.ts +59 -0
  76. package/dist/ui/internal/sidebar.svelte.js +18 -0
  77. package/dist/ui/internal/telemetry.ts +52 -17
  78. package/dist/ui/internal/types.d.ts +7 -0
  79. package/dist/ui/web.js +55 -13
  80. package/package.json +40 -41
  81. package/dist/docs/agent-instructions.md +0 -18
  82. package/dist/docs/base.md +0 -98
  83. package/dist/docs/cli.md +0 -22
  84. package/dist/docs/graphene.md +0 -1462
  85. package/dist/ui/component-utilities/autoFormatting.js +0 -301
  86. package/dist/ui/component-utilities/builtInFormats.js +0 -482
  87. package/dist/ui/component-utilities/chartContext.js +0 -12
  88. package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
  89. package/dist/ui/component-utilities/checkInputs.js +0 -95
  90. package/dist/ui/component-utilities/convert.js +0 -15
  91. package/dist/ui/component-utilities/dateParsing.js +0 -57
  92. package/dist/ui/component-utilities/dropdownContext.ts +0 -1
  93. package/dist/ui/component-utilities/echarts.js +0 -272
  94. package/dist/ui/component-utilities/echartsThemes.js +0 -453
  95. package/dist/ui/component-utilities/formatTitle.js +0 -24
  96. package/dist/ui/component-utilities/formatting.js +0 -250
  97. package/dist/ui/component-utilities/getColumnExtents.js +0 -79
  98. package/dist/ui/component-utilities/getColumnSummary.js +0 -67
  99. package/dist/ui/component-utilities/getCompletedData.js +0 -114
  100. package/dist/ui/component-utilities/getDistinctCount.js +0 -7
  101. package/dist/ui/component-utilities/getDistinctValues.js +0 -15
  102. package/dist/ui/component-utilities/getSeriesConfig.js +0 -237
  103. package/dist/ui/component-utilities/getSortedData.js +0 -7
  104. package/dist/ui/component-utilities/getStackPercentages.js +0 -43
  105. package/dist/ui/component-utilities/getStackedData.js +0 -17
  106. package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
  107. package/dist/ui/component-utilities/globalContexts.js +0 -1
  108. package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
  109. package/dist/ui/component-utilities/replaceNulls.js +0 -14
  110. package/dist/ui/component-utilities/tableUtils.ts +0 -120
  111. package/dist/ui/components/Area.svelte +0 -214
  112. package/dist/ui/components/Bar.svelte +0 -350
  113. package/dist/ui/components/Chart.svelte +0 -989
  114. package/dist/ui/components/ErrorChart.svelte +0 -118
  115. package/dist/ui/components/Line.svelte +0 -227
  116. package/dist/ui/internal/NavSidebar.svelte +0 -396
  117. package/dist/ui/internal/PageError.svelte +0 -23
  118. package/dist/ui/internal/checkSocket.ts +0 -48
  119. package/dist/ui/internal/theme.ts +0 -88
  120. package/dist/ui/public/inter-latin-ext.woff2 +0 -0
  121. package/dist/ui/public/inter-latin.woff2 +0 -0
@@ -0,0 +1,64 @@
1
+ Creates a date range picker with start/end date inputs and optional preset ranges. The selected range can be used to filter queries or in markdown.
2
+
3
+ Here's an example:
4
+
5
+ ````markdown
6
+ ```sql sales
7
+ select date, revenue from orders
8
+ ```
9
+
10
+ <DateRange
11
+ title="Date Range"
12
+ name=date_filter
13
+ data=sales
14
+ dates=date
15
+ />
16
+ ````
17
+
18
+ The selected start and end dates are then referenced in GSQL as `$date_filter_start` and `$date_filter_end`. For example:
19
+
20
+ ```sql
21
+ select date, revenue
22
+ from orders
23
+ where date >= $date_filter_start and date < $date_filter_end
24
+ ```
25
+
26
+ Date range selections sync into the page URL query string as `{name}_start` and `{name}_end`, eg. `localhost:4000/my_dashboard?date_filter_start=2024-01-01&date_filter_end=2024-02-01`.
27
+
28
+ # Attributes
29
+
30
+ | Attribute | Description | Required | Options | Default |
31
+ |------|-------------|----------|---------|---------|
32
+ | name | Name of the date range, used to reference the selected value elsewhere as `"$name.start"` and `"$name.end"` | true | - | - |
33
+ | data | GSQL query or table name to infer the date domain from | false | query name | - |
34
+ | dates | Column name from the query containing date values, used to determine the min/max of the domain | false | column name | - |
35
+ | start | Initial start date | false | date string or Date | - |
36
+ | end | Initial end date | false | date string or Date | - |
37
+ | defaultValue | Preset label to apply on first load (e.g., `"Last 30 Days"`) | false | preset label | - |
38
+ | presetRanges | List of preset range labels to show in the dropdown. Accepts a comma-separated string or an array. | false | preset labels | See defaults below |
39
+ | title | Title to display above the input | false | string | - |
40
+ | description | Subtitle text displayed below the title | false | string | - |
41
+
42
+ # Preset ranges
43
+
44
+ By default, the following presets are available in the dropdown:
45
+
46
+ - Last 7 Days
47
+ - Last 30 Days
48
+ - Last 90 Days
49
+ - Last 365 Days
50
+ - Last Month
51
+ - Last Year
52
+ - Month to Date
53
+ - Month to Today
54
+ - Year to Date
55
+ - Year to Today
56
+ - All Time
57
+
58
+ The `Last N Days` and `Last N Months` patterns are also supported for any number N (e.g., `"Last 14 Days"`, `"Last 6 Months"`).
59
+
60
+ You can override the list with the `presetRanges` prop:
61
+
62
+ ```markdown
63
+ <DateRange name="date_filter" presetRanges="Last 7 Days, Last 30 Days, Last Year" />
64
+ ```
@@ -0,0 +1,62 @@
1
+ Creates a dropdown menu with a list of options that can be selected. The selected option can be used to filter queries or in markdown.
2
+
3
+ Here's an example:
4
+
5
+ ````markdown
6
+ ```sql statuses
7
+ select distinct status from orders
8
+ ```
9
+
10
+ <Dropdown
11
+ title="Select Order Status"
12
+ name="status_dropdown"
13
+ data="statuses"
14
+ value="status"
15
+ defaultValue="Complete"
16
+ />
17
+ ````
18
+
19
+ The user-selected value would then be referenced in GSQL as `$status_dropdown`. For example:
20
+
21
+ ```sql
22
+ select *
23
+ from orders
24
+ where status = $status_dropdown
25
+ ```
26
+
27
+ Dropdown selections also sync into the page URL query string. Single-select dropdowns use one key, and multi-select dropdowns repeat the key for each selected value.
28
+
29
+ # Attributes
30
+
31
+ | Attribute | Description | Required | Options | Default |
32
+ |------|-------------|----------|---------|---------|
33
+ | name | Name of the dropdown, used to reference the selected value elsewhere as `"$name"` | true | - | - |
34
+ | data | GSQL query or table name | false | query name | - |
35
+ | value | Column name from the query containing values to pick from | false | column name | - |
36
+ | multiple | Enables multi-select which returns a list and syncs repeated values into the URL query string | false | `true`, `false` | `false` |
37
+ | defaultValue | Value to use when the dropdown is first loaded. Must be one of the options in the dropdown. Lists supported for multi-select. | false | value from dropdown, list of values e.g. `"Value 1, Value 2"` | - |
38
+ | selectAllByDefault | Selects and returns all values, multiple attribute required | false | `true`, `false` | `false` |
39
+ | noDefault | Stops any default from being selected. Overrides any set `defaultValue`. | false | boolean | `false` |
40
+ | disableSelectAll | Removes the `"Select all"` button. Recommended for large datasets. | false | boolean | `false` |
41
+ | label | Column name from the query containing labels to display instead of the values (e.g., you may want to have the drop-down use `customer_id` as the value, but show `customer_name` to your users) | false | column name | Uses the column in value |
42
+ | title | Title to display above the dropdown | false | string | - |
43
+ | description | Adds an info icon with description tooltip on hover | false | string | - |
44
+
45
+ # DropdownOption sub-component
46
+
47
+ The `DropdownOption` sub-component can be used to manually add options to a dropdown. This is useful to add a default option, or to add options that are not in a query.
48
+
49
+ Here's an example:
50
+
51
+ ```markdown
52
+ <Dropdown name=hardcoded>
53
+ <DropdownOption valueLabel="Option One" value=1 />
54
+ <DropdownOption valueLabel="Option Two" value=2 />
55
+ <DropdownOption valueLabel="Option Three" value=3 />
56
+ </Dropdown>
57
+ ```
58
+
59
+ | Attribute | Description | Required | Options | Default |
60
+ |------|-------------|----------|---------|---------|
61
+ | value | Value to use when the option is selected | true | - | - |
62
+ | valueLabel | Label to display for the option in the dropdown | false | - | Uses the value |
@@ -0,0 +1,162 @@
1
+ Use `ECharts` when you need chart behavior that goes beyond the built-in chart components.
2
+
3
+ Graphene charts are powered by Apache ECharts (v6). In markdown, you define the ECharts option object **inside the `<ECharts>` tag body**.
4
+
5
+ Example:
6
+
7
+ ```markdown
8
+ <ECharts data="sales_by_month">
9
+ title: {text: "Revenue"},
10
+ tooltip: {trigger: "axis"},
11
+ series: [{type: "line", encode: {x: "month", y: "revenue"}}],
12
+ </ECharts>
13
+ ```
14
+
15
+ # Attributes
16
+
17
+ | Attribute | Description | Required | Options | Default |
18
+ |----------|-------------|----------|---------|---------|
19
+ | data | GSQL query or table name | true | query/table name | - |
20
+ | height | Chart height in px or CSS size string | false | number, string | `240px` |
21
+ | width | Chart width in px or CSS size string | false | number, string | `100%` |
22
+ | renderer | ECharts renderer | false | `svg`, `canvas` | `svg` |
23
+
24
+ ## Config body syntax
25
+
26
+ Inside `<ECharts>...</ECharts>`, Graphene parses the config as JSON5:
27
+ - Unquoted keys are allowed (`xAxis: {}`)
28
+ - Trailing commas are allowed
29
+ - You can wrap the whole thing in `{ ... }` or omit the outer braces
30
+ - Inline Javascript is not allowed
31
+
32
+ ## What Graphene handles automatically
33
+
34
+ You don't need to configure these — Graphene applies them by default:
35
+
36
+ - Axes: created if missing, types inferred from field metadata (time, category, value), tick formatting applied
37
+ - Layout: grid padding computed to prevent title/legend overlap
38
+ - Style: color palette, fonts, axis borders, split lines, and series marker defaults (via the Graphene theme)
39
+
40
+ Your config typically only needs to specify the series `type`, `encode` mappings, and any explicit overrides to the above.
41
+
42
+ ## Encode fields by series type
43
+
44
+ Each series type maps columns via `encode`. Graphene accepts:
45
+
46
+ | Series type | Encode fields |
47
+ |-------------|---------------|
48
+ | `bar`, `line`, `scatter`, `candlestick`, `heatmap`, `effectScatter` | `x`, `y`, `splitBy` |
49
+ | `pie`, `funnel` | `itemName`, `value` |
50
+ | `treemap` | `itemName`, `value` |
51
+ | `sankey`, `chord` | `source`, `target`, `value` |
52
+ | `themeRiver` | `single`, `value`, `seriesName` |
53
+
54
+ For a beeswarm, use a `scatter` series and set `jitter` (plus optional `jitterOverlap`/`jitterMargin`) on the categorical axis.
55
+
56
+ ## Customizing with split hints
57
+
58
+ To keep configs concise, Graphene supports a split hint:
59
+
60
+ - `encode.splitBy: "field"`: split one series template into one series per distinct field value
61
+ - `encode.splitBy: ["groupField", "stackField"]` (bar only): expands to grouped+stacked bars, where the first field groups and the second stacks
62
+ - with a single split field, `series.stack` decides stacked vs grouped behavior
63
+ - `stackPercentage: true`: convert stacked values to percentages (100% stacked)
64
+
65
+ Examples:
66
+
67
+ ```markdown
68
+ <ECharts data="sales_by_month_and_region">
69
+ title: {text: "Revenue by Region"},
70
+ series: [{
71
+ type: 'bar',
72
+ encode: {x: 'month', y: 'revenue', splitBy: 'region'},
73
+ stack: 'revenue-stack',
74
+ stackPercentage: true
75
+ }]
76
+ </ECharts>
77
+
78
+ <!-- Char that is both grouped by region and stacked by channel -->
79
+ <ECharts data="sales_by_month_region_channel">
80
+ title: {text: "Revenue by Region and Channel"},
81
+ series: [{
82
+ type: 'bar',
83
+ encode: {x: 'month', y: 'revenue', splitBy: ['region', 'channel']},
84
+ stack: 'revenue-stack',
85
+ stackPercentage: true
86
+ }]
87
+ />
88
+ ```
89
+
90
+ ## More examples
91
+
92
+ ### Heatmap
93
+
94
+ ```markdown
95
+ <ECharts data="delay_by_hour_and_day" height=520px>
96
+ title: {text: "Avg Delay by Hour & Day of Week (min)"},
97
+ tooltip: {trigger: 'item'},
98
+ visualMap: {
99
+ min: -5, max: 30,
100
+ calculable: true,
101
+ orient: 'horizontal',
102
+ left: 'center',
103
+ bottom: 4,
104
+ inRange: {color: ['#5B8F9E', '#e4eff3', '#D4A94C']},
105
+ },
106
+ xAxis: {type: 'category', position: 'top', data: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']},
107
+ yAxis: {type: 'category', inverse: true, data: ['6am','7am','8am','9am','10am','11am','12pm']},
108
+ series: [{type: 'heatmap', encode: {x: 'day_label', y: 'hour_label', value: 'avg_delay'}}],
109
+ </ECharts>
110
+ ```
111
+
112
+ ### Scatter/bubble
113
+
114
+ ```markdown
115
+ <ECharts data="airport_stats" height=480px>
116
+ title: {text: "Departure vs Arrival Delay by Airport"},
117
+ tooltip: {trigger: 'item'},
118
+ visualMap: {
119
+ dimension: 'flight_count',
120
+ type: 'continuous',
121
+ min: 0, max: 3000,
122
+ inRange: {symbolSize: [4, 32]},
123
+ show: false,
124
+ },
125
+ xAxis: {type: 'value', name: 'Avg Departure Delay (min)', nameLocation: 'middle', nameGap: 22},
126
+ yAxis: {type: 'value', name: 'Avg Arrival Delay (min)', nameLocation: 'middle', nameGap: 20},
127
+ series: [{
128
+ type: 'scatter',
129
+ encode: {x: 'avg_dep_delay', y: 'avg_arr_delay', itemName: 'code'},
130
+ tooltip: {formatter: '{b}'},
131
+ }],
132
+ </ECharts>
133
+ ```
134
+
135
+ ### Lollipop
136
+
137
+ Combine a `pictorialBar` series (the stem) with a `scatter` series (the dot) on a category y-axis.
138
+
139
+ ```markdown
140
+ <ECharts data="avg_delay_by_carrier">
141
+ title: {text: "Avg Delay by Carrier (min)"},
142
+ xAxis: {type: 'value'},
143
+ yAxis: {type: 'category', inverse: true, encode: {y: 'carrier'}},
144
+ series: [
145
+ {
146
+ type: 'pictorialBar',
147
+ symbol: 'rect',
148
+ symbolSize: ['100%', 2],
149
+ symbolPosition: 'end',
150
+ encode: {x: 'avg_delay', y: 'carrier'},
151
+ },
152
+ {
153
+ type: 'scatter',
154
+ symbolSize: 12,
155
+ encode: {x: 'avg_delay', y: 'carrier', itemName: 'carrier'},
156
+ label: {show: true, position: 'right', formatter: '{b}', fontSize: 11},
157
+ },
158
+ ],
159
+ </ECharts>
160
+ ```
161
+
162
+ For common chart types, prefer `BarChart`, `LineChart`, `AreaChart`, `ScatterPlot`, and `PieChart`. Use `ECharts` when you need deeper customization.
@@ -0,0 +1,393 @@
1
+ # Graphene SQL (GSQL)
2
+
3
+ GSQL abstracts over the underlying database SQL. Graphene translates GSQL into database SQL when running queries.
4
+
5
+ GSQL is comprised of four primary statements: `table`, `select`, `table X as`, and `extend`.
6
+
7
+ ## `table` statements
8
+
9
+ `table` statements declare tables that already exist in your database. Here's an example of two tables, `orders` and `users`, in GSQL.
10
+
11
+ ```sql
12
+ table orders (
13
+
14
+ -- Base columns
15
+
16
+ id BIGINT
17
+ user_id BIGINT
18
+ created_at DATETIME
19
+ status STRING -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
20
+ amount FLOAT -- Amount paid by customer #units=usd
21
+ cost FLOAT -- Cost of materials #units=usd
22
+
23
+ -- Join relationships
24
+
25
+ join one users on user_id = users.id
26
+
27
+ -- Dimensions
28
+
29
+ revenue_recognized: status in ('Processing', 'Shipped', 'Complete')
30
+
31
+ -- Measures
32
+
33
+ revenue: sum(case when revenue_recognized then amount else 0 end) #units=usd
34
+ cogs: sum(case when revenue_recognized then cost else 0 end) #units=usd
35
+ profit: revenue - cogs #units=usd
36
+ profit_margin: profit / revenue #ratio
37
+ )
38
+
39
+ table users (
40
+ id BIGINT
41
+ name VARCHAR
42
+ email VARCHAR
43
+ age INTEGER
44
+ country_code VARCHAR
45
+
46
+ join many orders on id = orders.user_id
47
+ )
48
+ ```
49
+
50
+ We can break down a `table` statement into three parts: base columns, join relationships, and stored expressions (dimensions and measures).
51
+
52
+ ### Base columns (required)
53
+
54
+ The base column set is simply a reflection of the underlying database table's schema. Similar to `create table` statements in regular SQL DDL, you list each column's name and data type.
55
+
56
+ For array columns, GSQL uses `array<T>` as the standard syntax:
57
+
58
+ ```sql
59
+ table events (
60
+ id BIGINT
61
+ tags array<string>
62
+ scores array<int64>
63
+ )
64
+ ```
65
+
66
+ ### Join relationships
67
+
68
+ Join relationships in a `table` statement declare joins that can be used when querying them. This makes query writing easier and more foolproof.
69
+
70
+ The other main difference about joins in GSQL vs. regular SQL is that you have to explain if there are many rows in the left table for each row in the right table, or vice versa. This additional bit of information allows Graphene to prevent incorrect aggregation as a result of row duplication (aka fan-out) through joins.
71
+
72
+ This information is provided with the two supported join types, `join one` and `join many`:
73
+ - `join one` is used when each row in **this** table maps to at most one row in the **joined** table.
74
+ - `join many` is used when each row in **this** table can map to many rows in the **joined** table.
75
+
76
+ In the example above with `orders` and `users`, the joins confirm that there are many orders per user, and only one user per order.
77
+
78
+ Note that all joins in GSQL are left outer joins. There is no inner, right, or cross join.
79
+
80
+ #### Multiple join relationships between the same two tables
81
+
82
+ Sometimes there are multiple valid ways to join two tables together. You can model this in Graphene by aliasing the various joins with `as`, just as you would in normal SQL. For example:
83
+
84
+ ```sql
85
+ table projects (
86
+ ...
87
+ owner_id BIGINT
88
+ viewer_id BIGINT
89
+
90
+ join one users as project_owner on owner_id = project_owner.id
91
+ join one users as project_viewer on viewer_id = project_viewer.id
92
+ )
93
+
94
+ table users (
95
+ ...
96
+ id BIGINT
97
+
98
+ join many projects as projects_as_owner on id = projects_as_owner.owner_id
99
+ join many projects as projects_as_viewer on id = projects_as_viewer.viewer_id
100
+ )
101
+ ```
102
+
103
+ #### Best practices for modeling join relationships
104
+
105
+ - For a given `table` statement, only model joins that are directly on that table. Multi-hop join paths do not need to be written explicitly in order for queries to traverse them.
106
+ - A join between two tables should be modeled in both the respective `table` statements. This may seem redundant but it offers more flexibility for queries to choose which table to set in the `from` (remember that direction matters in queries since all joins are left joins).
107
+
108
+ ### Stored expressions
109
+
110
+ **Stored expressions** are GSQL expressions (ie. any arbitrary combination of functions, operators, and column references) that you want to make reusable to queries. Stored expressions are great for canonizing metrics, segments, and other important business definitions.
111
+
112
+ A stored expression must be given a name via `name: expression` or `expression as name`. It can then be referenced by name in queries that use the table.
113
+
114
+ Like expressions in regular SQL, expressions in GSQL are either scalar or aggregative. In BI parlance, these would be called dimensions and measures, respectively.
115
+
116
+ Expressions can refer to other expressions, as from the example before:
117
+
118
+ ```sql
119
+ table orders (
120
+ ...
121
+
122
+ /* Scalar expressions */
123
+ revenue_recognized: status in ('Processing', 'Shipped', 'Complete')
124
+
125
+ /* Agg expressions */
126
+ revenue: sum(case when revenue_recognized then amount else 0 end) #units=usd
127
+ cogs: sum(case when revenue_recognized then cost else 0 end) #units=usd
128
+ profit: revenue - cogs #units=usd -- even though there are no agg functions here, this is still aggregative as it references other aggregative expressions
129
+ profit_margin: profit / revenue #ratio
130
+ )
131
+ ```
132
+
133
+ ### Metadata annotations
134
+
135
+ Certain GSQL expressions will create field-level metadata that Graphene uses for better formatting and visualizations defaults. For example, `date_trunc('month', created_at)` will attach `timeGrain=month` metadata to the resulting column, which informs Graphene that the values represent months and not just days that happen to land on the first of every month. Graphene leverages this information to make sure that value formatting and chart axes look good by default.
136
+
137
+ There isn't always a SQL expression that can tip Graphene to the semantic meaning of a field, however:
138
+ - The field could be a base column that has no source expression
139
+ - There might not be enough information in the expression (eg. what currency a float is tied to)
140
+
141
+ For this reason, some metadata should be set explicitly in the GSQL model, using annotations. Metadata annotations resemble hashtags (eg. `#ratio`, `#units=usd`) that can be inlined or written above the object they decorate.
142
+
143
+ #### Recognized metadata
144
+
145
+ | Key | Inferrable? | Effect |
146
+ |---|---|---|
147
+ | `#ratio` | no | Value is 0–1; rendered as `value × 100%` (e.g. `0.42` → `42%`) |
148
+ | `#pct` | no | Value is already 0–100; rendered as `value%` (e.g. `42` → `42%`) |
149
+ | `#units=<currency>` | no | Adds currency symbol and compacts to K/M/B. Accepted values: `usd`, `eur`, `gbp`, `cad`, `aud`, `jpy` |
150
+ | `#timeGrain=<grain>` | yes (from `date_trunc`, `date_bin`, casts) | Controls time axis label format. Values: `year`, `quarter`, `month`, `week`, `day`, `hour`, `minute`, `second` |
151
+ | `#timeOrdinal=<ordinal>` | yes (from `extract`) | Treats values as ordinal positions on a categorical axis. Values: `hour_of_day`, `day_of_month`, `day_of_year`, `week_of_year`, `month_of_year`, `quarter_of_year`, `dow_0s` (0=Sun), `dow_1s` (1=Sun), `dow_1m` (1=Mon) |
152
+
153
+ ## `select` statements
154
+
155
+ `select` in GSQL has been extended to leverage join relationships, dimensions, and measures.
156
+
157
+ ### Using join relationships
158
+
159
+ If a `table` has join relationships declared in it, a `select` query on that table can leverage that join without needing to write its own join statement. This is helpful for query writers who have not memorized all the correct join keys.
160
+
161
+ If you recall the model from before:
162
+
163
+ ```sql
164
+ table orders (
165
+ ...
166
+ user_id BIGINT
167
+ join one users on user_id = users.id
168
+ )
169
+
170
+ table users (
171
+ id BIGINT
172
+ name VARCHAR
173
+ ...
174
+ )
175
+ ```
176
+
177
+ We can write a query that leverages the modeled join relationship between `orders` and `users`:
178
+
179
+
180
+ ```sql
181
+ -- Top 10 customers by order count
182
+ select
183
+ users.name, -- Use the dot operator to traverse the modeled join relationship
184
+ count(*)
185
+ from orders -- A join statement here is not needed
186
+ group by 1
187
+ order by 2 desc
188
+ limit 10
189
+ ```
190
+
191
+ #### Multi-hop joins
192
+
193
+ Sometimes you need to access columns or stored expressions in a table that is two or more joins away from the `from` table. To do this, simply use more dot operators to trace the desired join path. For example, say there is another table added to our project, `countries`:
194
+
195
+ ```sql
196
+ table orders (
197
+ ...
198
+
199
+ join one users on user_id = users.id
200
+ )
201
+
202
+ table users (
203
+ ...
204
+
205
+ join many orders on id = orders.user_id
206
+ join one countries on country_code = countries.code
207
+ )
208
+
209
+ table countries (
210
+ code VARCHAR
211
+ name VARCHAR
212
+ currency VARCHAR
213
+ free_shipping BOOLEAN
214
+
215
+ join many users on code = users.country_code
216
+ )
217
+ ```
218
+
219
+ We can write the following query to show the top ten countries by order count:
220
+
221
+ ```sql
222
+ -- Top 10 countries by order count
223
+ select
224
+ users.countries.name, -- Orders -> Users -> Countries
225
+ count(*)
226
+ from orders
227
+ group by 1
228
+ order by 2 desc
229
+ limit 10
230
+ ```
231
+
232
+ ### Using stored expressions in queries
233
+
234
+ A stored expression can be invoked in a query by simply referencing it by name.
235
+
236
+ Again, using the orders table from before:
237
+
238
+ ```sql
239
+ table orders (
240
+ id BIGINT
241
+ user_id BIGINT
242
+ created_at DATETIME
243
+ status STRING -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
244
+ amount FLOAT -- Amount paid by customer #units=usd
245
+ cost FLOAT -- Cost of materials #units=usd
246
+
247
+ join one users on user_id = users.id
248
+
249
+ revenue_recognized: status in ('Processing', 'Shipped', 'Complete')
250
+ revenue: sum(case when revenue_recognized then amount else 0 end) #units=usd
251
+ cogs: sum(case when revenue_recognized then cost else 0 end) #units=usd
252
+ profit: revenue - cogs #units=usd
253
+ profit_margin: profit / revenue #ratio
254
+ )
255
+ ```
256
+
257
+ We can count the number of orders that were revenue-recognized vs. not:
258
+
259
+ ```sql
260
+ -- Number of revenue-recognized orders vs. not
261
+ select
262
+ revenue_recognized, -- Stored expression in orders
263
+ count(*)
264
+ from orders
265
+ group by 1
266
+ ```
267
+
268
+ This would be equivalent to:
269
+
270
+ ```sql
271
+ select
272
+ status in ('Processing', 'Shipped', 'Complete') as revenue_recognized,
273
+ count(*)
274
+ from orders
275
+ group by 1
276
+ ```
277
+
278
+ You can see that invoking a stored expression is like using a macro: the definition for the stored expression is effectively expanded in-line by Graphene when it runs the query.
279
+
280
+ #### Using measures
281
+
282
+ The macro concept is important to understand when invoking stored expressions that are **aggregative** (ie. contain agg functions), which can also be called "measures." Here's an example.
283
+
284
+ ```sql
285
+ -- Profit by status
286
+ select
287
+ status,
288
+ profit
289
+ from orders
290
+ group by 1
291
+ order by 1 asc
292
+ ```
293
+
294
+ Note that, while `profit` looks like a column here, it is _not_ a column. That's because this query is equivalent to:
295
+
296
+ ```sql
297
+ select
298
+ status,
299
+ sum(case when revenue_recognized then amount else 0 end) - sum(case when revenue_recognized then cost else 0 end) as profit -- Profit is defined as revenue - cogs, which respectively expands out to these two filtered sums
300
+ from orders
301
+ group by 1
302
+ order by 1 asc
303
+ ```
304
+
305
+ [CRITICAL!] This means:
306
+ - You would NEVER wrap a measure in an agg function like `sum(my_measure)`, for the same reason that you cannot do `SUM(SUM(foo))` in regular SQL.
307
+ - You would NEVER group by a measure like `group by my_measure`, for the same reason that you cannot do `GROUP BY SUM(foo)` in regular SQL.
308
+ - You CAN wrap a measure in a scalar function like `floor(my_measure)`, for the same reason that can do `FLOOR(SUM(foo))` in regular SQL.
309
+ - You CAN compose measures together in expressions like `my_measure + my_other_measure`, for the same reason that you can do `SUM(foo) + SUM(bar)` in regular SQL.
310
+
311
+ Another way of thinking about this is that measures are "self-aggregating."
312
+
313
+ ### Other miscellaneous details
314
+
315
+ - The clauses in a `select` statement (`select`, `from`, `join`, `group by`, etc.) can be written in any order. They cannot be repeated, however.
316
+ - `group by all` is implied if aggregative and scalar expressions are both present in the `select` clause. This means that `group by` can be omitted and the query will still effectively execute the `group by all`.
317
+ - Expressions in `group by` are implicitly selected, so `from orders select avg(amount) group by user_id` will return two columns.
318
+ - `count` is a reserved word. Do not alias your columns as `count`.
319
+ - Inline window functions are supported using ANSI-style `OVER (...)` clauses. Query-level named windows (`WINDOW w AS (...)`) are not supported.
320
+ - Percentiles can be computed easily using Graphene's special functions `pXX(col)` (e.g., p50, p975, p9999).
321
+ - Graphene supports almost all functions of the connected data warehouse. Check package.json to see which database you're connected to.
322
+
323
+ ## `table X as` statements
324
+
325
+ You can turn the output of any `select` statement into a table with `table foo as (select ...)`. Here's an example of an additional table `user_facts` added to the two tables from earlier:
326
+
327
+ ```sql
328
+ table orders (
329
+ id BIGINT
330
+ user_id BIGINT
331
+ created_at DATETIME
332
+ status STRING -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
333
+ amount FLOAT -- Amount paid by customer #units=usd
334
+ cost FLOAT -- Cost of materials #units=usd
335
+
336
+ join one users on user_id = users.id
337
+
338
+ revenue_recognized: status in ('Processing', 'Shipped', 'Complete')
339
+ revenue: sum(case when revenue_recognized then amount else 0 end) #units=usd
340
+ cogs: sum(case when revenue_recognized then cost else 0 end) #units=usd
341
+ profit: revenue - cogs #units=usd
342
+ profit_margin: profit / revenue #ratio
343
+ )
344
+
345
+ table users (
346
+ id BIGINT
347
+ name VARCHAR
348
+ email VARCHAR
349
+ age INTEGER
350
+
351
+ join many orders on id = orders.user_id
352
+ join one user_facts on id = user_facts.id
353
+
354
+ ltv: user_facts.ltv #units=usd
355
+ lifetime_orders: user_facts.lifetime_orders
356
+ )
357
+
358
+ table user_facts as (
359
+ select id, orders.revenue as ltv, count(orders.id) as lifetime_orders,
360
+ from users group by id
361
+ )
362
+ ```
363
+
364
+ `table X as` statements are conceptually the same as view tables and CTEs in regular SQL. A few things to note:
365
+ - You cannot declare join relationships or stored expressions directly in a `table X as` statement. Use an `extend` statement.
366
+ - In the example above, the `ltv` and `lifetime_orders` columns from `user_facts` are "hoisted" back into `users` so that they appear as if they are columns from `users`. This is simply a design choice which allows query writers to never need to know about `user_facts`.
367
+
368
+ ## `extend` statements
369
+
370
+ `extend` statements allow you to add join relationships or stored expressions to an existing table. This is especially useful for tables created via `table X as` statements, which do not support defining these properties directly.
371
+
372
+ For example, if we have a `table X as` statement that creates a daily summary of orders:
373
+
374
+ ```sql
375
+ table regional_orders as (
376
+ select
377
+ region,
378
+ count(*) as num_orders,
379
+ sum(amount) as total_revenue
380
+ from orders
381
+ group by 1
382
+ )
383
+ ```
384
+
385
+ We can extend this table to add measures or joins:
386
+
387
+ ```sql
388
+ extend regional_orders (
389
+ join one regions on region = regions.name
390
+
391
+ avg_order_value: total_revenue / num_orders #units=usd
392
+ )
393
+ ```