Skip to content

fix(protocol): auto-create a missing database at connect instead of failing 2PC - #174

Closed
maxpert wants to merge 1 commit into
masterfrom
fix/auto-create-database
Closed

maxpert wants to merge 1 commit into
masterfrom
fix/auto-create-database

Conversation

@maxpert

@maxpert maxpert commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #158 / #171. A MySQL client that named a nonexistent database at handshake, COM_INIT_DB or USE was accepted silently; its first write then failed inside PREPARE with prepare quorum not achieved: got 0 acks (1105), because the "database not found" prepare result was neither Rejected nor ConflictDetected and so counted as a missing ack. LLDAP hit this on a single node while creating its base tables.

Fix

  • Connect-time EnsureDatabase (protocol.DatabaseEnsurer): handshake and COM_INIT_DB ensure the database before the OK packet; USE delegates to it. The connection is registered for drain before the call so GracefulDrain can close a parked connect.
  • Exists → nil. Reserved system schemas (information_schema, performance_schema, mysql, sys, case-insensitive) are virtual: accepted, never created. Knob off → MySQL 1049 (42000) Unknown database. Otherwise CREATE DATABASE IF NOT EXISTS through the normal replicated DDL path.
  • Retry only on typed transient contention (1213 write-write conflict, ErrDDLLockHeld) with capped backoff and a deadline from the existing DDL validation timeout; everything else fails fast with the underlying error.
  • HandleQuery fails fast with 1049 for statements naming a nonexistent real database; reserved names are exempt.
  • New config [mysql] auto_create_database (default true), documented in reference.mdx, config.toml and every example config.
  • replica.ReadOnlyHandler implements DatabaseEnsurer symmetrically (never creates).

Pre-existing defects found by the new concurrency test, fixed here

  • Binary intent keys (0xFF/0xFE-prefixed) were formatted with %s into proto3 string fields and zerolog fields; invalid UTF-8 made the gRPC TransactionResponse unmarshalable, so a conflict on one node surfaced as a missing ack. Conflict messages now use %q; log sites use one UTF-8-safe helper.
  • PREPARE "database not found" is now Rejected:true; a database-op intent conflict is ConflictDetected:true (client sees 1213, retryable).
  • An idempotent no-op CREATE/DROP DATABASE IF [NOT] EXISTS no longer bumps the schema version.
  • DDLLockManager comments and four doc lines claimed cluster-wide serialization; the lock is per-node. Cross-node exclusion is the Pebble write-intent conflict in PREPARE.

Test plan

  • Unit: coordinator/auto_create_database_test.go, coordinator/ensure_database_classify_test.go, protocol/database_ensurer_test.go, protocol/handlers/metadata_test.go, db/replication_engine_test.go, db/meta_store_pebble_test.go, replica/handler_test.go, cfg/config_test.go. Every assertion carries a mutation proof that was re-executed by an independent review.
  • Integration: TestClusterAutoCreateDatabaseOnConnect, TestClusterAutoCreateDatabaseConcurrentSafety (two nodes, same name, simultaneous connect: exactly one database, equal schema versions), TestClusterAutoCreateDatabaseDisabled. Full suite 33/33 ok.
  • Reporter's exact flow (LLDAP 0.6.3, no pre-created database) run three times against a binary built from this tree: single node, 3-node cluster, and auto_create_database = false. 3/3 runs, 27/27 assertions each: migrations reach v11, zero quorum errors, exactly one lldap database on every node, 13 tables, knob off yields 1049 and creates nothing. LLDAP then stops on Failed to unpack last_insert_id, which is Generated AUTO_INCREMENT ids overflow 32-bit application id types (blocks LLDAP and similar apps) #170.
  • -race clean on touched packages except two pre-existing races in db/concurrent_conflict_test.go and db/heartbeat_gc_test.go (outside this change).

Out of scope, noted

Pre-existing vet warnings in db/vector_index_manager_test.go and grpc/forward_session.go; the two db races above; TestInsertReplication is load-sensitive (identical timing on master); test-only t.Logf %s on IntentKey in db/insert_or_replace_test.go; SHOW DATABASES LIKE ignores its pattern (unchanged from master).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SABpZUuBQPtum3EGAhUzrj

…ailing 2PC

