Skip to content

Commit 58f73ad

Browse files
authored
feat: add type-safe sql queries with query registry (#11)
* feat: add type-safe SQL queries with QueryRegistry - Generate TypeScript types from SQL files using DESCRIBE queries - Add specific marker types (SQLDateMarker, SQLStringMarker, etc.) - Parse @param annotations in SQL files for parameter types - Generate JSDoc hints showing which sql.*() helper to use - Add intelligent caching to avoid redundant Databricks queries - Integrate with useAnalyticsQuery for full type inference
1 parent 4964c07 commit 58f73ad

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3056
-678
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ coverage
1010
docs
1111

1212
.turbo
13+
14+
# AppKit type generation cache
15+
.appkit-types-cache.json

apps/dev-playground/client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
8-
"build": "tsc -b && vite build",
8+
"build": "vite build",
99
"lint": "eslint .",
1010
"preview": "vite preview"
1111
},
Lines changed: 148 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,160 @@
11
// Auto-generated by AppKit - DO NOT EDIT
2+
// Generated by 'npx appkit-generate-types' or Vite plugin during build
23
import "@databricks/app-kit-ui/react";
4+
import type { SQLTypeMarker, SQLStringMarker, SQLNumberMarker, SQLBooleanMarker, SQLBinaryMarker, SQLDateMarker, SQLTimestampMarker } from "@databricks/app-kit-ui/js";
35

