package stats import ( "context" "database/sql" "fmt" "regexp" "strings" ) const HydrogenTankCapacityTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_hydrogen_tank_capacity ( vin VARCHAR(64) NOT NULL, source_vehicle_id BIGINT NOT NULL, source_model_id BIGINT NULL, plate_number VARCHAR(50) NOT NULL DEFAULT '', brand_name VARCHAR(500) NOT NULL DEFAULT '', model_name VARCHAR(500) NOT NULL DEFAULT '', tank_capacity_l DECIMAL(12,2) NOT NULL, active TINYINT(1) NOT NULL DEFAULT 1, source_updated_at DATETIME NULL, synced_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (vin), KEY idx_hydrogen_capacity_model (source_model_id), KEY idx_hydrogen_capacity_active (active,vin) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` type HydrogenCapacitySyncResult struct { Read int Written int Deactivated int64 } type hydrogenCapacityRow struct { VIN string SourceVehicleID int64 SourceModelID sql.NullInt64 Plate string Brand string Model string CapacityLiters float64 SourceUpdatedAt sql.NullTime } var mysqlIdentifier = regexp.MustCompile(`^[A-Za-z0-9_]+$`) func SyncHydrogenTankCapacities(ctx context.Context, db *sql.DB, sourceSchema string) (HydrogenCapacitySyncResult, error) { result := HydrogenCapacitySyncResult{} if db == nil { return result, fmt.Errorf("mysql database is required") } sourceSchema = strings.TrimSpace(sourceSchema) if !mysqlIdentifier.MatchString(sourceSchema) { return result, fmt.Errorf("invalid asset schema %q", sourceSchema) } query := fmt.Sprintf(`SELECT vi.id,vi.vehicle_model_id,UPPER(TRIM(vi.vin)),COALESCE(vi.plate_number,''), COALESCE(vm.brand,''),COALESCE(vm.model,''),vm.tank_capacity, CASE WHEN vi.update_time IS NULL THEN vm.update_time WHEN vm.update_time IS NULL THEN vi.update_time ELSE GREATEST(vi.update_time,vm.update_time) END FROM %s.vehicle_info vi JOIN %s.vehicle_model vm ON vm.id=vi.vehicle_model_id WHERE COALESCE(vi.del_flag,'0')='0' AND COALESCE(vm.del_flag,'0')='0' AND LENGTH(TRIM(vi.vin))=17 AND vm.tank_capacity>0`, sourceSchema, sourceSchema) rows, err := db.QueryContext(ctx, query) if err != nil { return result, fmt.Errorf("read asset hydrogen capacities: %w", err) } byVIN := map[string]hydrogenCapacityRow{} for rows.Next() { var row hydrogenCapacityRow if err := rows.Scan(&row.SourceVehicleID, &row.SourceModelID, &row.VIN, &row.Plate, &row.Brand, &row.Model, &row.CapacityLiters, &row.SourceUpdatedAt); err != nil { rows.Close() return result, err } result.Read++ if row.CapacityLiters <= 0 || row.CapacityLiters > 10000 { continue } if previous, exists := byVIN[row.VIN]; !exists || newerCapacityRow(row, previous) { byVIN[row.VIN] = row } } if err := rows.Close(); err != nil { return result, err } tx, err := db.BeginTx(ctx, nil) if err != nil { return result, err } defer tx.Rollback() _, err = tx.ExecContext(ctx, `UPDATE vehicle_hydrogen_tank_capacity SET active=0 WHERE active=1`) if err != nil { return result, err } const upsert = `INSERT INTO vehicle_hydrogen_tank_capacity( vin,source_vehicle_id,source_model_id,plate_number,brand_name,model_name,tank_capacity_l,active,source_updated_at,synced_at ) VALUES(?,?,?,?,?,?,?,1,?,NOW(3)) ON DUPLICATE KEY UPDATE source_vehicle_id=VALUES(source_vehicle_id),source_model_id=VALUES(source_model_id), plate_number=VALUES(plate_number),brand_name=VALUES(brand_name),model_name=VALUES(model_name), tank_capacity_l=VALUES(tank_capacity_l),active=1,source_updated_at=VALUES(source_updated_at),synced_at=NOW(3)` for _, row := range byVIN { if _, err := tx.ExecContext(ctx, upsert, row.VIN, row.SourceVehicleID, row.SourceModelID, row.Plate, row.Brand, row.Model, row.CapacityLiters, row.SourceUpdatedAt); err != nil { return result, err } result.Written++ } if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM vehicle_hydrogen_tank_capacity WHERE active=0`).Scan(&result.Deactivated); err != nil { return result, err } if err := tx.Commit(); err != nil { return result, err } return result, nil } func newerCapacityRow(left, right hydrogenCapacityRow) bool { if left.SourceUpdatedAt.Valid != right.SourceUpdatedAt.Valid { return left.SourceUpdatedAt.Valid } if left.SourceUpdatedAt.Valid && !left.SourceUpdatedAt.Time.Equal(right.SourceUpdatedAt.Time) { return left.SourceUpdatedAt.Time.After(right.SourceUpdatedAt.Time) } return left.SourceVehicleID > right.SourceVehicleID } func LoadHydrogenTankCapacities(ctx context.Context, query Queryer) (map[string]float64, error) { if query == nil { return nil, fmt.Errorf("mysql queryer is required") } rows, err := query.QueryContext(ctx, `SELECT UPPER(TRIM(vin)),tank_capacity_l FROM vehicle_hydrogen_tank_capacity WHERE active=1 AND tank_capacity_l>0`) if err != nil { return nil, err } defer rows.Close() capacities := map[string]float64{} for rows.Next() { var vin string var capacity float64 if err := rows.Scan(&vin, &capacity); err != nil { return nil, err } if len(vin) == 17 && capacity > 0 && capacity <= 10000 { capacities[vin] = capacity } } return capacities, rows.Err() }