Defect (issue #158 follow-up): a MySQL client that named a nonexistent
database at handshake, COM_INIT_DB or USE was accepted silently. Its first
write then died inside PREPARE with "prepare quorum not achieved: got 0
acks" (1105), because the "database not found" PrepareResult was neither
Rejected nor ConflictDetected and so counted as a missing ack. LLDAP hit
this on a single node while creating its base tables.

Fix
- protocol.DatabaseEnsurer: handshake and COM_INIT_DB call EnsureDatabase
  before the OK packet; USE delegates to it. Connection is registered for
  drain before the call so GracefulDrain can close a parked connect.
- coordinator.EnsureDatabase: exists -> nil; reserved system schemas
  (information_schema, performance_schema, mysql, sys; case-insensitive)
  are virtual: accepted, never created; knob off -> MySQL 1049 (42000)
  "Unknown database"; else CREATE DATABASE IF NOT EXISTS through the
  normal replicated DDL path. Retry only on typed transient contention
  (1213 write-write conflict, ErrDDLLockHeld), capped backoff, deadline
  from the existing DDL validation timeout; everything else fails fast
  with the underlying error.
- HandleQuery fails fast with 1049 for statements naming a nonexistent
  real database; reserved names are exempt and reach the same path as
  before.
- Config: [mysql] auto_create_database (default true), documented in
  reference.mdx, config.toml and every example config.
- replica.ReadOnlyHandler implements DatabaseEnsurer symmetrically (never
  creates).
- protocol.ErrUnknownDatabase typed error; HandleUseDatabase returns it.

Pre-existing defects found by the new concurrency test and fixed
- Binary intent keys (0xFF/0xFE-prefixed) were formatted with %s into
  proto3 string fields and zerolog fields; invalid UTF-8 made the gRPC
  TransactionResponse unmarshalable, so a conflict on one node was
  reported as a missing ack. Conflict messages now use %q and log sites
  use one UTF-8-safe helper (db/meta_store_pebble.go, db/replication_engine.go).
- PREPARE "database not found" is now Rejected:true; a database-op intent
  conflict is ConflictDetected:true (client sees 1213, retryable).
- An idempotent no-op CREATE/DROP DATABASE IF [NOT] EXISTS no longer bumps
  the schema version (CommitResult.IdempotentNoOp -> WriteTransaction
  returns whether the schema changed).
- DDLLockManager comments claimed cluster-wide serialization; it is
  per-node. Cross-node exclusion is the Pebble intent-key conflict.

Tests
- Unit: coordinator/auto_create_database_test.go,
  coordinator/ensure_database_classify_test.go,
  protocol/database_ensurer_test.go, protocol/handlers/metadata_test.go,
  db/replication_engine_test.go, db/meta_store_pebble_test.go,
  replica/handler_test.go, cfg/config_test.go. Every assertion carries a
  mutation proof re-executed by an independent review.
- Integration: TestClusterAutoCreateDatabaseOnConnect,
  TestClusterAutoCreateDatabaseConcurrentSafety (two nodes, same name,
  simultaneous connect: exactly one database, equal schema versions),
  TestClusterAutoCreateDatabaseDisabled.

Evidence (tree digest a7df0a3d986bd17f, 504 files)
- Unit set 25/25 ok; integration suite 33/33 ok; -race clean on touched
  packages except two pre-existing races in db/concurrent_conflict_test.go
  and db/heartbeat_gc_test.go (outside this change).
- Reporter's exact flow (LLDAP 0.6.3, no pre-created database) run three
  times against a binary built from this tree: single node, 3-node
  cluster, and auto_create_database=false: 3/3 runs, 27/27 assertions each
  (migrations reach v11, zero quorum errors, exactly one lldap database on
  every node, 13 tables, knob off yields 1049 and creates nothing).
- Two fresh-context reviews closed (9 findings + 4 follow-ups); no
  CRITICAL/HIGH found.

Out of scope, noted: pre-existing vet warnings in
db/vector_index_manager_test.go and grpc/forward_session.go; the two db
races above; TestInsertReplication is load-sensitive (verified identical
timing on master); test-only t.Logf %s on IntentKey in
db/insert_or_replace_test.go; SHOW DATABASES LIKE ignores its
pattern (unchanged from master). After this change LLDAP proceeds to
issue #170 (AUTO_INCREMENT ids exceed i32), which is by design.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SABpZUuBQPtum3EGAhUzrj
@maxpert maxpert closed this Sep 15, 2026
@maxpert
maxpert deleted the fix/auto-create-database branch September 15, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant