fix: ALTER spam в логах — DDL на qt_client_schema_state только при нужде
Раньше ensureClientSchemaStateTable запускался на каждом цикле синка (каждые 5 минут) и пытался ALTER TABLE, даже если все колонки уже были. Для пользователей без DDL-прав это давало WARN-спам в каждом цикле. Два изменения: - schemaOnce (sync.Once) на Service: ensureClientSchemaStateTable вызывается не более одного раза за жизнь процесса - columnExists() проверяет information_schema.COLUMNS перед каждым ALTER — если колонка уже есть, ALTER пропускается без ошибки Если таблица уже мигрирована сервером, клиент молча пропускает все DDL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,11 @@ func (s *Service) GetReadiness() (*SyncReadiness, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.schemaOnce.Do(func() {
|
||||||
|
if err := ensureClientSchemaStateTable(mariaDB); err != nil {
|
||||||
|
slog.Warn("qt_client_schema_state migration skipped (no DDL rights — run server migrate)", "error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
if err := s.reportClientSchemaState(mariaDB, now); err != nil {
|
if err := s.reportClientSchemaState(mariaDB, now); err != nil {
|
||||||
slog.Warn("failed to report client schema state", "error", err)
|
slog.Warn("failed to report client schema state", "error", err)
|
||||||
}
|
}
|
||||||
@@ -141,39 +146,51 @@ func ensureClientSchemaStateTable(db *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tableExists(db, "qt_client_schema_state") {
|
if tableExists(db, "qt_client_schema_state") {
|
||||||
if err := db.Exec(`
|
// Each ALTER is guarded by a column existence check so users without DDL
|
||||||
ALTER TABLE qt_client_schema_state
|
// rights don't get a permission error on every sync cycle — the server
|
||||||
ADD COLUMN IF NOT EXISTS hostname VARCHAR(255) NOT NULL DEFAULT '' AFTER username
|
// migration tool is the authoritative path for schema changes.
|
||||||
`).Error; err != nil {
|
if !columnExists(db, "qt_client_schema_state", "hostname") {
|
||||||
return fmt.Errorf("add qt_client_schema_state.hostname: %w", err)
|
if err := db.Exec(`
|
||||||
|
ALTER TABLE qt_client_schema_state
|
||||||
|
ADD COLUMN IF NOT EXISTS hostname VARCHAR(255) NOT NULL DEFAULT '' AFTER username
|
||||||
|
`).Error; err != nil {
|
||||||
|
return fmt.Errorf("add qt_client_schema_state.hostname: %w", err)
|
||||||
|
}
|
||||||
|
if err := db.Exec(`
|
||||||
|
ALTER TABLE qt_client_schema_state
|
||||||
|
DROP PRIMARY KEY,
|
||||||
|
ADD PRIMARY KEY (username, hostname)
|
||||||
|
`).Error; err != nil && !isDuplicatePrimaryKeyDefinition(err) {
|
||||||
|
return fmt.Errorf("set qt_client_schema_state primary key: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.Exec(`
|
type colMigration struct {
|
||||||
ALTER TABLE qt_client_schema_state
|
column string
|
||||||
DROP PRIMARY KEY,
|
stmt string
|
||||||
ADD PRIMARY KEY (username, hostname)
|
|
||||||
`).Error; err != nil && !isDuplicatePrimaryKeyDefinition(err) {
|
|
||||||
return fmt.Errorf("set qt_client_schema_state primary key: %w", err)
|
|
||||||
}
|
}
|
||||||
|
migrations := []colMigration{
|
||||||
for _, stmt := range []string{
|
{"last_sync_at", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_at DATETIME NULL AFTER app_version"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_at DATETIME NULL AFTER app_version",
|
{"last_sync_status", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_status VARCHAR(32) NULL AFTER last_sync_at"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_status VARCHAR(32) NULL AFTER last_sync_at",
|
{"pending_changes_count", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS pending_changes_count INT NOT NULL DEFAULT 0 AFTER last_sync_status"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS pending_changes_count INT NOT NULL DEFAULT 0 AFTER last_sync_status",
|
{"pending_errors_count", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS pending_errors_count INT NOT NULL DEFAULT 0 AFTER pending_changes_count"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS pending_errors_count INT NOT NULL DEFAULT 0 AFTER pending_changes_count",
|
{"configurations_count", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS configurations_count INT NOT NULL DEFAULT 0 AFTER pending_errors_count"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS configurations_count INT NOT NULL DEFAULT 0 AFTER pending_errors_count",
|
{"projects_count", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS projects_count INT NOT NULL DEFAULT 0 AFTER configurations_count"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS projects_count INT NOT NULL DEFAULT 0 AFTER configurations_count",
|
{"estimate_pricelist_version", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS estimate_pricelist_version VARCHAR(128) NULL AFTER projects_count"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS estimate_pricelist_version VARCHAR(128) NULL AFTER projects_count",
|
{"warehouse_pricelist_version", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS warehouse_pricelist_version VARCHAR(128) NULL AFTER estimate_pricelist_version"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS warehouse_pricelist_version VARCHAR(128) NULL AFTER estimate_pricelist_version",
|
{"competitor_pricelist_version", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS competitor_pricelist_version VARCHAR(128) NULL AFTER warehouse_pricelist_version"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS competitor_pricelist_version VARCHAR(128) NULL AFTER warehouse_pricelist_version",
|
{"last_sync_error_code", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_error_code VARCHAR(128) NULL AFTER competitor_pricelist_version"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_error_code VARCHAR(128) NULL AFTER competitor_pricelist_version",
|
{"last_sync_error_text", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_error_text TEXT NULL AFTER last_sync_error_code"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS last_sync_error_text TEXT NULL AFTER last_sync_error_code",
|
{"local_pricelist_count", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS local_pricelist_count INT NOT NULL DEFAULT 0 AFTER last_sync_error_text"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS local_pricelist_count INT NOT NULL DEFAULT 0 AFTER last_sync_error_text",
|
{"pricelist_items_count", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS pricelist_items_count INT NOT NULL DEFAULT 0 AFTER local_pricelist_count"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS pricelist_items_count INT NOT NULL DEFAULT 0 AFTER local_pricelist_count",
|
{"components_count", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS components_count INT NOT NULL DEFAULT 0 AFTER pricelist_items_count"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS components_count INT NOT NULL DEFAULT 0 AFTER pricelist_items_count",
|
{"db_size_bytes", "ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS db_size_bytes BIGINT NOT NULL DEFAULT 0 AFTER components_count"},
|
||||||
"ALTER TABLE qt_client_schema_state ADD COLUMN IF NOT EXISTS db_size_bytes BIGINT NOT NULL DEFAULT 0 AFTER components_count",
|
}
|
||||||
} {
|
for _, m := range migrations {
|
||||||
if err := db.Exec(stmt).Error; err != nil {
|
if columnExists(db, "qt_client_schema_state", m.column) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := db.Exec(m.stmt).Error; err != nil {
|
||||||
return fmt.Errorf("expand qt_client_schema_state: %w", err)
|
return fmt.Errorf("expand qt_client_schema_state: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,6 +198,17 @@ func ensureClientSchemaStateTable(db *gorm.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func columnExists(db *gorm.DB, tableName, columnName string) bool {
|
||||||
|
var count int64
|
||||||
|
if err := db.Raw(`
|
||||||
|
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?
|
||||||
|
`, tableName, columnName).Scan(&count).Error; err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return count > 0
|
||||||
|
}
|
||||||
|
|
||||||
func tableExists(db *gorm.DB, tableName string) bool {
|
func tableExists(db *gorm.DB, tableName string) bool {
|
||||||
var count int64
|
var count int64
|
||||||
// For MariaDB/MySQL, check information_schema
|
// For MariaDB/MySQL, check information_schema
|
||||||
@@ -197,9 +225,6 @@ func (s *Service) reportClientSchemaState(mariaDB *gorm.DB, checkedAt time.Time)
|
|||||||
if strings.EqualFold(mariaDB.Dialector.Name(), "sqlite") {
|
if strings.EqualFold(mariaDB.Dialector.Name(), "sqlite") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := ensureClientSchemaStateTable(mariaDB); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
username := strings.TrimSpace(s.localDB.GetDBUser())
|
username := strings.TrimSpace(s.localDB.GetDBUser())
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type Service struct {
|
|||||||
localDB *localdb.LocalDB
|
localDB *localdb.LocalDB
|
||||||
directDB *gorm.DB
|
directDB *gorm.DB
|
||||||
pricelistMu sync.Mutex // prevents concurrent pricelist syncs
|
pricelistMu sync.Mutex // prevents concurrent pricelist syncs
|
||||||
|
schemaOnce sync.Once // ensures ensureClientSchemaStateTable runs at most once per process
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new sync service
|
// NewService creates a new sync service
|
||||||
|
|||||||
Reference in New Issue
Block a user