GoogleCloudPlatform / GoogleCloudPlatform/cloud-spanner-emulator

Spanner Emulator: UNIQUE constraint violation detection takes ~7 seconds

Open
#291 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
334
Forks
77
Avg merge
8m
Merged PRs (30d)
2

Description

When inserting a duplicate value that violates a UNIQUE INDEX constraint, the Spanner Emulator takes approximately 7 seconds to detect and return the error. This delay is significantly longer than expected and may impact development and testing workflows.

### Environment
- **Spanner Emulator**: Running on localhost:9010
- **@google-cloud/spanner**: ^7.0.0

### Steps to Reproduce

1. Create a table with a UNIQUE INDEX on a column (e.g., `email`)
2. Insert a record successfully
3. Attempt to insert another record with the same value for the unique column
4. Observe the delay before the error is returned

### Expected Behavior

The UNIQUE constraint violation should be detected and an error should be returned immediately (within milliseconds).

### Actual Behavior

The error is returned after approximately 7 seconds, which suggests the emulator may be performing unnecessary operations or timeouts before detecting the constraint violation.

### Code to Reproduce

```typescript
import { Spanner } from "@google-cloud/spanner";
import { randomUUID } from "crypto";

const ALREADY_EXISTS = 6;
const projectId = "dev-project";
const instanceId = "dev-instance";
const databaseId = "dev-database";
process.env.SPANNER_EMULATOR_HOST = "localhost:9010";

function log(...args: any[]): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}]`, ...args);
}

const spanner = new Spanner({
projectId: projectId,
});

export const db = spanner.instance(instanceId).database(databaseId);

interface User {
id: string;
name: string;
email: string;
}

async function createUser(user: User): Promise {
try {
return await db.runTransactionAsync(async (tx) => {
const [rows] = await tx.run({
sql: `INSERT users (id, name, email)
VALUES (@id, @name, @email)
THEN RETURN *`,
params: {
id: user.id,
name: user.name,
email: user.email
},
});

await tx.commit();
return rows[0].toJSON() as User;
});
} catch (error) {
if (error && typeof error === "object" && "code" in error) {
const grpcCode = (error as { code: number }).code;

if (grpcCode === ALREADY_EXISTS) {
throw new Error(`User with this email already exists: ${user.email}`);
}
}

throw new Error(`Failed to create user: ${error}`);
}
}

async function main() {
const testEmail = "test@example.com";

try {
log("Attempting to create first user...");
await createUser({
id: randomUUID(),
name: "Test User 1",
email: testEmail,
});
log("✓ Successfully created first user");

log("\nAttempting to create second user (same email)...");
await createUser({
id: randomUUID(),
name: "Test User 2",
email: testEmail, // same email
});
log("✗ No error occurred (unexpected behavior)");
} catch (error: any) {
log("Error message:", error.message);
if (error && typeof error === "object" && "code" in error) {
log("Error code:", (error as { code: number }).code);
if ((error as { code: number }).code === ALREADY_EXISTS) {
log("→ UNIQUE INDEX violation: User with this email already exists");
}
}
if (error.details) {
log("Error details:", JSON.stringify(error.details, null, 2));
}
}
}

main()
```

### Database Schema

```sql
CREATE TABLE users (
id STRING(36) NOT NULL,
name STRING(100) NOT NULL,
email STRING(255) NOT NULL
) PRIMARY KEY(id);

CREATE UNIQUE INDEX idx_users_email ON users(email);
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.