46
declare module "@databricks/app-kit-ui/react" {
5-
interface PluginRegistry {
6-
"reconnect": {
7-
"/": {
8-
message: string;
7+
interface QueryRegistry {
8+
apps_list: {
9+
name: "apps_list";
10+
parameters: Record<string, never>;
11+
result: Array<{
12+
/** @sqlType STRING */
13+
id: string;
14+
/** @sqlType STRING */
15+
name: string;
16+
/** @sqlType STRING */
17+
creator: string;
18+
/** @sqlType STRING */
19+
tags: string;
20+
/** @sqlType DECIMAL(38,6) */
21+
totalSpend: number;
22+
/** @sqlType DATE */
23+
createdAt: string;
24+
}>;
925
};
10-
"/stream": {
11-
type: string;
12-
count: number;
13-
total: number;
14-
timestamp: string;
15-
content: string;
26+
cost_recommendations: {
27+
name: "cost_recommendations";
28+
parameters: Record<string, never>;
29+
result: Array<{
30+
/** @sqlType INT */
31+
dummy: number;
32+
}>;
1633
};
17-
}
18-
"analytics": {
19-
"/users/me/query/:query_key": {
20-
chunk_index: number;
21-
row_offset: number;
22-
row_count: number;
23-
data: any[];
34+
example: {
35+
name: "example";
36+
parameters: Record<string, never>;
37+
result: Array<{
38+
/** @sqlType BOOLEAN */
39+
"(1 = 1)": boolean;
40+
}>;
2441
};
25-
"/query/:query_key": {
26-
chunk_index: number;
27-
row_offset: number;
28-
row_count: number;
29-
data: any[];
42+
spend_data: {
43+
name: "spend_data";
44+
parameters: {
45+
/** STRING - use sql.string() */
46+
groupBy: SQLStringMarker;
47+
/** STRING - use sql.string() */
48+
aggregationLevel: SQLStringMarker;
49+
/** DATE - use sql.date() */
50+
startDate: SQLDateMarker;
51+
/** DATE - use sql.date() */
52+
endDate: SQLDateMarker;
53+
/** STRING - use sql.string() */
54+
appId: SQLStringMarker;
55+
/** STRING - use sql.string() */
56+
creator: SQLStringMarker;
57+
};
58+
result: Array<{
59+
/** @sqlType STRING */
60+
group_key: string;
61+
/** @sqlType TIMESTAMP */
62+
aggregation_period: string;
63+
/** @sqlType DECIMAL(38,6) */
64+
cost_usd: number;
65+
}>;
3066
};
31-
}
32-
}
33-
34-
interface QueryRegistry {
35-
36-
apps_list: {
37-
id: string;
38-
name: string;
39-
creator: string;
40-
tags: string[];
41-
totalSpend: number;
42-
createdAt: string;
43-
}[];
4467
spend_summary: {
45-
total: number;
46-
average: number;
47-
forecasted: number;
48-
}[];
49-
untagged_apps: {
50-
app_name: string;
51-
creator: string;
52-
total_cost_usd: number;
53-
avg_period_cost_usd: number;
54-
}[];
55-
spend_data: {
56-
group_key: string;
57-
aggregation_period: string;
58-
cost_usd: number;
59-
}[];
60-
top_contributors: {
61-
app_name: string;
62-
total_cost_usd: number;
63-
}[];
68+
name: "spend_summary";
69+
parameters: {
70+
/** STRING - use sql.string() */
71+
aggregationLevel: SQLStringMarker;
72+
/** DATE - use sql.date() */
73+
endDate: SQLDateMarker;
74+
/** DATE - use sql.date() */
75+
startDate: SQLDateMarker;
76+
};
77+
result: Array<{
78+
/** @sqlType DECIMAL(33,0) */
79+
total: number;
80+
/** @sqlType DECIMAL(33,0) */
81+
average: number;
82+
/** @sqlType DECIMAL(33,0) */
83+
forecasted: number;
84+
}>;
85+
};
6486
sql_helpers_test: {
65-
string_value: string;
66-
number_value: number;
67-
boolean_value: boolean;
68-
date_value: string;
69-
timestamp_value: string;
70-
binary_value: string;
71-
binary_hex: string;
72-
binary_length: number;
73-
};
87+
name: "sql_helpers_test";
88+
parameters: {
89+
/** STRING - use sql.string() */
90+
stringParam: SQLStringMarker;
91+
/** NUMERIC - use sql.number() */
92+
numberParam: SQLNumberMarker;
93+
/** BOOLEAN - use sql.boolean() */
94+
booleanParam: SQLBooleanMarker;
95+
/** DATE - use sql.date() */
96+
dateParam: SQLDateMarker;
97+
/** TIMESTAMP - use sql.timestamp() */
98+
timestampParam: SQLTimestampMarker;
99+
/** STRING - use sql.string() */
100+
binaryParam: SQLStringMarker;
101+
};
102+
result: Array<{
103+
/** @sqlType STRING */
104+
string_value: string;
105+
/** @sqlType STRING */
106+
number_value: string;
107+
/** @sqlType STRING */
108+
boolean_value: string;
109+
/** @sqlType STRING */
110+
date_value: string;
111+
/** @sqlType STRING */
112+
timestamp_value: string;
113+
/** @sqlType BINARY */
114+
binary_value: string;
115+
/** @sqlType STRING */
116+
binary_hex: string;
117+
/** @sqlType INT */
118+
binary_length: number;
119+
}>;
120+
};
121+
top_contributors: {
122+
name: "top_contributors";
123+
parameters: {
124+
/** STRING - use sql.string() */
125+
aggregationLevel: SQLStringMarker;
126+
/** DATE - use sql.date() */
127+
startDate: SQLDateMarker;
128+
/** DATE - use sql.date() */
129+
endDate: SQLDateMarker;
130+
};
131+
result: Array<{
132+
/** @sqlType STRING */
133+
app_name: string;
134+
/** @sqlType DECIMAL(38,6) */
135+
total_cost_usd: number;
136+
}>;
137+
};
138+
untagged_apps: {
139+
name: "untagged_apps";
140+
parameters: {
141+
/** STRING - use sql.string() */
142+
aggregationLevel: SQLStringMarker;
143+
/** DATE - use sql.date() */
144+
startDate: SQLDateMarker;
145+
/** DATE - use sql.date() */
146+
endDate: SQLDateMarker;
147+
};
148+
result: Array<{
149+
/** @sqlType STRING */
150+
app_name: string;
151+
/** @sqlType STRING */
152+
creator: string;
153+
/** @sqlType DECIMAL(38,6) */
154+
total_cost_usd: number;
155+
/** @sqlType DECIMAL(38,10) */
156+
avg_period_cost_usd: number;
157+
}>;
158+
};
74159
}
75160
}

apps/dev-playground/client/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@
99
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
1010

1111
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as TypeSafetyRouteRouteImport } from './routes/type-safety.route'
1213
import { Route as TelemetryRouteRouteImport } from './routes/telemetry.route'
1314
import { Route as SqlHelpersRouteRouteImport } from './routes/sql-helpers.route'
1415
import { Route as ReconnectRouteRouteImport } from './routes/reconnect.route'
1516
import { Route as DataVisualizationRouteRouteImport } from './routes/data-visualization.route'
1617
import { Route as AnalyticsRouteRouteImport } from './routes/analytics.route'
1718
import { Route as IndexRouteImport } from './routes/index'
1819

20+
const TypeSafetyRouteRoute = TypeSafetyRouteRouteImport.update({
21+
id: '/type-safety',
22+
path: '/type-safety',
23+
getParentRoute: () => rootRouteImport,
24+
} as any)
1925
const TelemetryRouteRoute = TelemetryRouteRouteImport.update({
2026
id: '/telemetry',
2127
path: '/telemetry',
@@ -54,6 +60,7 @@ export interface FileRoutesByFullPath {
5460
'/reconnect': typeof ReconnectRouteRoute
5561
'/sql-helpers': typeof SqlHelpersRouteRoute
5662
'/telemetry': typeof TelemetryRouteRoute
63+
'/type-safety': typeof TypeSafetyRouteRoute
5764
}
5865
export interface FileRoutesByTo {
5966
'/': typeof IndexRoute
@@ -62,6 +69,7 @@ export interface FileRoutesByTo {
6269
'/reconnect': typeof ReconnectRouteRoute
6370
'/sql-helpers': typeof SqlHelpersRouteRoute
6471
'/telemetry': typeof TelemetryRouteRoute
72+
'/type-safety': typeof TypeSafetyRouteRoute
6573
}
6674
export interface FileRoutesById {
6775
__root__: typeof rootRouteImport
@@ -71,6 +79,7 @@ export interface FileRoutesById {
7179
'/reconnect': typeof ReconnectRouteRoute
7280
'/sql-helpers': typeof SqlHelpersRouteRoute
7381
'/telemetry': typeof TelemetryRouteRoute
82+
'/type-safety': typeof TypeSafetyRouteRoute
7483
}
7584
export interface FileRouteTypes {
7685
fileRoutesByFullPath: FileRoutesByFullPath
@@ -81,6 +90,7 @@ export interface FileRouteTypes {
8190
| '/reconnect'
8291
| '/sql-helpers'
8392
| '/telemetry'
93+
| '/type-safety'
8494
fileRoutesByTo: FileRoutesByTo
8595
to:
8696
| '/'
@@ -89,6 +99,7 @@ export interface FileRouteTypes {
8999
| '/reconnect'
90100
| '/sql-helpers'
91101
| '/telemetry'
102+
| '/type-safety'
92103
id:
93104
| '__root__'
94105
| '/'
@@ -97,6 +108,7 @@ export interface FileRouteTypes {
97108
| '/reconnect'
98109
| '/sql-helpers'
99110
| '/telemetry'
111+
| '/type-safety'
100112
fileRoutesById: FileRoutesById
101113
}
102114
export interface RootRouteChildren {
@@ -106,10 +118,18 @@ export interface RootRouteChildren {
106118
ReconnectRouteRoute: typeof ReconnectRouteRoute
107119
SqlHelpersRouteRoute: typeof SqlHelpersRouteRoute
108120
TelemetryRouteRoute: typeof TelemetryRouteRoute
121+
TypeSafetyRouteRoute: typeof TypeSafetyRouteRoute
109122
}
110123

111124
declare module '@tanstack/react-router' {
112125
interface FileRoutesByPath {
126+
'/type-safety': {
127+
id: '/type-safety'
128+
path: '/type-safety'
129+
fullPath: '/type-safety'
130+
preLoaderRoute: typeof TypeSafetyRouteRouteImport
131+
parentRoute: typeof rootRouteImport
132+
}
113133
'/telemetry': {
114134
id: '/telemetry'
115135
path: '/telemetry'
@@ -162,6 +182,7 @@ const rootRouteChildren: RootRouteChildren = {
162182
ReconnectRouteRoute: ReconnectRouteRoute,
163183
SqlHelpersRouteRoute: SqlHelpersRouteRoute,
164184
TelemetryRouteRoute: TelemetryRouteRoute,
185+
TypeSafetyRouteRoute: TypeSafetyRouteRoute,
165186
}
166187
export const routeTree = rootRouteImport
167188
._addFileChildren(rootRouteChildren)

apps/dev-playground/client/src/routes/__root.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ function RootComponent() {
6363
SQL Helpers
6464
</Button>
6565
</Link>
66+
<Link to="/type-safety" className="no-underline">
67+
<Button
68+
variant="ghost"
69+
className="text-gray-700 hover:text-gray-900"
70+
>
71+
Type Safety
72+
</Button>
73+
</Link>
6674
</div>
6775
</nav>
6876
</div>

apps/dev-playground/client/src/routes/index.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,24 @@ function IndexRoute() {
121121
</Button>
122122
</div>
123123
</Card>
124+
125+
<Card className="p-6 hover:shadow-lg transition-shadow cursor-pointer">
126+
<div className="flex flex-col h-full">
127+
<h3 className="text-2xl font-semibold text-gray-900 mb-3">
128+
Type-Safe SQL
129+
</h3>
130+
<p className="text-gray-600 mb-6 flex-grow">
131+
Generate TypeScript types from SQL files at build time. Full
132+
IntelliSense for query names, parameters, and results.
133+
</p>
134+
<Button
135+
onClick={() => navigate({ to: "/type-safety" })}
136+
className="w-full"
137+
>
138+
Explore Type Safety
139+
</Button>
140+
</div>
141+
</Card>
124142
</div>
125143

126144
<div className="text-center pt-12 border-t border-gray-200">

0 commit comments

Comments
 (0)