619 lines
16 KiB
Go
619 lines
16 KiB
Go
package migration
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"cmroubao/backend-api/internal/platform/database"
|
|
)
|
|
|
|
const (
|
|
claimsUserID = "00000000-0000-4000-8000-000000000401"
|
|
claimsDeviceID = "00000000-0000-4000-8000-000000000402"
|
|
claimsAssetID = "00000000-0000-4000-8000-000000000403"
|
|
claimsTaskID = "00000000-0000-4000-8000-000000000404"
|
|
claimsEventID = "00000000-0000-4000-8000-000000000405"
|
|
claimsExecutionID = "00000000-0000-4000-8000-000000000406"
|
|
claimsSecondAsset = "00000000-0000-4000-8000-000000000407"
|
|
claimsSecondTask = "00000000-0000-4000-8000-000000000408"
|
|
claimsNewEventID = "00000000-0000-4000-8000-000000000409"
|
|
claimsSecondExec = "00000000-0000-4000-8000-000000000410"
|
|
claimsTimestamp = "2026-07-26T12:00:00Z"
|
|
claimsExpiry = "2026-07-26T12:10:00Z"
|
|
claimsCreator = "local-admin"
|
|
claimsRequestSHA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
claimsDeviceSecret = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
)
|
|
|
|
func TestClaimsMigrationPreservesHistoryAcrossUpDownUp(t *testing.T) {
|
|
db, runner := openClaimsMigrationDatabase(t)
|
|
ctx := context.Background()
|
|
|
|
if applied, err := runner.Up(ctx); err != nil {
|
|
t.Fatalf("initial Up() error = %v", err)
|
|
} else if applied != 11 {
|
|
t.Fatalf("initial Up() applied = %d, want 11", applied)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("initial Down(v11) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("initial Down(v10) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("initial Down(v9) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("initial Down(v8) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("initial Down(v7) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("initial Down(v6) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("initial Down(v5) error = %v", err)
|
|
}
|
|
|
|
seedClaimsHistoricalFixture(t, db)
|
|
|
|
if applied, err := runner.Up(ctx); err != nil {
|
|
t.Fatalf("Up(v5-v11) over historical data error = %v", err)
|
|
} else if applied != 7 {
|
|
t.Fatalf("Up(v5-v11) applied = %d, want 7", applied)
|
|
}
|
|
assertClaimsHistory(t, db, true)
|
|
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v11) with compatible history error = %v", err)
|
|
}
|
|
assertClaimsHistory(t, db, true)
|
|
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v10) with compatible history error = %v", err)
|
|
}
|
|
assertClaimsHistory(t, db, true)
|
|
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v9) with compatible history error = %v", err)
|
|
}
|
|
assertClaimsHistory(t, db, true)
|
|
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v8) with compatible history error = %v", err)
|
|
}
|
|
assertClaimsHistory(t, db, true)
|
|
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v7) with compatible history error = %v", err)
|
|
}
|
|
assertClaimsHistory(t, db, true)
|
|
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v6) with compatible history error = %v", err)
|
|
}
|
|
assertClaimsHistory(t, db, true)
|
|
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v5) with compatible history error = %v", err)
|
|
}
|
|
assertClaimsHistory(t, db, true)
|
|
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v4) with compatible history error = %v", err)
|
|
}
|
|
assertClaimsHistory(t, db, false)
|
|
|
|
if applied, err := runner.Up(ctx); err != nil {
|
|
t.Fatalf("final Up(v4-v11) error = %v", err)
|
|
} else if applied != 8 {
|
|
t.Fatalf("final Up(v4-v11) applied = %d, want 8", applied)
|
|
}
|
|
assertClaimsHistory(t, db, true)
|
|
}
|
|
|
|
func TestClaimsMigrationEnforcesLifecycleConstraints(t *testing.T) {
|
|
db, runner := openClaimsMigrationDatabase(t)
|
|
if _, err := runner.Up(context.Background()); err != nil {
|
|
t.Fatalf("Up() error = %v", err)
|
|
}
|
|
seedClaimsHistoricalFixture(t, db)
|
|
insertClaimsTask(
|
|
t,
|
|
db,
|
|
claimsSecondAsset,
|
|
claimsSecondTask,
|
|
"second-source",
|
|
)
|
|
|
|
assertClaimsStatementRejected(
|
|
t,
|
|
db,
|
|
`UPDATE devices SET accessibility_enabled = 2 WHERE id = ?`,
|
|
claimsDeviceID,
|
|
)
|
|
assertClaimsStatementRejected(
|
|
t,
|
|
db,
|
|
`UPDATE devices SET pdd_installed = -1 WHERE id = ?`,
|
|
claimsDeviceID,
|
|
)
|
|
assertClaimsStatementRejected(
|
|
t,
|
|
db,
|
|
`UPDATE purchase_tasks SET claim_token_hash = ? WHERE id = ?`,
|
|
strings.Repeat("A", 64),
|
|
claimsTaskID,
|
|
)
|
|
|
|
if _, err := db.Exec(
|
|
`UPDATE purchase_tasks
|
|
SET status = 'CLAIMED',
|
|
claimed_by_user_id = ?,
|
|
claimed_by_device_id = ?,
|
|
claim_generation = 1,
|
|
claim_token_hash = ?,
|
|
claim_issued_at = ?,
|
|
claim_expires_at = ?
|
|
WHERE id = ?`,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsRequestSHA,
|
|
claimsTimestamp,
|
|
claimsExpiry,
|
|
claimsTaskID,
|
|
); err != nil {
|
|
t.Fatalf("set first active claim: %v", err)
|
|
}
|
|
assertClaimsStatementRejected(
|
|
t,
|
|
db,
|
|
`UPDATE purchase_tasks
|
|
SET status = 'CLAIMED',
|
|
claimed_by_user_id = ?,
|
|
claimed_by_device_id = ?,
|
|
claim_generation = 1,
|
|
claim_token_hash = ?,
|
|
claim_issued_at = ?,
|
|
claim_expires_at = ?
|
|
WHERE id = ?`,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsRequestSHA,
|
|
claimsTimestamp,
|
|
claimsExpiry,
|
|
claimsSecondTask,
|
|
)
|
|
|
|
assertClaimsStatementRejected(
|
|
t,
|
|
db,
|
|
`INSERT INTO task_executions (
|
|
id, task_id, attempt_no, claim_generation, user_id, device_id,
|
|
current_step, last_heartbeat_at, order_submitted, started_at
|
|
) VALUES (?, ?, 1, 1, ?, ?, 'STARTED', ?, 1, ?)`,
|
|
claimsExecutionID,
|
|
claimsTaskID,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsTimestamp,
|
|
claimsTimestamp,
|
|
)
|
|
if _, err := db.Exec(
|
|
`INSERT INTO task_executions (
|
|
id, task_id, attempt_no, claim_generation, user_id, device_id,
|
|
current_step, last_heartbeat_at, order_submitted, started_at
|
|
) VALUES (?, ?, 1, 1, ?, ?, 'STARTED', ?, 0, ?)`,
|
|
claimsExecutionID,
|
|
claimsTaskID,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsTimestamp,
|
|
claimsTimestamp,
|
|
); err != nil {
|
|
t.Fatalf("insert valid execution: %v", err)
|
|
}
|
|
assertClaimsStatementRejected(
|
|
t,
|
|
db,
|
|
`INSERT INTO task_executions (
|
|
id, task_id, attempt_no, claim_generation, user_id, device_id,
|
|
current_step, last_heartbeat_at, order_submitted, started_at
|
|
) VALUES (?, ?, 1, 1, ?, ?, ?, ?, 0, ?)`,
|
|
claimsSecondExec,
|
|
claimsSecondTask,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
strings.Repeat("A", 65),
|
|
claimsTimestamp,
|
|
claimsTimestamp,
|
|
)
|
|
|
|
if _, err := db.Exec(
|
|
`INSERT INTO lifecycle_requests (
|
|
user_id, device_id, operation, idempotency_key,
|
|
request_sha256, result_kind, created_at
|
|
) VALUES (?, ?, 'CLAIM_NEXT', 'no-task', ?, 'NO_TASK', ?)`,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsRequestSHA,
|
|
claimsTimestamp,
|
|
); err != nil {
|
|
t.Fatalf("insert valid NO_TASK replay: %v", err)
|
|
}
|
|
assertClaimsStatementRejected(
|
|
t,
|
|
db,
|
|
`INSERT INTO lifecycle_requests (
|
|
user_id, device_id, operation, idempotency_key,
|
|
request_sha256, result_kind, created_at
|
|
) VALUES (?, ?, 'START', 'invalid-no-task', ?, 'NO_TASK', ?)`,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsRequestSHA,
|
|
claimsTimestamp,
|
|
)
|
|
if _, err := db.Exec(
|
|
`INSERT INTO lifecycle_requests (
|
|
user_id, device_id, operation, idempotency_key,
|
|
request_sha256, result_kind, task_id, claim_generation,
|
|
execution_id, created_at
|
|
) VALUES (?, ?, 'START', 'valid-start', ?, 'EXECUTION', ?, 1, ?, ?)`,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsRequestSHA,
|
|
claimsTaskID,
|
|
claimsExecutionID,
|
|
claimsTimestamp,
|
|
); err != nil {
|
|
t.Fatalf("insert valid START replay: %v", err)
|
|
}
|
|
if _, err := db.Exec(
|
|
`INSERT INTO lifecycle_requests (
|
|
user_id, device_id, operation, idempotency_key,
|
|
request_sha256, result_kind, task_id, claim_generation, created_at
|
|
) VALUES (?, ?, 'RELEASE', 'valid-release', ?, 'TASK', ?, 1, ?)`,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsRequestSHA,
|
|
claimsTaskID,
|
|
claimsTimestamp,
|
|
); err != nil {
|
|
t.Fatalf("insert valid RELEASE replay: %v", err)
|
|
}
|
|
if _, err := db.Exec(
|
|
`INSERT INTO lifecycle_requests (
|
|
user_id, device_id, operation, idempotency_key,
|
|
request_sha256, result_kind, task_id, claim_generation,
|
|
execution_id, created_at
|
|
) VALUES (?, ?, 'CANCEL_ACK', 'valid-cancel-ack', ?, 'TASK', ?, 1, ?, ?)`,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsRequestSHA,
|
|
claimsTaskID,
|
|
claimsExecutionID,
|
|
claimsTimestamp,
|
|
); err != nil {
|
|
t.Fatalf("insert valid CANCEL_ACK replay: %v", err)
|
|
}
|
|
assertClaimsStatementRejected(
|
|
t,
|
|
db,
|
|
`INSERT INTO lifecycle_requests (
|
|
user_id, device_id, operation, idempotency_key,
|
|
request_sha256, result_kind, task_id, claim_generation, created_at
|
|
) VALUES (?, ?, 'CANCEL_ACK', 'cancel-ack-without-execution', ?,
|
|
'TASK', ?, 1, ?)`,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
claimsRequestSHA,
|
|
claimsTaskID,
|
|
claimsTimestamp,
|
|
)
|
|
assertClaimsStatementRejected(
|
|
t,
|
|
db,
|
|
`INSERT INTO task_events (
|
|
id, task_id, event_type, message, occurred_at
|
|
) VALUES (?, ?, 'TASK_UNKNOWN', 'unknown', ?)`,
|
|
claimsNewEventID,
|
|
claimsTaskID,
|
|
claimsTimestamp,
|
|
)
|
|
}
|
|
|
|
func TestClaimsMigrationDownFailsClosedForNewAuditData(t *testing.T) {
|
|
db, runner := openClaimsMigrationDatabase(t)
|
|
ctx := context.Background()
|
|
if _, err := runner.Up(ctx); err != nil {
|
|
t.Fatalf("Up() error = %v", err)
|
|
}
|
|
seedClaimsHistoricalFixture(t, db)
|
|
if _, err := db.Exec(
|
|
`INSERT INTO task_events (
|
|
id, task_id, event_type, message, occurred_at,
|
|
actor_user_id, actor_device_id
|
|
) VALUES (?, ?, 'TASK_CLAIMED', 'claimed', ?, ?, ?)`,
|
|
claimsNewEventID,
|
|
claimsTaskID,
|
|
claimsTimestamp,
|
|
claimsUserID,
|
|
claimsDeviceID,
|
|
); err != nil {
|
|
t.Fatalf("insert v4 audit event: %v", err)
|
|
}
|
|
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v11) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v10) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v9) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v8) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v7) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v6) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err != nil {
|
|
t.Fatalf("Down(v5) error = %v", err)
|
|
}
|
|
if err := runner.Down(ctx); err == nil {
|
|
t.Fatal("Down(v4) succeeded with non-representable audit event")
|
|
}
|
|
if !claimsColumnExists(t, db, "task_events", "actor_device_id") {
|
|
t.Fatal("failed Down(v4) partially replaced task_events")
|
|
}
|
|
if !claimsTableExists(t, db, "task_executions") {
|
|
t.Fatal("failed Down(v4) partially removed task_executions")
|
|
}
|
|
var count int
|
|
if err := db.QueryRow(
|
|
"SELECT COUNT(*) FROM task_events WHERE id = ?",
|
|
claimsNewEventID,
|
|
).Scan(&count); err != nil {
|
|
t.Fatalf("query retained audit event: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("retained audit event count = %d, want 1", count)
|
|
}
|
|
}
|
|
|
|
func openClaimsMigrationDatabase(
|
|
t *testing.T,
|
|
) (*sql.DB, *Runner) {
|
|
t.Helper()
|
|
db, err := database.Open(
|
|
context.Background(),
|
|
filepath.Join(t.TempDir(), "claims-migration.db"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("database.Open() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
runner, err := New(db)
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
return db, runner
|
|
}
|
|
|
|
func seedClaimsHistoricalFixture(t *testing.T, db *sql.DB) {
|
|
t.Helper()
|
|
statements := []struct {
|
|
query string
|
|
args []any
|
|
}{
|
|
{
|
|
`INSERT INTO users (
|
|
id, username, password_hash, role, is_active, created_at, updated_at
|
|
) VALUES (?, 'buyer', 'hash', 'BUYER', 1, ?, ?)`,
|
|
[]any{claimsUserID, claimsTimestamp, claimsTimestamp},
|
|
},
|
|
{
|
|
`INSERT INTO devices (
|
|
id, name, token_hash, bound_user_id, is_enabled, created_at, updated_at
|
|
) VALUES (?, 'historical device', ?, ?, 1, ?, ?)`,
|
|
[]any{
|
|
claimsDeviceID,
|
|
claimsDeviceSecret,
|
|
claimsUserID,
|
|
claimsTimestamp,
|
|
claimsTimestamp,
|
|
},
|
|
},
|
|
}
|
|
for _, statement := range statements {
|
|
if _, err := db.Exec(statement.query, statement.args...); err != nil {
|
|
t.Fatalf("seed historical auth data: %v", err)
|
|
}
|
|
}
|
|
insertClaimsTask(t, db, claimsAssetID, claimsTaskID, "historical-source")
|
|
if _, err := db.Exec(
|
|
`INSERT INTO task_events (
|
|
id, task_id, event_type, message, occurred_at, actor_user_id
|
|
) VALUES (?, ?, 'TASK_CREATED', 'created', ?, ?)`,
|
|
claimsEventID,
|
|
claimsTaskID,
|
|
claimsTimestamp,
|
|
claimsUserID,
|
|
); err != nil {
|
|
t.Fatalf("seed historical task event: %v", err)
|
|
}
|
|
}
|
|
|
|
func insertClaimsTask(
|
|
t *testing.T,
|
|
db *sql.DB,
|
|
assetID string,
|
|
taskID string,
|
|
sourceRef string,
|
|
) {
|
|
t.Helper()
|
|
if _, err := db.Exec(
|
|
`INSERT INTO assets (
|
|
id, creator_subject, purpose, media_type, size_bytes,
|
|
sha256, storage_key, created_at
|
|
) VALUES (?, ?, 'TASK_REFERENCE', 'image/jpeg', 10, ?, ?, ?)`,
|
|
assetID,
|
|
claimsCreator,
|
|
claimsRequestSHA,
|
|
"assets/"+assetID+".jpg",
|
|
claimsTimestamp,
|
|
); err != nil {
|
|
t.Fatalf("insert asset %s: %v", assetID, err)
|
|
}
|
|
if _, err := db.Exec(
|
|
`INSERT INTO purchase_tasks (
|
|
id, creator_subject, source_ref, title, description, sku,
|
|
image_asset_id, quantity, currency, status, version,
|
|
created_at, updated_at, created_by_user_id
|
|
) VALUES (?, ?, ?, 'title', 'description', 'sku', ?, 1, 'CNY',
|
|
'PENDING', 1, ?, ?, ?)`,
|
|
taskID,
|
|
claimsCreator,
|
|
sourceRef,
|
|
assetID,
|
|
claimsTimestamp,
|
|
claimsTimestamp,
|
|
claimsUserID,
|
|
); err != nil {
|
|
t.Fatalf("insert task %s: %v", taskID, err)
|
|
}
|
|
}
|
|
|
|
func assertClaimsHistory(t *testing.T, db *sql.DB, v4 bool) {
|
|
t.Helper()
|
|
var taskCount int
|
|
if err := db.QueryRow(
|
|
"SELECT COUNT(*) FROM purchase_tasks WHERE id = ?",
|
|
claimsTaskID,
|
|
).Scan(&taskCount); err != nil {
|
|
t.Fatalf("query historical task: %v", err)
|
|
}
|
|
if taskCount != 1 {
|
|
t.Fatalf("historical task count = %d, want 1", taskCount)
|
|
}
|
|
var actorUserID sql.NullString
|
|
if err := db.QueryRow(
|
|
"SELECT actor_user_id FROM task_events WHERE id = ?",
|
|
claimsEventID,
|
|
).Scan(&actorUserID); err != nil {
|
|
t.Fatalf("query historical event actor: %v", err)
|
|
}
|
|
if !actorUserID.Valid || actorUserID.String != claimsUserID {
|
|
t.Fatalf("historical event actor = %v, want %s", actorUserID, claimsUserID)
|
|
}
|
|
if claimsColumnExists(
|
|
t,
|
|
db,
|
|
"purchase_tasks",
|
|
"claim_generation",
|
|
) != v4 {
|
|
t.Fatalf("claim_generation presence does not match v4=%t", v4)
|
|
}
|
|
if claimsColumnExists(
|
|
t,
|
|
db,
|
|
"task_events",
|
|
"actor_device_id",
|
|
) != v4 {
|
|
t.Fatalf("actor_device_id presence does not match v4=%t", v4)
|
|
}
|
|
if claimsTableExists(t, db, "task_executions") != v4 {
|
|
t.Fatalf("task_executions presence does not match v4=%t", v4)
|
|
}
|
|
if v4 {
|
|
var generation int
|
|
var tokenHash sql.NullString
|
|
if err := db.QueryRow(
|
|
`SELECT claim_generation, claim_token_hash
|
|
FROM purchase_tasks WHERE id = ?`,
|
|
claimsTaskID,
|
|
).Scan(&generation, &tokenHash); err != nil {
|
|
t.Fatalf("query historical claim defaults: %v", err)
|
|
}
|
|
if generation != 0 || tokenHash.Valid {
|
|
t.Fatalf(
|
|
"historical claim defaults generation=%d token=%v",
|
|
generation,
|
|
tokenHash,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertClaimsStatementRejected(
|
|
t *testing.T,
|
|
db *sql.DB,
|
|
query string,
|
|
args ...any,
|
|
) {
|
|
t.Helper()
|
|
if _, err := db.Exec(query, args...); err == nil {
|
|
t.Fatalf("statement unexpectedly succeeded: %s", query)
|
|
}
|
|
}
|
|
|
|
func claimsTableExists(t *testing.T, db *sql.DB, name string) bool {
|
|
t.Helper()
|
|
var count int
|
|
if err := db.QueryRow(
|
|
`SELECT COUNT(*) FROM sqlite_master
|
|
WHERE type = 'table' AND name = ?`,
|
|
name,
|
|
).Scan(&count); err != nil {
|
|
t.Fatalf("query table %s: %v", name, err)
|
|
}
|
|
return count == 1
|
|
}
|
|
|
|
func claimsColumnExists(
|
|
t *testing.T,
|
|
db *sql.DB,
|
|
table string,
|
|
column string,
|
|
) bool {
|
|
t.Helper()
|
|
rows, err := db.Query("PRAGMA table_info(" + table + ")")
|
|
if err != nil {
|
|
t.Fatalf("PRAGMA table_info(%s): %v", table, err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var cid int
|
|
var name string
|
|
var dataType string
|
|
var notNull int
|
|
var defaultValue any
|
|
var primaryKey int
|
|
if err := rows.Scan(
|
|
&cid,
|
|
&name,
|
|
&dataType,
|
|
¬Null,
|
|
&defaultValue,
|
|
&primaryKey,
|
|
); err != nil {
|
|
t.Fatalf("scan table_info(%s): %v", table, err)
|
|
}
|
|
if name == column {
|
|
return true
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
t.Fatalf("iterate table_info(%s): %v", table, err)
|
|
}
|
|
return false
|
|
}
|