Skip to content

Commit 14ea16d

Browse files
authored
feat(appkit): relocate typegen cache to committed .appkit/ dir (#502)
* feat(shared): add committed/ephemeral typegen cache path resolvers - Shared path resolver for cache PATH. Single-source-of-truth for all tasks and runtime - Sort cache entries so output shape is deterministic (prevents noise on `git diff`) - Discern connectivity errors from warehouse Auth access with a tailored error message on the latter - move cache from `node_modules/.databricks` to `.appkit/` - ignore `.appkit/**` on Biome linter - Add a one-liner to template Readme.md - Add a few relevant tests --- Signed-off-by: Atila Fassina <atila@fassina.eu>
1 parent 72dd0a6 commit 14ea16d

23 files changed

Lines changed: 797 additions & 55 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"version": "1",
3+
"endpoints": {
4+
"default": {
5+
"hash": "e66c960c5d57436d7fc4e21086650a534892a1548ccf2b333db2af6046832471",
6+
"requestType": "{\n messages?: {\n role?: \"user\" | \"assistant\";\n content?: string;\n }[];\n /** @openapi integer, nullable */\n n?: number | null;\n max_tokens?: number;\n /** @openapi double, nullable */\n top_p?: number | null;\n reasoning_effort?: \"low\" | \"medium\" | \"high\" | \"max\" | null;\n /** @openapi double, nullable */\n temperature?: number | null;\n stop?: string | string[] | null;\n /** @openapi integer, nullable */\n top_k?: number | null;\n}",
7+
"responseType": "{\n model?: string;\n choices?: {\n index?: number;\n message?: {\n role?: \"user\" | \"assistant\";\n content?: string;\n };\n finish_reason?: string;\n }[];\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n } | null;\n object?: string;\n id?: string;\n created?: number;\n}",
8+
"chunkType": "{\n model?: string;\n choices?: {\n index?: number;\n delta?: {\n role?: \"user\" | \"assistant\";\n content?: string;\n };\n finish_reason?: string | null;\n }[];\n object?: string;\n id?: string;\n created?: number;\n}",
9+
"requestKeys": [
10+
"messages",
11+
"n",
12+
"max_tokens",
13+
"top_p",
14+
"reasoning_effort",
15+
"temperature",
16+
"stop",
17+
"top_k"
18+
]
19+
}
20+
}
21+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
{
2+
"version": "3",
3+
"queries": {
4+
"app_activity_heatmap": {
5+
"hash": "744281be86b14c2b14ed76b94060e4af",
6+
"type": "{\n name: \"app_activity_heatmap\";\n parameters: {\n /** DATE - use sql.date() */\n startDate: SQLDateMarker;\n /** DATE - use sql.date() */\n endDate: SQLDateMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n app_name: string;\n /** @sqlType STRING */\n day_of_week: string;\n /** @sqlType DECIMAL(35,2) */\n spend: number;\n }>;\n }",
7+
"retry": false
8+
},
9+
"apps_list": {
10+
"hash": "4b7393b43f8e7beaa52174ed4d918c04",
11+
"type": "{\n name: \"apps_list\";\n parameters: Record<string, never>;\n result: Array<{\n /** @sqlType STRING */\n id: string;\n /** @sqlType STRING */\n name: string;\n /** @sqlType STRING */\n creator: string;\n /** @sqlType STRING */\n tags: string;\n /** @sqlType DECIMAL(38,6) */\n totalSpend: number;\n /** @sqlType DATE */\n createdAt: string;\n }>;\n }",
12+
"retry": false
13+
},
14+
"cost_recommendations": {
15+
"hash": "730c7d8b5e2726981088b5975157b0da",
16+
"type": "{\n name: \"cost_recommendations\";\n parameters: Record<string, never>;\n result: Array<{\n /** @sqlType INT */\n dummy: number;\n }>;\n }",
17+
"retry": false
18+
},
19+
"dashboard_fare_distribution": {
20+
"hash": "d280e96e06e1b5189d8fe734901e6e72",
21+
"type": "{\n name: \"dashboard_fare_distribution\";\n parameters: {\n /** STRING - use sql.string() */\n dateFrom: SQLStringMarker;\n /** STRING - use sql.string() */\n dateTo: SQLStringMarker;\n /** STRING - use sql.string() */\n pickupZip: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n fare_bucket: string;\n /** @sqlType BIGINT */\n trip_count: number;\n /** @sqlType DOUBLE */\n avg_distance: number;\n }>;\n }",
22+
"retry": false
23+
},
24+
"dashboard_hourly_heatmap": {
25+
"hash": "827ca45db36ccf6f9c86c653c6175d69",
26+
"type": "{\n name: \"dashboard_hourly_heatmap\";\n parameters: {\n /** STRING - use sql.string() */\n dateFrom: SQLStringMarker;\n /** STRING - use sql.string() */\n dateTo: SQLStringMarker;\n /** STRING - use sql.string() */\n pickupZip: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMin: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMax: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType INT */\n day_of_week: number;\n /** @sqlType INT */\n hour_of_day: number;\n /** @sqlType BIGINT */\n trip_count: number;\n /** @sqlType DOUBLE */\n avg_fare: number;\n }>;\n }",
27+
"retry": false
28+
},
29+
"dashboard_kpi_sparklines": {
30+
"hash": "1b3c14b03e1de455ff78f2d3feaec047",
31+
"type": "{\n name: \"dashboard_kpi_sparklines\";\n parameters: {\n /** STRING - use sql.string() */\n dateFrom: SQLStringMarker;\n /** STRING - use sql.string() */\n dateTo: SQLStringMarker;\n /** STRING - use sql.string() */\n pickupZip: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMin: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMax: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType DATE */\n trip_date: string;\n /** @sqlType BIGINT */\n trip_count: number;\n /** @sqlType DOUBLE */\n total_revenue: number;\n /** @sqlType DOUBLE */\n avg_fare: number;\n /** @sqlType DOUBLE */\n avg_distance: number;\n }>;\n }",
32+
"retry": false
33+
},
34+
"dashboard_kpis": {
35+
"hash": "24ef19b8b1218edb8f247f6d79c915ff",
36+
"type": "{\n name: \"dashboard_kpis\";\n parameters: {\n /** STRING - use sql.string() */\n dateFrom: SQLStringMarker;\n /** STRING - use sql.string() */\n dateTo: SQLStringMarker;\n /** STRING - use sql.string() */\n pickupZip: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMin: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMax: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType BIGINT */\n total_trips: number;\n /** @sqlType DOUBLE */\n avg_fare: number;\n /** @sqlType DOUBLE */\n avg_distance: number;\n /** @sqlType DOUBLE */\n max_fare: number;\n /** @sqlType DOUBLE */\n min_fare: number;\n }>;\n }",
37+
"retry": false
38+
},
39+
"dashboard_top_zone": {
40+
"hash": "fe3ed4ef76e4e7eff329c88072a44fe9",
41+
"type": "{\n name: \"dashboard_top_zone\";\n parameters: {\n /** STRING - use sql.string() */\n dateFrom: SQLStringMarker;\n /** STRING - use sql.string() */\n dateTo: SQLStringMarker;\n /** STRING - use sql.string() */\n pickupZip: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMin: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMax: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType INT */\n pickup_zip: number;\n /** @sqlType BIGINT */\n trip_count: number;\n }>;\n }",
42+
"retry": false
43+
},
44+
"dashboard_top_zones": {
45+
"hash": "363c554c5a060ee2236f4e9fa1cf3c61",
46+
"type": "{\n name: \"dashboard_top_zones\";\n parameters: {\n /** STRING - use sql.string() */\n dateFrom: SQLStringMarker;\n /** STRING - use sql.string() */\n dateTo: SQLStringMarker;\n /** STRING - use sql.string() */\n pickupZip: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMin: SQLStringMarker;\n /** STRING - use sql.string() */\n fareMax: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n pickup_zip: string;\n /** @sqlType BIGINT */\n trip_count: number;\n /** @sqlType DOUBLE */\n total_revenue: number;\n /** @sqlType DOUBLE */\n avg_fare: number;\n }>;\n }",
47+
"retry": false
48+
},
49+
"dashboard_trips_over_time": {
50+
"hash": "eb8bed65ed02787ed050a016472b72f6",
51+
"type": "{\n name: \"dashboard_trips_over_time\";\n parameters: {\n /** STRING - use sql.string() */\n dateFrom: SQLStringMarker;\n /** STRING - use sql.string() */\n dateTo: SQLStringMarker;\n /** STRING - use sql.string() */\n pickupZip: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType DATE */\n trip_date: string;\n /** @sqlType BIGINT */\n trip_count: number;\n /** @sqlType DOUBLE */\n avg_fare: number;\n /** @sqlType DOUBLE */\n total_revenue: number;\n }>;\n }",
52+
"retry": false
53+
},
54+
"example": {
55+
"hash": "aeb02ed3e8a6c77279099406f8709543",
56+
"type": "{\n name: \"example\";\n parameters: Record<string, never>;\n result: Array<{\n /** @sqlType BOOLEAN */\n \"(1 = 1)\": boolean;\n }>;\n }",
57+
"retry": false
58+
},
59+
"spend_data": {
60+
"hash": "caa0430652fe15eff658e48e6dac2446",
61+
"type": "{\n name: \"spend_data\";\n parameters: {\n /** STRING - use sql.string() */\n groupBy: SQLStringMarker;\n /** STRING - use sql.string() */\n aggregationLevel: SQLStringMarker;\n /** DATE - use sql.date() */\n startDate: SQLDateMarker;\n /** DATE - use sql.date() */\n endDate: SQLDateMarker;\n /** STRING - use sql.string() */\n appId: SQLStringMarker;\n /** STRING - use sql.string() */\n creator: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n group_key: string;\n /** @sqlType TIMESTAMP */\n aggregation_period: string;\n /** @sqlType DECIMAL(38,6) */\n cost_usd: number;\n }>;\n }",
62+
"retry": false
63+
},
64+
"spend_summary": {
65+
"hash": "bbe188624c3f5904c3a7593cb32982d5",
66+
"type": "{\n name: \"spend_summary\";\n parameters: {\n /** STRING - use sql.string() */\n aggregationLevel: SQLStringMarker;\n /** DATE - use sql.date() */\n endDate: SQLDateMarker;\n /** DATE - use sql.date() */\n startDate: SQLDateMarker;\n };\n result: Array<{\n /** @sqlType DECIMAL(33,0) */\n total: number;\n /** @sqlType DECIMAL(33,0) */\n average: number;\n /** @sqlType DECIMAL(33,0) */\n forecasted: number;\n }>;\n }",
67+
"retry": false
68+
},
69+
"sql_helpers_test": {
70+
"hash": "1322df4ba9c107e8d23e2a04bae860c5",
71+
"type": "{\n name: \"sql_helpers_test\";\n parameters: {\n /** STRING - use sql.string() */\n stringParam: SQLStringMarker;\n /** NUMERIC - use sql.numeric() */\n numberParam: SQLNumberMarker;\n /** BOOLEAN - use sql.boolean() */\n booleanParam: SQLBooleanMarker;\n /** DATE - use sql.date() */\n dateParam: SQLDateMarker;\n /** TIMESTAMP - use sql.timestamp() */\n timestampParam: SQLTimestampMarker;\n /** STRING - use sql.string() */\n binaryParam: SQLStringMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n string_value: string;\n /** @sqlType INT */\n number_value: number;\n /** @sqlType BOOLEAN */\n boolean_value: boolean;\n /** @sqlType STRING */\n date_value: string;\n /** @sqlType STRING */\n timestamp_value: string;\n /** @sqlType BINARY */\n binary_value: string;\n /** @sqlType STRING */\n binary_hex: string;\n /** @sqlType INT */\n binary_length: number;\n }>;\n }",
72+
"retry": false
73+
},
74+
"top_contributors": {
75+
"hash": "2d58759cca2fe31dae06475a23080120",
76+
"type": "{\n name: \"top_contributors\";\n parameters: {\n /** STRING - use sql.string() */\n aggregationLevel: SQLStringMarker;\n /** DATE - use sql.date() */\n startDate: SQLDateMarker;\n /** DATE - use sql.date() */\n endDate: SQLDateMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n app_name: string;\n /** @sqlType DECIMAL(38,6) */\n total_cost_usd: number;\n }>;\n }",
77+
"retry": false
78+
},
79+
"untagged_apps": {
80+
"hash": "5946262b49710b8ab458d1bf950ff8c9",
81+
"type": "{\n name: \"untagged_apps\";\n parameters: {\n /** STRING - use sql.string() */\n aggregationLevel: SQLStringMarker;\n /** DATE - use sql.date() */\n startDate: SQLDateMarker;\n /** DATE - use sql.date() */\n endDate: SQLDateMarker;\n };\n result: Array<{\n /** @sqlType STRING */\n app_name: string;\n /** @sqlType STRING */\n creator: string;\n /** @sqlType DECIMAL(38,6) */\n total_cost_usd: number;\n /** @sqlType DECIMAL(38,10) */\n avg_period_cost_usd: number;\n }>;\n }",
82+
"retry": false
83+
}
84+
},
85+
"metrics": {
86+
"customers": {
87+
"hash": "26269cc37c4ea648a8a9cafb66a402dd",
88+
"schema": {
89+
"key": "customers",
90+
"source": "appkit_demo.public.customer_metrics",
91+
"lane": "obo",
92+
"measures": [
93+
{
94+
"name": "active_accounts",
95+
"type": "bigint",
96+
"isMeasure": true,
97+
"displayName": "Active Accounts",
98+
"format": "#,##0"
99+
},
100+
{
101+
"name": "churn_rate",
102+
"type": "decimal",
103+
"isMeasure": true,
104+
"displayName": "Churn Rate"
105+
},
106+
{
107+
"name": "avg_ltv",
108+
"type": "double",
109+
"isMeasure": true,
110+
"displayName": "Average LTV",
111+
"format": "$#,##0.00"
112+
}
113+
],
114+
"dimensions": [
115+
{
116+
"name": "segment",
117+
"type": "string",
118+
"isMeasure": false,
119+
"displayName": "Customer Segment"
120+
},
121+
{
122+
"name": "region",
123+
"type": "string",
124+
"isMeasure": false,
125+
"displayName": "Region"
126+
},
127+
{
128+
"name": "csm_email",
129+
"type": "string",
130+
"isMeasure": false,
131+
"displayName": "CSM Email"
132+
}
133+
]
134+
},
135+
"retry": false
136+
},
137+
"revenue": {
138+
"hash": "8d0592a864778470213ca22517f21fae",
139+
"schema": {
140+
"key": "revenue",
141+
"source": "appkit_demo.public.revenue_metrics",
142+
"lane": "sp",
143+
"measures": [
144+
{
145+
"name": "mrr",
146+
"type": "double",
147+
"isMeasure": true,
148+
"displayName": "Monthly Recurring Revenue",
149+
"format": "$#,##0.00"
150+
},
151+
{
152+
"name": "arr",
153+
"type": "double",
154+
"isMeasure": true,
155+
"description": "Annualized contract value across all active subscriptions",
156+
"displayName": "Annual Recurring Revenue",
157+
"format": "$#,##0.00"
158+
},
159+
{
160+
"name": "new_arr",
161+
"type": "double",
162+
"isMeasure": true,
163+
"displayName": "New ARR",
164+
"format": "$#,##0.00"
165+
},
166+
{
167+
"name": "churned_arr",
168+
"type": "double",
169+
"isMeasure": true,
170+
"displayName": "Churned ARR",
171+
"format": "$#,##0.00"
172+
}
173+
],
174+
"dimensions": [
175+
{
176+
"name": "region",
177+
"type": "string",
178+
"isMeasure": false,
179+
"displayName": "Region"
180+
},
181+
{
182+
"name": "segment",
183+
"type": "string",
184+
"isMeasure": false,
185+
"displayName": "Customer Segment"
186+
},
187+
{
188+
"name": "created_at",
189+
"type": "timestamp_ltz",
190+
"isMeasure": false,
191+
"displayName": "Subscription Start",
192+
"timeGrains": [
193+
"day",
194+
"hour",
195+
"minute",
196+
"month",
197+
"quarter",
198+
"week",
199+
"year"
200+
]
201+
}
202+
]
203+
},
204+
"retry": false
205+
}
206+
}
207+
}

biome.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"!**/*.d.ts",
1515
"!**/build",
1616
"!**/coverage",
17+
"!**/.appkit",
1718
"!packages/appkit-ui/src/react/ui",
1819
"!**/routeTree.gen.ts",
1920
"!docs/.docusaurus",

docs/docs/app-management.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ databricks apps deploy --help
4242
```
4343
:::
4444

45+
:::info Type cache and warehouse-less deploys
46+
47+
The build step runs type generation in blocking mode. If your deploy pipeline cannot reach a SQL warehouse, the build still succeeds **only if the committed `.appkit/` type cache covers every query** — otherwise it fails rather than shipping degraded types. Commit `.appkit/` while the warehouse is still reachable, before removing warehouse access from CI/CD. See [Type generation → Deploying without warehouse access](./development/type-generation.md#deploying-without-warehouse-access).
48+
49+
:::
50+
4551
### Examples
4652

4753
- Deploy to a specific target:

docs/docs/development/project-setup.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ my-app/
2424
├── config/
2525
│ └── queries/
2626
│ └── my_query.sql
27+
├── .appkit/ # committed type cache (do NOT gitignore)
2728
├── app.yaml
2829
├── package.json
2930
└── tsconfig.json

docs/docs/development/type-generation.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,27 @@ Pass `--wait` for CI and production builds, where accurate types must be present
8282
npx @databricks/appkit generate-types --wait
8383
```
8484

85-
In blocking mode the generator starts a stopped warehouse, waits (bounded) for it to reach `RUNNING`, and then describes your queries. It fails only when the configured warehouse no longer exists (deleted/deleting), so a transient outage or a cold warehouse degrades gracefully rather than breaking the build. The app template wires this up for you: `postinstall` and `predev` run the non-blocking default, while `prebuild` runs `--wait`.
85+
In blocking mode the generator skips the warehouse entirely for any query whose types are already in the [type cache](#type-cache) (a cache **hit** makes zero warehouse round-trips). Only a cache **miss** contacts the warehouse: it starts a stopped warehouse, waits (bounded) for it to reach `RUNNING`, and then describes the query. A miss that cannot be resolved to a real type — a deleted warehouse, or (see below) a CI environment with no warehouse access — **fails the build** rather than shipping `result: unknown`. A transient connectivity blip still degrades gracefully. The app template wires this up for you: `postinstall` and `predev` run the non-blocking default, while `prebuild` runs `--wait`.
86+
87+
## Type cache
88+
89+
Type generation is backed by a per-app cache so that unchanged queries never pay for a repeat `DESCRIBE`. The cache lives in a committed `.appkit/` directory at your app root:
90+
91+
- `.appkit/types-cache.json` — query and metric-view DESCRIBE results
92+
- `.appkit/serving-types-cache.json` — model-serving endpoint schemas
93+
94+
**`.appkit/` is a committed build artifact — check it into git; do not add it to `.gitignore`.** Committing it is what lets a blocking (`--wait`) build succeed with **no warehouse access**: on a fresh `git checkout` + `pnpm install`, every query is already a cache hit, so the build makes zero warehouse round-trips. (Ephemeral coordination state — a background-worker lock and the UI-variant choices file — stays under `node_modules/.databricks/appkit/` and is *not* committed.)
95+
96+
The cache serializes deterministically (top-level keys sorted), so a no-op regeneration produces a no-op diff and unrelated query changes don't collide in merges.
97+
98+
### Deploying without warehouse access
99+
100+
If your CI/CD environment cannot reach a SQL warehouse (for example, an authorization boundary removes warehouse access from the deploy pipeline), a blocking build still works **as long as the committed `.appkit/` cache covers every query** — all hits, no warehouse needed.
101+
102+
This imposes a rollout ordering: **produce and commit `.appkit/` while the warehouse is still reachable, then remove warehouse access.** A build that runs after warehouse access is gone with an uninitialized or incomplete cache fails loudly (never silently degrades to `unknown`), and the error message tells you which case you hit:
103+
104+
- **`No committed type cache found`** (no `.appkit/` yet) — run `generate-types --wait` against a warehouse and commit `.appkit/`.
105+
- **`The committed .appkit/ cache is missing or stale`** (a query was added or changed since the cache was built, or its entry is missing) — regenerate against a warehouse and commit the updated `.appkit/`.
86106

87107
## Metric-view types
88108

0 commit comments

Comments
 (0)