Skip to content

Commit f98149c

Browse files
Refactor frontline database layer and add SQL query documentation (#5288)
* refactor: copy new mysql package * fix: generate into new mysql package * refactor: move frontline queries * optimize frontline database * fix: remove wrong index * chore: clean up indices some were also uncommented for no reason, I have checked the db * docs: make sql comments more helpful and less verbose * Update dev/k8s/manifests/frontline.yaml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update svc/frontline/config.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent e1401a4 commit f98149c

38 files changed

+2151
-112
lines changed

dev/k8s/manifests/frontline.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ data:
1212
http_port = 7443
1313
apex_domain = "unkey.local"
1414
ctrl_addr = "http://ctrl-api:7091"
15+
database_url = "unkey:password@tcp(mysql:3306)/unkey?parseTime=true&interpolateParams=true"
1516
1617
[tls]
1718
cert_file = "/certs/unkey.local.crt"
1819
key_file = "/certs/unkey.local.key"
1920
20-
[database]
21-
primary = "unkey:password@tcp(mysql:3306)/unkey?parseTime=true&interpolateParams=true"
2221
2322
[gossip]
2423
lan_port = 7946

docs/engineering/contributing/quality/documentation.mdx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ Before submitting documentation, verify each item.
2525
- [ ] Package has a `doc.go` if it has non-trivial behavior
2626
- [ ] Non-obvious behavior is documented (edge cases, nil handling, concurrency)
2727
- [ ] The why is explained for design choices that are not self-evident
28+
- [ ] Every named SQL query has a doc comment block
2829

2930
**Quality**
3031
- [ ] Doc comments start with the symbol name
3132
- [ ] Uses prose, not bullet lists, unless items are parallel
3233
- [ ] Depth matches complexity
34+
- [ ] SQL comments add non-obvious context instead of restating obvious clauses
3335
- [ ] Cross-references use bracket syntax: `[TypeName]`, `[FuncName]`
3436
- [ ] No stale documentation from copy-paste or refactoring
3537

@@ -38,6 +40,7 @@ Before submitting documentation, verify each item.
3840
- [ ] For value plus error returns, you checked what value returns on failure
3941
- [ ] For unmarshal operations, you verified whether partial values return
4042
- [ ] Examples compile and run
43+
- [ ] SQL comment examples match real query behavior (ordering, fallback, joins)
4144

4245
## Writing style
4346

@@ -151,6 +154,45 @@ func (r *RateLimiter) Allow(ctx context.Context, identifier string, cost int) (b
151154

152155
**Context**: Document context behavior only if it is non-standard.
153156

157+
## SQL query documentation
158+
159+
Named SQL queries are part of the public contract between application code and the database. Treat query comments like API documentation.
160+
161+
For SQL query docs, explain why this query exists, how it resolves non-obvious behavior, and what guarantees callers can rely on. Do not just restate the `SELECT` clause.
162+
163+
Keep SQL comments concise. In most cases, two to five lines are enough. If a comment is longer than the query, each sentence must carry non-obvious information such as fallback guarantees, deterministic ordering, intentional join behavior, or performance tradeoffs.
164+
165+
Avoid duplicate explanations. If the SQL already makes behavior obvious, for example `AND s.health = 'healthy'`, do not repeat that in prose unless you are documenting a non-obvious guarantee that depends on it.
166+
167+
For selection queries with fallback logic, document deterministic behavior explicitly. If exact matching must win over wildcard matching, explain how SQL enforces it, for example with `ORDER BY` and not candidate list order.
168+
169+
For query docs that are not obvious from a quick read, include a small concrete example with inputs and the expected returned row. This is required for logic that depends on ordering, fallback, or tie-breaking.
170+
171+
Document performance-sensitive choices when they are intentional, for example using `LIMIT 1` to avoid transferring large payload rows that are not selected.
172+
173+
```sql
174+
-- name: FindBestCertificateByCandidates :one
175+
-- FindBestCertificateByCandidates returns one certificate row for the provided
176+
-- hostnames, preferring an exact hostname over wildcard matches.
177+
-- MySQL does not preserve IN-list order, so exact-first behavior is enforced by
178+
-- ORDER BY against exact_hostname, not by candidate position.
179+
--
180+
-- Example: with candidates ['api.example.com', '*.example.com'] and
181+
-- exact_hostname 'api.example.com', this query returns 'api.example.com' when
182+
-- both rows exist. If only '*.example.com' exists, it returns the wildcard row.
183+
--
184+
-- LIMIT 1 avoids returning non-selected certificate and key payload rows.
185+
SELECT
186+
hostname,
187+
workspace_id,
188+
certificate,
189+
encrypted_private_key
190+
FROM certificates
191+
WHERE hostname IN (sqlc.slice('hostnames'))
192+
ORDER BY hostname = sqlc.arg(exact_hostname) DESC
193+
LIMIT 1;
194+
```
195+
154196
## What not to document
155197

156198
Do not document implementation details in doc comments. Those belong inside the function. Do not explain that context is used for cancellation or mention O(1) performance unless it is surprising.

pkg/db/schema.sql

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,9 @@ CREATE TABLE `deployment_topology` (
539539
`desired_status` enum('stopped','running') NOT NULL,
540540
`created_at` bigint NOT NULL,
541541
`updated_at` bigint,
542-
CONSTRAINT `deployment_topology_pk` PRIMARY KEY(`pk`)
542+
CONSTRAINT `deployment_topology_pk` PRIMARY KEY(`pk`),
543+
CONSTRAINT `unique_region_per_deployment` UNIQUE(`deployment_id`,`region_id`),
544+
CONSTRAINT `unique_version_per_region` UNIQUE(`region_id`,`version`)
543545
);
544546

545547
CREATE TABLE `acme_users` (
@@ -603,7 +605,7 @@ CREATE TABLE `sentinels` (
603605
`environment_id` varchar(255) NOT NULL,
604606
`k8s_name` varchar(64) NOT NULL,
605607
`k8s_address` varchar(255) NOT NULL,
606-
`region_id` varchar(255) NOT NULL DEFAULT 'TODO',
608+
`region_id` varchar(255) NOT NULL,
607609
`image` varchar(255) NOT NULL,
608610
`desired_state` enum('running','standby','archived') NOT NULL DEFAULT 'running',
609611
`health` enum('unknown','paused','healthy','unhealthy') NOT NULL DEFAULT 'unknown',
@@ -617,7 +619,9 @@ CREATE TABLE `sentinels` (
617619
CONSTRAINT `sentinels_pk` PRIMARY KEY(`pk`),
618620
CONSTRAINT `sentinels_id_unique` UNIQUE(`id`),
619621
CONSTRAINT `sentinels_k8s_name_unique` UNIQUE(`k8s_name`),
620-
CONSTRAINT `sentinels_k8s_address_unique` UNIQUE(`k8s_address`)
622+
CONSTRAINT `sentinels_k8s_address_unique` UNIQUE(`k8s_address`),
623+
CONSTRAINT `one_env_per_region` UNIQUE(`environment_id`,`region_id`),
624+
CONSTRAINT `unique_version_per_region` UNIQUE(`region_id`,`version`)
621625
);
622626

623627
CREATE TABLE `instances` (
@@ -708,7 +712,9 @@ CREATE TABLE `cilium_network_policies` (
708712
`created_at` bigint NOT NULL,
709713
`updated_at` bigint,
710714
CONSTRAINT `cilium_network_policies_pk` PRIMARY KEY(`pk`),
711-
CONSTRAINT `cilium_network_policies_id_unique` UNIQUE(`id`)
715+
CONSTRAINT `cilium_network_policies_id_unique` UNIQUE(`id`),
716+
CONSTRAINT `one_deployment_per_region` UNIQUE(`deployment_id`,`region_id`,`k8s_name`),
717+
CONSTRAINT `unique_version_per_region` UNIQUE(`region_id`,`version`)
712718
);
713719

714720
CREATE TABLE `clusters` (
@@ -778,11 +784,12 @@ CREATE INDEX `project_idx` ON `custom_domains` (`project_id`);
778784
CREATE INDEX `verification_status_idx` ON `custom_domains` (`verification_status`);
779785
CREATE INDEX `workspace_idx` ON `acme_challenges` (`workspace_id`);
780786
CREATE INDEX `status_idx` ON `acme_challenges` (`status`);
781-
CREATE INDEX `idx_environment_id` ON `sentinels` (`environment_id`);
787+
CREATE INDEX `idx_environment_health_region_routing` ON `sentinels` (`environment_id`,`region_id`,`health`);
782788
CREATE INDEX `idx_deployment_id` ON `instances` (`deployment_id`);
783789
CREATE INDEX `idx_region` ON `instances` (`region_id`);
784790
CREATE INDEX `environment_id_idx` ON `frontline_routes` (`environment_id`);
785791
CREATE INDEX `deployment_id_idx` ON `frontline_routes` (`deployment_id`);
792+
CREATE INDEX `fqdn_environment_deployment_idx` ON `frontline_routes` (`fully_qualified_domain_name`,`environment_id`,`deployment_id`);
786793
CREATE INDEX `installation_id_idx` ON `github_repo_connections` (`installation_id`);
787794
CREATE INDEX `workspace_idx` ON `horizontal_autoscaling_policies` (`workspace_id`);
788795

pkg/mysql/database.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,11 @@ func (d *database) RO() *Replica {
149149
// This should be called when the application is shutting down.
150150
func (d *database) Close() error {
151151
// Close the write replica connection
152-
writeCloseErr := d.writeReplica.db.Close()
152+
writeCloseErr := d.writeReplica.Close()
153153

154154
// Only close the read replica if it's a separate connection
155155
if d.readReplica != nil {
156-
readCloseErr := d.readReplica.db.Close()
156+
readCloseErr := d.readReplica.Close()
157157
if readCloseErr != nil {
158158
return fault.Wrap(readCloseErr)
159159
}

pkg/mysql/replica.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,7 @@ func (r *Replica) Begin(ctx context.Context) (DBTx, error) {
174174
// Wrap the transaction with tracing
175175
return WrapTxWithContext(tx, r.mode+"_tx", ctx), nil
176176
}
177+
178+
func (r *Replica) Close() error {
179+
return r.db.Close()
180+
}

pkg/mysql/schema.sql

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,9 @@ CREATE TABLE `deployment_topology` (
539539
`desired_status` enum('stopped','running') NOT NULL,
540540
`created_at` bigint NOT NULL,
541541
`updated_at` bigint,
542-
CONSTRAINT `deployment_topology_pk` PRIMARY KEY(`pk`)
542+
CONSTRAINT `deployment_topology_pk` PRIMARY KEY(`pk`),
543+
CONSTRAINT `unique_region_per_deployment` UNIQUE(`deployment_id`,`region_id`),
544+
CONSTRAINT `unique_version_per_region` UNIQUE(`region_id`,`version`)
543545
);
544546

545547
CREATE TABLE `acme_users` (
@@ -603,7 +605,7 @@ CREATE TABLE `sentinels` (
603605
`environment_id` varchar(255) NOT NULL,
604606
`k8s_name` varchar(64) NOT NULL,
605607
`k8s_address` varchar(255) NOT NULL,
606-
`region_id` varchar(255) NOT NULL DEFAULT 'TODO',
608+
`region_id` varchar(255) NOT NULL,
607609
`image` varchar(255) NOT NULL,
608610
`desired_state` enum('running','standby','archived') NOT NULL DEFAULT 'running',
609611
`health` enum('unknown','paused','healthy','unhealthy') NOT NULL DEFAULT 'unknown',
@@ -617,7 +619,9 @@ CREATE TABLE `sentinels` (
617619
CONSTRAINT `sentinels_pk` PRIMARY KEY(`pk`),
618620
CONSTRAINT `sentinels_id_unique` UNIQUE(`id`),
619621
CONSTRAINT `sentinels_k8s_name_unique` UNIQUE(`k8s_name`),
620-
CONSTRAINT `sentinels_k8s_address_unique` UNIQUE(`k8s_address`)
622+
CONSTRAINT `sentinels_k8s_address_unique` UNIQUE(`k8s_address`),
623+
CONSTRAINT `one_env_per_region` UNIQUE(`environment_id`,`region_id`),
624+
CONSTRAINT `unique_version_per_region` UNIQUE(`region_id`,`version`)
621625
);
622626

623627
CREATE TABLE `instances` (
@@ -708,7 +712,9 @@ CREATE TABLE `cilium_network_policies` (
708712
`created_at` bigint NOT NULL,
709713
`updated_at` bigint,
710714
CONSTRAINT `cilium_network_policies_pk` PRIMARY KEY(`pk`),
711-
CONSTRAINT `cilium_network_policies_id_unique` UNIQUE(`id`)
715+
CONSTRAINT `cilium_network_policies_id_unique` UNIQUE(`id`),
716+
CONSTRAINT `one_deployment_per_region` UNIQUE(`deployment_id`,`region_id`),
717+
CONSTRAINT `unique_version_per_region` UNIQUE(`region_id`,`version`)
712718
);
713719

714720
CREATE TABLE `clusters` (
@@ -778,11 +784,12 @@ CREATE INDEX `project_idx` ON `custom_domains` (`project_id`);
778784
CREATE INDEX `verification_status_idx` ON `custom_domains` (`verification_status`);
779785
CREATE INDEX `workspace_idx` ON `acme_challenges` (`workspace_id`);
780786
CREATE INDEX `status_idx` ON `acme_challenges` (`status`);
781-
CREATE INDEX `idx_environment_id` ON `sentinels` (`environment_id`);
787+
CREATE INDEX `idx_environment_health_region_routing` ON `sentinels` (`environment_id`,`region_id`,`health`);
782788
CREATE INDEX `idx_deployment_id` ON `instances` (`deployment_id`);
783789
CREATE INDEX `idx_region` ON `instances` (`region_id`);
784790
CREATE INDEX `environment_id_idx` ON `frontline_routes` (`environment_id`);
785791
CREATE INDEX `deployment_id_idx` ON `frontline_routes` (`deployment_id`);
792+
CREATE INDEX `fqdn_environment_deployment_idx` ON `frontline_routes` (`fully_qualified_domain_name`,`environment_id`,`deployment_id`);
786793
CREATE INDEX `installation_id_idx` ON `github_repo_connections` (`installation_id`);
787794
CREATE INDEX `workspace_idx` ON `horizontal_autoscaling_policies` (`workspace_id`);
788795

svc/frontline/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ go_library(
1818
"//pkg/clock",
1919
"//pkg/cluster",
2020
"//pkg/config",
21-
"//pkg/db",
2221
"//pkg/logger",
2322
"//pkg/otel",
2423
"//pkg/prometheus",
@@ -28,6 +27,7 @@ go_library(
2827
"//pkg/tls",
2928
"//pkg/version",
3029
"//pkg/zen",
30+
"//svc/frontline/internal/db",
3131
"//svc/frontline/internal/errorpage",
3232
"//svc/frontline/routes",
3333
"//svc/frontline/services/caches",

svc/frontline/config.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ type Config struct {
5858
// See [config.TLS].
5959
TLS *config.TLS `toml:"tls"`
6060

61-
// Database configures MySQL connections. See [config.DatabaseConfig].
62-
Database config.DatabaseConfig `toml:"database"`
61+
// DatabaseURL is the connection string for the MySQL database.
62+
// It should be a globally load balanced endpoint and only requires read access.
63+
DatabaseURL string `toml:"database_url" config:"required"`
6364

6465
Observability config.Observability `toml:"observability"`
6566

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "db",
5+
srcs = [
6+
"certificate_find_by_hostnames.sql_generated.go",
7+
"database.go",
8+
"doc.go",
9+
"generate.go",
10+
"ingress_route_find_by_fqdn.sql_generated.go",
11+
"models_generated.go",
12+
"querier_generated.go",
13+
"sentinel_find_by_environment_id.sql_generated.go",
14+
],
15+
importpath = "github.com/unkeyed/unkey/svc/frontline/internal/db",
16+
visibility = ["//visibility:public"],
17+
deps = ["//pkg/mysql"],
18+
)

svc/frontline/internal/db/certificate_find_by_hostnames.sql_generated.go

Lines changed: 83 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)